lucid 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. data/.gitignore +30 -10
  2. data/.rspec +1 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +15 -0
  6. data/Gemfile +4 -2
  7. data/HISTORY.md +22 -0
  8. data/{LICENSE.txt → LICENSE} +6 -3
  9. data/README.md +22 -8
  10. data/Rakefile +2 -1
  11. data/bin/lucid +10 -10
  12. data/bin/lucid-gen +4 -0
  13. data/lib/autotest/discover.rb +11 -0
  14. data/lib/autotest/lucid.rb +6 -0
  15. data/lib/autotest/lucid_mixin.rb +135 -0
  16. data/lib/autotest/lucid_rails.rb +6 -0
  17. data/lib/autotest/lucid_rails_rspec.rb +6 -0
  18. data/lib/autotest/lucid_rails_rspec2.rb +6 -0
  19. data/lib/autotest/lucid_rspec.rb +6 -0
  20. data/lib/autotest/lucid_rspec2.rb +6 -0
  21. data/lib/lucid.rb +32 -1
  22. data/lib/lucid/ast.rb +20 -0
  23. data/lib/lucid/ast/background.rb +116 -0
  24. data/lib/lucid/ast/comment.rb +24 -0
  25. data/lib/lucid/ast/doc_string.rb +44 -0
  26. data/lib/lucid/ast/empty_background.rb +33 -0
  27. data/lib/lucid/ast/examples.rb +49 -0
  28. data/lib/lucid/ast/feature.rb +99 -0
  29. data/lib/lucid/ast/has_steps.rb +74 -0
  30. data/lib/lucid/ast/location.rb +41 -0
  31. data/lib/lucid/ast/multiline_argument.rb +31 -0
  32. data/lib/lucid/ast/names.rb +13 -0
  33. data/lib/lucid/ast/outline_table.rb +194 -0
  34. data/lib/lucid/ast/scenario.rb +103 -0
  35. data/lib/lucid/ast/scenario_outline.rb +144 -0
  36. data/lib/lucid/ast/specs.rb +38 -0
  37. data/lib/lucid/ast/step.rb +122 -0
  38. data/lib/lucid/ast/step_collection.rb +92 -0
  39. data/lib/lucid/ast/step_invocation.rb +196 -0
  40. data/lib/lucid/ast/table.rb +730 -0
  41. data/lib/lucid/ast/tags.rb +28 -0
  42. data/lib/lucid/ast/tdl_walker.rb +195 -0
  43. data/lib/lucid/cli/app.rb +78 -0
  44. data/lib/lucid/cli/configuration.rb +261 -0
  45. data/lib/lucid/cli/options.rb +463 -0
  46. data/lib/lucid/cli/profile.rb +101 -0
  47. data/lib/lucid/configuration.rb +53 -0
  48. data/lib/lucid/core_ext/disable_autorunners.rb +15 -0
  49. data/lib/lucid/core_ext/instance_exec.rb +70 -0
  50. data/lib/lucid/core_ext/proc.rb +36 -0
  51. data/lib/lucid/core_ext/string.rb +9 -0
  52. data/lib/lucid/errors.rb +40 -0
  53. data/lib/lucid/factory.rb +43 -0
  54. data/lib/lucid/formatter/ansicolor.rb +168 -0
  55. data/lib/lucid/formatter/console.rb +218 -0
  56. data/lib/lucid/formatter/debug.rb +33 -0
  57. data/lib/lucid/formatter/duration.rb +11 -0
  58. data/lib/lucid/formatter/gherkin_formatter_adapter.rb +94 -0
  59. data/lib/lucid/formatter/gpretty.rb +24 -0
  60. data/lib/lucid/formatter/html.rb +610 -0
  61. data/lib/lucid/formatter/interceptor.rb +66 -0
  62. data/lib/lucid/formatter/io.rb +31 -0
  63. data/lib/lucid/formatter/jquery-min.js +154 -0
  64. data/lib/lucid/formatter/json.rb +19 -0
  65. data/lib/lucid/formatter/json_pretty.rb +10 -0
  66. data/lib/lucid/formatter/junit.rb +177 -0
  67. data/lib/lucid/formatter/lucid.css +283 -0
  68. data/lib/lucid/formatter/lucid.sass +244 -0
  69. data/lib/lucid/formatter/ordered_xml_markup.rb +24 -0
  70. data/lib/lucid/formatter/progress.rb +95 -0
  71. data/lib/lucid/formatter/rerun.rb +91 -0
  72. data/lib/lucid/formatter/standard.rb +235 -0
  73. data/lib/lucid/formatter/stepdefs.rb +14 -0
  74. data/lib/lucid/formatter/steps.rb +49 -0
  75. data/lib/lucid/formatter/summary.rb +35 -0
  76. data/lib/lucid/formatter/unicode.rb +53 -0
  77. data/lib/lucid/formatter/usage.rb +132 -0
  78. data/lib/lucid/generator.rb +21 -0
  79. data/lib/lucid/generators/project.rb +70 -0
  80. data/lib/lucid/generators/project/Gemfile.tt +6 -0
  81. data/lib/lucid/generators/project/browser-symbiont.rb +24 -0
  82. data/lib/lucid/generators/project/driver-symbiont.rb +4 -0
  83. data/lib/lucid/generators/project/errors.rb +26 -0
  84. data/lib/lucid/generators/project/events-symbiont.rb +36 -0
  85. data/lib/lucid/generators/project/lucid-symbiont.yml +6 -0
  86. data/lib/lucid/interface.rb +8 -0
  87. data/lib/lucid/interface_methods.rb +125 -0
  88. data/lib/lucid/interface_rb/matcher.rb +108 -0
  89. data/lib/lucid/interface_rb/rb_hook.rb +18 -0
  90. data/lib/lucid/interface_rb/rb_language.rb +190 -0
  91. data/lib/lucid/interface_rb/rb_lucid.rb +119 -0
  92. data/lib/lucid/interface_rb/rb_step_definition.rb +122 -0
  93. data/lib/lucid/interface_rb/rb_transform.rb +57 -0
  94. data/lib/lucid/interface_rb/rb_world.rb +136 -0
  95. data/lib/lucid/interface_rb/regexp_argument_matcher.rb +21 -0
  96. data/lib/lucid/load_path.rb +13 -0
  97. data/lib/lucid/parser.rb +2 -126
  98. data/lib/lucid/platform.rb +27 -0
  99. data/lib/lucid/rspec/allow_doubles.rb +20 -0
  100. data/lib/lucid/rspec/disallow_options.rb +27 -0
  101. data/lib/lucid/runtime.rb +200 -0
  102. data/lib/lucid/runtime/facade.rb +60 -0
  103. data/lib/lucid/runtime/interface_io.rb +60 -0
  104. data/lib/lucid/runtime/orchestrator.rb +218 -0
  105. data/lib/lucid/runtime/results.rb +64 -0
  106. data/lib/lucid/runtime/specs_loader.rb +79 -0
  107. data/lib/lucid/spec_file.rb +112 -0
  108. data/lib/lucid/step_definition_light.rb +20 -0
  109. data/lib/lucid/step_definitions.rb +13 -0
  110. data/lib/lucid/step_match.rb +99 -0
  111. data/lib/lucid/tdl_builder.rb +282 -0
  112. data/lib/lucid/term/ansicolor.rb +118 -0
  113. data/lib/lucid/unit.rb +11 -0
  114. data/lib/lucid/wire_support/configuration.rb +38 -0
  115. data/lib/lucid/wire_support/connection.rb +61 -0
  116. data/lib/lucid/wire_support/request_handler.rb +32 -0
  117. data/lib/lucid/wire_support/wire_exception.rb +32 -0
  118. data/lib/lucid/wire_support/wire_language.rb +54 -0
  119. data/lib/lucid/wire_support/wire_packet.rb +34 -0
  120. data/lib/lucid/wire_support/wire_protocol.rb +43 -0
  121. data/lib/lucid/wire_support/wire_protocol/requests.rb +125 -0
  122. data/lib/lucid/wire_support/wire_step_definition.rb +26 -0
  123. data/lucid.gemspec +25 -14
  124. metadata +220 -12
  125. data/lib/lucid/app.rb +0 -103
  126. data/lib/lucid/options.rb +0 -168
  127. data/lib/lucid/version.rb +0 -3
  128. data/lucid.yml +0 -8
@@ -0,0 +1,218 @@
1
+ require 'lucid/formatter/ansicolor'
2
+ require 'lucid/formatter/duration'
3
+ require 'lucid/formatter/summary'
4
+
5
+ module Lucid
6
+ module Formatter
7
+
8
+ # This module contains helper methods that are used by formatters that
9
+ # print output to the terminal.
10
+ #
11
+ # FORMAT is a hash of Proc objects, keyed by step-definition types, e.g.
12
+ # "FORMAT[:passed]". The Proc is called for each line of the step's
13
+ # output.
14
+ #
15
+ # format_step calls format_string, format_string calls format_for to obtain
16
+ # the formatting Proc.
17
+ #
18
+ # Example:
19
+ #
20
+ # The ANSI color console formatter defines a map of step-type to output
21
+ # color (e.g. "passed" to "green"), then builds methods named for the
22
+ # step-types (e.g. "def passed"), which themselves wrap the corresponding
23
+ # color-named methods provided by Term::ANSIColor (e.g. "def red").
24
+ #
25
+ # During output, each line is processed by passing it to the formatter Proc
26
+ # which returns the formatted (e.g. colored) string.
27
+
28
+ module Console
29
+ extend ANSIColor
30
+ include Duration
31
+ include Summary
32
+
33
+ def format_step(keyword, step_match, status, source_indent)
34
+ comment = if source_indent
35
+ c = (' # ' + step_match.file_colon_line).indent(source_indent)
36
+ format_string(c, :comment)
37
+ else
38
+ ''
39
+ end
40
+
41
+ format = format_for(status, :param)
42
+ line = keyword + step_match.format_args(format) + comment
43
+ format_string(line, status)
44
+ end
45
+
46
+ def format_string(o, status)
47
+ fmt = format_for(status)
48
+ o.to_s.split("\n").map do |line|
49
+ if Proc === fmt
50
+ fmt.call(line)
51
+ else
52
+ fmt % line
53
+ end
54
+ end.join("\n")
55
+ end
56
+
57
+ def print_steps(status)
58
+ print_elements(runtime.steps(status), status, 'steps')
59
+ end
60
+
61
+ def print_elements(elements, status, kind)
62
+ if elements.any?
63
+ @io.puts(format_string("(::) #{status} #{kind} (::)", status))
64
+ @io.puts
65
+ @io.flush
66
+ end
67
+
68
+ elements.each_with_index do |element, i|
69
+ if status == :failed
70
+ print_exception(element.exception, status, 0)
71
+ else
72
+ @io.puts(format_string(element.backtrace_line, status))
73
+ end
74
+ @io.puts
75
+ @io.flush
76
+ end
77
+ end
78
+
79
+ def print_stats(features, options)
80
+ @failures = runtime.scenarios(:failed).select { |s| s.is_a?(Lucid::AST::Scenario) || s.is_a?(Lucid::AST::OutlineTable::ExampleRow) }
81
+ @failures.collect! { |s| (s.is_a?(Lucid::AST::OutlineTable::ExampleRow)) ? s.scenario_outline : s }
82
+
83
+ if !@failures.empty?
84
+ @io.puts format_string("Failing Scenarios:", :failed)
85
+ @failures.each do |failure|
86
+ profiles_string = options.custom_profiles.empty? ? '' : (options.custom_profiles.map{|profile| "-p #{profile}" }).join(' ') + ' '
87
+ source = options[:source] ? format_string(" # Scenario: " + failure.name, :comment) : ''
88
+ @io.puts format_string("Lucid #{profiles_string}" + failure.file_colon_line, :failed) + source
89
+ end
90
+ @io.puts
91
+ end
92
+
93
+ @io.puts scenario_summary(runtime) {|status_count, status| format_string(status_count, status)}
94
+ @io.puts step_summary(runtime) {|status_count, status| format_string(status_count, status)}
95
+
96
+ @io.puts(format_duration(features.duration)) if features && features.duration
97
+
98
+ @io.flush
99
+ end
100
+
101
+ def print_exception(e, status, indent)
102
+ message = "#{e.message} (#{e.class})"
103
+ if ENV['LUCID_TRUNCATE_OUTPUT']
104
+ message = linebreaks(message, ENV['LUCID_TRUNCATE_OUTPUT'].to_i)
105
+ end
106
+
107
+ string = "#{message}\n#{e.backtrace.join("\n")}".indent(indent)
108
+ @io.puts(format_string(string, status))
109
+ end
110
+
111
+ # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/10655
112
+ def linebreaks(s, max)
113
+ s.gsub(/.{1,#{max}}(?:\s|\Z)/){($& + 5.chr).gsub(/\n\005/,"\n").gsub(/\005/,"\n")}.rstrip
114
+ end
115
+
116
+ def print_matchers(options)
117
+ return unless options[:matchers]
118
+ undefined = runtime.steps(:undefined)
119
+ return if undefined.empty?
120
+
121
+ unknown_programming_language = runtime.unknown_programming_language?
122
+ matchers = undefined.map do |step|
123
+ step_name = Undefined === step.exception ? step.exception.step_name : step.name
124
+ step_multiline_class = step.multiline_arg ? step.multiline_arg.class : nil
125
+ matcher = @runtime.matcher_text(step.actual_keyword, step_name, step_multiline_class)
126
+ matcher
127
+ end.compact.uniq
128
+
129
+ text = "\nYou can implement test definitions for undefined test steps with these matchers:\n\n"
130
+ text += matchers.join("\n\n")
131
+ @io.puts format_string(text, :undefined)
132
+
133
+ if unknown_programming_language
134
+ @io.puts format_string("\nIf you want matchers in a different programming language," +
135
+ "\njust make sure a file with the appropriate file extension" +
136
+ "\nexists where Lucid looks for test definitions.", :failed)
137
+ end
138
+
139
+ @io.puts
140
+ @io.flush
141
+ end
142
+
143
+ def print_passing_wip(options)
144
+ return unless options[:wip]
145
+ passed = runtime.scenarios(:passed)
146
+ if passed.any?
147
+ @io.puts format_string("\nThe --wip switch was used, so nothing was expected to pass. These scenarios passed:", :failed)
148
+ print_elements(passed, :passed, "scenarios")
149
+ else
150
+ @io.puts format_string("\nThe --wip switch was used, so any failures were expected. All is good.\n", :passed)
151
+ end
152
+ end
153
+
154
+ def embed(file, mime_type, label)
155
+ # no-op
156
+ end
157
+
158
+ #define @delayed_messages = [] in your Formatter if you want to
159
+ #activate this feature
160
+ def puts(*messages)
161
+ if @delayed_messages
162
+ @delayed_messages += messages
163
+ else
164
+ if @io
165
+ @io.puts
166
+ messages.each do |message|
167
+ @io.puts(format_string(message, :tag))
168
+ end
169
+ @io.flush
170
+ end
171
+ end
172
+ end
173
+
174
+ def print_messages
175
+ @delayed_messages.each {|message| print_message(message)}
176
+ empty_messages
177
+ end
178
+
179
+ def print_table_row_messages
180
+ return if @delayed_messages.empty?
181
+ @io.print(format_string(@delayed_messages.join(', '), :tag).indent(2))
182
+ @io.flush
183
+ empty_messages
184
+ end
185
+
186
+ def print_message(message)
187
+ @io.puts(format_string(message, :tag).indent(@indent))
188
+ @io.flush
189
+ end
190
+
191
+ def empty_messages
192
+ @delayed_messages = []
193
+ end
194
+
195
+ def print_profile_information
196
+ return if @options[:skip_profile_information] || @options[:profiles].nil? || @options[:profiles].empty?
197
+ profiles = @options[:profiles]
198
+ profiles_sentence = ''
199
+ profiles_sentence = profiles.size == 1 ? profiles.first :
200
+ "#{profiles[0...-1].join(', ')} and #{profiles.last}"
201
+
202
+ @io.puts "Using the #{profiles_sentence} profile#{'s' if profiles.size> 1}..."
203
+ end
204
+
205
+ private
206
+
207
+ FORMATS = Hash.new{ |hash, format| hash[format] = method(format).to_proc }
208
+
209
+ def format_for(*keys)
210
+ key = keys.join('_').to_sym
211
+ fmt = FORMATS[key]
212
+ raise "No format for #{key.inspect}: #{FORMATS.inspect}" if fmt.nil?
213
+ fmt
214
+ end
215
+
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,33 @@
1
+ require 'lucid/formatter/progress'
2
+ require 'lucid/step_definition_light'
3
+
4
+ module Lucid
5
+ module Formatter
6
+ class Debug
7
+ def initialize(runtime, io, options)
8
+ @io = io
9
+ @indent = 0
10
+ end
11
+
12
+ def respond_to?(*args)
13
+ true
14
+ end
15
+
16
+ def method_missing(name, *args)
17
+ @indent -= 2 if name.to_s =~ /^after/
18
+ print(name)
19
+ @indent += 2 if name.to_s =~ /^before/
20
+ end
21
+
22
+ private
23
+
24
+ def print(text)
25
+ @io.puts "#{indent}#{text}"
26
+ end
27
+
28
+ def indent
29
+ (' ' * @indent)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ module Lucid
2
+ module Formatter
3
+ module Duration
4
+ # Format a duration in seconds in the Unix time format.
5
+ def format_duration(seconds)
6
+ m, s = seconds.divmod(60)
7
+ "#{m}m#{'%.3f' % s}s"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,94 @@
1
+ require 'lucid/formatter/io'
2
+ require 'gherkin/formatter/argument'
3
+
4
+ module Lucid
5
+ module Formatter
6
+ # Adapts Lucid formatter events to Gherkin formatter events
7
+ # This class will disappear when Lucid is based on Gherkin's model.
8
+ class GherkinFormatterAdapter
9
+ def initialize(gherkin_formatter, print_empty_match)
10
+ @gf = gherkin_formatter
11
+ @print_empty_match = print_empty_match
12
+ end
13
+
14
+ def before_feature(feature)
15
+ @gf.uri(feature.file)
16
+ @gf.feature(feature.gherkin_statement)
17
+ end
18
+
19
+ def before_background(background)
20
+ @outline = false
21
+ @gf.background(background.gherkin_statement)
22
+ end
23
+
24
+ def before_feature_element(feature_element)
25
+ case(feature_element)
26
+ when AST::Scenario
27
+ @outline = false
28
+ @gf.scenario(feature_element.gherkin_statement)
29
+ when AST::ScenarioOutline
30
+ @outline = true
31
+ @gf.scenario_outline(feature_element.gherkin_statement)
32
+ else
33
+ raise "Bad type: #{feature_element.class}"
34
+ end
35
+ end
36
+
37
+ def before_step(step)
38
+ @gf.step(step.gherkin_statement)
39
+ if @print_empty_match
40
+ if(@outline)
41
+ match = Gherkin::Formatter::Model::Match.new(step.gherkin_statement.outline_args, nil)
42
+ else
43
+ match = Gherkin::Formatter::Model::Match.new([], nil)
44
+ end
45
+ @gf.match(match)
46
+ end
47
+ @step_time = Time.now
48
+ end
49
+
50
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
51
+ arguments = step_match.step_arguments.map{|a| Gherkin::Formatter::Argument.new(a.offset, a.val)}
52
+ location = step_match.file_colon_line
53
+ match = Gherkin::Formatter::Model::Match.new(arguments, location)
54
+ if @print_empty_match
55
+ # Trick the formatter to believe that's what was printed previously so we get arg highlights on #result
56
+ @gf.instance_variable_set('@match', match)
57
+ else
58
+ @gf.match(match)
59
+ end
60
+
61
+ error_message = exception ? "#{exception.message} (#{exception.class})\n#{exception.backtrace.join("\n")}" : nil
62
+ unless @outline
63
+ @gf.result(Gherkin::Formatter::Model::Result.new(status, nil, error_message))
64
+ end
65
+ end
66
+
67
+ def before_examples(examples)
68
+ @gf.examples(examples.gherkin_statement)
69
+ end
70
+
71
+ #used for capturing duration
72
+ def after_step(step)
73
+ step_finish = (Time.now - @step_time)
74
+ @gf.append_duration(step_finish)
75
+ end
76
+
77
+ def after_feature(feature)
78
+ @gf.eof
79
+ end
80
+
81
+ def after_features(features)
82
+ @gf.done
83
+ end
84
+
85
+ def embed(file, mime_type, label)
86
+ data = File.open(file, 'rb') { |f| f.read }
87
+ if defined?(JRUBY_VERSION)
88
+ data = data.to_java_bytes
89
+ end
90
+ @gf.embedding(mime_type, data)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,24 @@
1
+ require 'lucid/formatter/gherkin_formatter_adapter'
2
+ require 'lucid/formatter/io'
3
+ require 'gherkin/formatter/argument'
4
+ require 'gherkin/formatter/pretty_formatter'
5
+
6
+ module Lucid
7
+ module Formatter
8
+ # The formatter used for <tt>--format gpretty</tt>
9
+ class Gpretty < GherkinFormatterAdapter
10
+ include Io
11
+
12
+ def initialize(runtime, io, options)
13
+ @io = ensure_io(io, "json")
14
+ super(Gherkin::Formatter::PrettyFormatter.new(@io, false), true)
15
+ end
16
+
17
+ def after_feature(feature)
18
+ super
19
+ @io.puts
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,610 @@
1
+ require 'erb'
2
+ require 'lucid/formatter/ordered_xml_markup'
3
+ require 'lucid/formatter/duration'
4
+ require 'lucid/formatter/io'
5
+
6
+ module Lucid
7
+ module Formatter
8
+ class Html
9
+ include ERB::Util
10
+ include Duration
11
+ include Io
12
+
13
+ def initialize(runtime, path_or_io, options)
14
+ @io = ensure_io(path_or_io, "html")
15
+ @runtime = runtime
16
+ @options = options
17
+ @buffer = {}
18
+ @builder = create_builder(@io)
19
+ @feature_number = 0
20
+ @scenario_number = 0
21
+ @step_number = 0
22
+ @header_red = nil
23
+ @delayed_messages = []
24
+ @img_id = 0
25
+ end
26
+
27
+ def embed(src, mime_type, label)
28
+ case(mime_type)
29
+ when /^image\/(png|gif|jpg|jpeg)/
30
+ embed_image(src, label)
31
+ end
32
+ end
33
+
34
+ def embed_image(src, label)
35
+ id = "img_#{@img_id}"
36
+ @img_id += 1
37
+ @builder.span(:class => 'embed') do |pre|
38
+ pre << %{<a href="" onclick="img=document.getElementById('#{id}'); img.style.display = (img.style.display == 'none' ? 'block' : 'none');return false">#{label}</a><br>&nbsp;
39
+ <img id="#{id}" style="display: none" src="#{src}"/>}
40
+ end
41
+ end
42
+
43
+ def before_features(features)
44
+ @step_count = features.step_count
45
+
46
+ @builder.declare!(:DOCTYPE, :html)
47
+
48
+ @builder << '<html>'
49
+ @builder.head do
50
+ @builder.meta(:charset => 'utf-8')
51
+ @builder.title 'Lucid'
52
+ inline_css
53
+ inline_js
54
+ end
55
+ @builder << '<body>'
56
+ @builder << "<!-- Step count #{@step_count}-->"
57
+ @builder << '<div class="lucid">'
58
+
59
+ @builder.div(:id => 'lucid-header') do
60
+ @builder.div(:id => 'label') do
61
+ @builder.h1('Lucid Features')
62
+ end
63
+ @builder.div(:id => 'summary') do
64
+ @builder.p('',:id => 'totals')
65
+ @builder.p('',:id => 'duration')
66
+ @builder.div(:id => 'expand-collapse') do
67
+ @builder.p('Expand All', :id => 'expander')
68
+ @builder.p('Collapse All', :id => 'collapser')
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def after_features(features)
75
+ print_stats(features)
76
+ @builder << '</div>'
77
+ @builder << '</body>'
78
+ @builder << '</html>'
79
+ end
80
+
81
+ def before_feature(feature)
82
+ @exceptions = []
83
+ @builder << '<div class="feature">'
84
+ end
85
+
86
+ def after_feature(feature)
87
+ @builder << '</div>'
88
+ end
89
+
90
+ def before_comment(comment)
91
+ @builder << '<pre class="comment">'
92
+ end
93
+
94
+ def after_comment(comment)
95
+ @builder << '</pre>'
96
+ end
97
+
98
+ def comment_line(comment_line)
99
+ @builder.text!(comment_line)
100
+ @builder.br
101
+ end
102
+
103
+ def after_tags(tags)
104
+ @tag_spacer = nil
105
+ end
106
+
107
+ def tag_name(tag_name)
108
+ @builder.text!(@tag_spacer) if @tag_spacer
109
+ @tag_spacer = ' '
110
+ @builder.span(tag_name, :class => 'tag')
111
+ end
112
+
113
+ def feature_name(keyword, name)
114
+ lines = name.split(/\r?\n/)
115
+ return if lines.empty?
116
+ @builder.h2 do |h2|
117
+ @builder.span(keyword + ': ' + lines[0], :class => 'val')
118
+ end
119
+ @builder.p(:class => 'narrative') do
120
+ lines[1..-1].each do |line|
121
+ @builder.text!(line.strip)
122
+ @builder.br
123
+ end
124
+ end
125
+ end
126
+
127
+ def before_background(background)
128
+ @in_background = true
129
+ @builder << '<div class="background">'
130
+ end
131
+
132
+ def after_background(background)
133
+ @in_background = nil
134
+ @builder << '</div>'
135
+ end
136
+
137
+ def background_name(keyword, name, file_colon_line, source_indent)
138
+ @listing_background = true
139
+ @builder.h3(:id => "background_#{@scenario_number}") do |h3|
140
+ @builder.span(keyword, :class => 'keyword')
141
+ @builder.text!(' ')
142
+ @builder.span(name, :class => 'val')
143
+ end
144
+ end
145
+
146
+ def before_feature_element(feature_element)
147
+ @scenario_number+=1
148
+ @scenario_red = false
149
+ css_class = {
150
+ AST::Scenario => 'scenario',
151
+ AST::ScenarioOutline => 'scenario outline'
152
+ }[feature_element.class]
153
+ @builder << "<div class='#{css_class}'>"
154
+ end
155
+
156
+ def after_feature_element(feature_element)
157
+ @builder << '</div>'
158
+ @open_step_list = true
159
+ end
160
+
161
+ def scenario_name(keyword, name, file_colon_line, source_indent)
162
+ @builder.span(:class => 'scenario_file') do
163
+ @builder << file_colon_line
164
+ end
165
+ @listing_background = false
166
+ @builder.h3(:id => "scenario_#{@scenario_number}") do
167
+ @builder.span(keyword + ':', :class => 'keyword')
168
+ @builder.text!(' ')
169
+ @builder.span(name, :class => 'val')
170
+ end
171
+ end
172
+
173
+ def before_outline_table(outline_table)
174
+ @outline_row = 0
175
+ @builder << '<table>'
176
+ end
177
+
178
+ def after_outline_table(outline_table)
179
+ @builder << '</table>'
180
+ @outline_row = nil
181
+ end
182
+
183
+ def before_examples(examples)
184
+ @builder << '<div class="examples">'
185
+ end
186
+
187
+ def after_examples(examples)
188
+ @builder << '</div>'
189
+ end
190
+
191
+ def examples_name(keyword, name)
192
+ @builder.h4 do
193
+ @builder.span(keyword, :class => 'keyword')
194
+ @builder.text!(' ')
195
+ @builder.span(name, :class => 'val')
196
+ end
197
+ end
198
+
199
+ def before_steps(steps)
200
+ @builder << '<ol>'
201
+ end
202
+
203
+ def after_steps(steps)
204
+ @builder << '</ol>'
205
+ end
206
+
207
+ def before_step(step)
208
+ @step_id = step.dom_id
209
+ @step_number += 1
210
+ @step = step
211
+ end
212
+
213
+ def after_step(step)
214
+ move_progress
215
+ end
216
+
217
+ def before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
218
+ @step_match = step_match
219
+ @hide_this_step = false
220
+ if exception
221
+ if @exceptions.include?(exception)
222
+ @hide_this_step = true
223
+ return
224
+ end
225
+ @exceptions << exception
226
+ end
227
+ if status != :failed && @in_background ^ background
228
+ @hide_this_step = true
229
+ return
230
+ end
231
+ @status = status
232
+ return if @hide_this_step
233
+ set_scenario_color(status)
234
+ @builder << "<li id='#{@step_id}' class='step #{status}'>"
235
+ end
236
+
237
+ def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background, file_colon_line)
238
+ return if @hide_this_step
239
+ if status == :undefined
240
+ keyword = @step.actual_keyword if @step.respond_to?(:actual_keyword)
241
+ step_multiline_class = @step.multiline_arg ? @step.multiline_arg.class : nil
242
+ @builder.pre do |pre|
243
+ pre << @runtime.matcher_text(keyword,step_match.instance_variable_get("@name") || '',step_multiline_class)
244
+ end
245
+ end
246
+ @builder << '</li>'
247
+ print_messages
248
+ end
249
+
250
+ def step_name(keyword, step_match, status, source_indent, background, file_colon_line)
251
+ background_in_scenario = background && !@listing_background
252
+ @skip_step = background_in_scenario
253
+
254
+ unless @skip_step
255
+ build_step(keyword, step_match, status)
256
+ end
257
+ end
258
+
259
+ def exception(exception, status)
260
+ build_exception_detail(exception)
261
+ end
262
+
263
+ def extra_failure_content(file_colon_line)
264
+ @matcher_extractor ||= MatcherExtractor.new
265
+ "<pre class=\"ruby\"><code>#{@matcher_extractor.matcher(file_colon_line)}</code></pre>"
266
+ end
267
+
268
+ def before_multiline_arg(multiline_arg)
269
+ return if @hide_this_step || @skip_step
270
+ if AST::Table === multiline_arg
271
+ @builder << '<table>'
272
+ end
273
+ end
274
+
275
+ def after_multiline_arg(multiline_arg)
276
+ return if @hide_this_step || @skip_step
277
+ if AST::Table === multiline_arg
278
+ @builder << '</table>'
279
+ end
280
+ end
281
+
282
+ def doc_string(string)
283
+ return if @hide_this_step
284
+ @builder.pre(:class => 'val') do |pre|
285
+ @builder << h(string).gsub("\n", '&#x000A;')
286
+ end
287
+ end
288
+
289
+ def before_table_row(table_row)
290
+ @row_id = table_row.dom_id
291
+ @col_index = 0
292
+ return if @hide_this_step
293
+ @builder << "<tr class='step' id='#{@row_id}'>"
294
+ end
295
+
296
+ def after_table_row(table_row)
297
+ return if @hide_this_step
298
+ print_table_row_messages
299
+ @builder << '</tr>'
300
+ if table_row.exception
301
+ @builder.tr do
302
+ @builder.td(:colspan => @col_index.to_s, :class => 'failed') do
303
+ @builder.pre do |pre|
304
+ pre << h(format_exception(table_row.exception))
305
+ end
306
+ end
307
+ end
308
+ if table_row.exception.is_a? ::Lucid::Pending
309
+ set_scenario_color_pending
310
+ else
311
+ set_scenario_color_failed
312
+ end
313
+ end
314
+ if @outline_row
315
+ @outline_row += 1
316
+ end
317
+ @step_number += 1
318
+ move_progress
319
+ end
320
+
321
+ def table_cell_value(value, status)
322
+ return if @hide_this_step
323
+
324
+ @cell_type = @outline_row == 0 ? :th : :td
325
+ attributes = {:id => "#{@row_id}_#{@col_index}", :class => 'step'}
326
+ attributes[:class] += " #{status}" if status
327
+ build_cell(@cell_type, value, attributes)
328
+ set_scenario_color(status)
329
+ @col_index += 1
330
+ end
331
+
332
+ def puts(message)
333
+ @delayed_messages << message
334
+ #@builder.pre(message, :class => 'message')
335
+ end
336
+
337
+ def print_messages
338
+ return if @delayed_messages.empty?
339
+
340
+ #@builder.ol do
341
+ @delayed_messages.each do |ann|
342
+ @builder.li(:class => 'step message') do
343
+ @builder << ann
344
+ end
345
+ end
346
+ #end
347
+ empty_messages
348
+ end
349
+
350
+ def print_table_row_messages
351
+ return if @delayed_messages.empty?
352
+
353
+ @builder.td(:class => 'message') do
354
+ @builder << @delayed_messages.join(", ")
355
+ end
356
+ empty_messages
357
+ end
358
+
359
+ def empty_messages
360
+ @delayed_messages = []
361
+ end
362
+
363
+ protected
364
+
365
+ def build_exception_detail(exception)
366
+ backtrace = Array.new
367
+ @builder.div(:class => 'message') do
368
+ message = exception.message
369
+ if defined?(RAILS_ROOT) && message.include?('Exception caught')
370
+ matches = message.match(/Showing <i>(.+)<\/i>(?:.+) #(\d+)/)
371
+ backtrace += ["#{RAILS_ROOT}/#{matches[1]}:#{matches[2]}"] if matches
372
+ matches = message.match(/<code>([^(\/)]+)<\//m)
373
+ message = matches ? matches[1] : ""
374
+ end
375
+
376
+ unless exception.instance_of?(RuntimeError)
377
+ message = "#{message} (#{exception.class})"
378
+ end
379
+
380
+ @builder.pre do
381
+ @builder.text!(message)
382
+ end
383
+ end
384
+ @builder.div(:class => 'backtrace') do
385
+ @builder.pre do
386
+ backtrace = exception.backtrace
387
+ backtrace.delete_if { |x| x =~ /\/gems\/(lucid|rspec)/ }
388
+ @builder << backtrace_line(backtrace.join("\n"))
389
+ end
390
+ end
391
+ extra = extra_failure_content(backtrace)
392
+ @builder << extra unless extra == ""
393
+ end
394
+
395
+ def set_scenario_color(status)
396
+ if status.nil? or status == :undefined or status == :pending
397
+ set_scenario_color_pending
398
+ end
399
+ if status == :failed
400
+ set_scenario_color_failed
401
+ end
402
+ end
403
+
404
+ def set_scenario_color_failed
405
+ @builder.script do
406
+ @builder.text!("makeRed('lucid-header');") unless @header_red
407
+ @header_red = true
408
+ @builder.text!("makeRed('scenario_#{@scenario_number}');") unless @scenario_red
409
+ @scenario_red = true
410
+ end
411
+ end
412
+
413
+ def set_scenario_color_pending
414
+ @builder.script do
415
+ @builder.text!("makeYellow('lucid-header');") unless @header_red
416
+ @builder.text!("makeYellow('scenario_#{@scenario_number}');") unless @scenario_red
417
+ end
418
+ end
419
+
420
+ def build_step(keyword, step_match, status)
421
+ step_name = step_match.format_args(lambda{|param| %{<span class="param">#{param}</span>}})
422
+ @builder.div(:class => 'step_name') do |div|
423
+ @builder.span(keyword, :class => 'keyword')
424
+ @builder.span(:class => 'step val') do |name|
425
+ name << h(step_name).gsub(/&lt;span class=&quot;(.*?)&quot;&gt;/, '<span class="\1">').gsub(/&lt;\/span&gt;/, '</span>')
426
+ end
427
+ end
428
+
429
+ step_file = step_match.file_colon_line
430
+ step_file.gsub(/^([^:]*\.rb):(\d*)/) do
431
+ if ENV['TM_PROJECT_DIRECTORY']
432
+ step_file = "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
433
+ end
434
+ end
435
+
436
+ @builder.div(:class => 'step_file') do |div|
437
+ @builder.span do
438
+ @builder << step_file
439
+ end
440
+ end
441
+ end
442
+
443
+ def build_cell(cell_type, value, attributes)
444
+ @builder.__send__(cell_type, attributes) do
445
+ @builder.div do
446
+ @builder.span(value,:class => 'step param')
447
+ end
448
+ end
449
+ end
450
+
451
+ def inline_css
452
+ @builder.style(:type => 'text/css') do
453
+ @builder << File.read(File.dirname(__FILE__) + '/lucid.css')
454
+ end
455
+ end
456
+
457
+ def inline_js
458
+ @builder.script(:type => 'text/javascript') do
459
+ @builder << inline_jquery
460
+ @builder << inline_js_content
461
+ end
462
+ end
463
+
464
+ def inline_jquery
465
+ File.read(File.dirname(__FILE__) + '/jquery-min.js')
466
+ end
467
+
468
+ def inline_js_content
469
+ <<-EOF
470
+
471
+ SCENARIOS = "h3[id^='scenario_'],h3[id^=background_]";
472
+
473
+ $(document).ready(function() {
474
+ $(SCENARIOS).css('cursor', 'pointer');
475
+ $(SCENARIOS).click(function() {
476
+ $(this).siblings().toggle(250);
477
+ });
478
+
479
+ $("#collapser").css('cursor', 'pointer');
480
+ $("#collapser").click(function() {
481
+ $(SCENARIOS).siblings().hide();
482
+ });
483
+
484
+ $("#expander").css('cursor', 'pointer');
485
+ $("#expander").click(function() {
486
+ $(SCENARIOS).siblings().show();
487
+ });
488
+ })
489
+
490
+ function moveProgressBar(percentDone) {
491
+ $("lucid-header").css('width', percentDone +"%");
492
+ }
493
+ function makeRed(element_id) {
494
+ $('#'+element_id).css('background', '#C40D0D');
495
+ $('#'+element_id).css('color', '#FFFFFF');
496
+ }
497
+ function makeYellow(element_id) {
498
+ $('#'+element_id).css('background', '#FAF834');
499
+ $('#'+element_id).css('color', '#000000');
500
+ }
501
+
502
+ EOF
503
+ end
504
+
505
+ def move_progress
506
+ @builder << " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
507
+ end
508
+
509
+ def percent_done
510
+ result = 100.0
511
+ if @step_count != 0
512
+ result = ((@step_number).to_f / @step_count.to_f * 1000).to_i / 10.0
513
+ end
514
+ result
515
+ end
516
+
517
+ def format_exception(exception)
518
+ (["#{exception.message}"] + exception.backtrace).join("\n")
519
+ end
520
+
521
+ def backtrace_line(line)
522
+ line.gsub(/\A([^:]*\.(?:rb|feature|haml)):(\d*).*\z/) do
523
+ if ENV['TM_PROJECT_DIRECTORY']
524
+ "<a href=\"txmt://open?url=file://#{File.expand_path($1)}&line=#{$2}\">#{$1}:#{$2}</a> "
525
+ else
526
+ line
527
+ end
528
+ end
529
+ end
530
+
531
+ def print_stats(features)
532
+ @builder << "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{format_duration(features.duration)} seconds</strong>\";</script>"
533
+ @builder << "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{print_stat_string(features)}\";</script>"
534
+ end
535
+
536
+ def print_stat_string(features)
537
+ string = String.new
538
+ string << dump_count(@runtime.scenarios.length, "scenario")
539
+ scenario_count = print_status_counts{|status| @runtime.scenarios(status)}
540
+ string << scenario_count if scenario_count
541
+ string << "<br />"
542
+ string << dump_count(@runtime.steps.length, "step")
543
+ step_count = print_status_counts{|status| @runtime.steps(status)}
544
+ string << step_count if step_count
545
+ end
546
+
547
+ def print_status_counts
548
+ counts = [:failed, :skipped, :undefined, :pending, :passed].map do |status|
549
+ elements = yield status
550
+ elements.any? ? "#{elements.length} #{status.to_s}" : nil
551
+ end.compact
552
+ return " (#{counts.join(', ')})" if counts.any?
553
+ end
554
+
555
+ def dump_count(count, what, state=nil)
556
+ [count, state, "#{what}#{count == 1 ? '' : 's'}"].compact.join(" ")
557
+ end
558
+
559
+ def create_builder(io)
560
+ OrderedXmlMarkup.new(:target => io, :indent => 0)
561
+ end
562
+
563
+ class MatcherExtractor #:nodoc:
564
+ class NullConverter; def convert(code, pre); code; end; end #:nodoc:
565
+ begin; require 'syntax/convertors/html'; @@converter = Syntax::Convertors::HTML.for_syntax "ruby"; rescue LoadError => e; @@converter = NullConverter.new; end
566
+
567
+ def matcher(error)
568
+ raw_code, line = matcher_for(error[0])
569
+ highlighted = @@converter.convert(raw_code, false)
570
+ highlighted << "\n<span class=\"comment\"># gem install syntax to get syntax highlighting</span>" if @@converter.is_a?(NullConverter)
571
+ post_process(highlighted, line)
572
+ end
573
+
574
+ def matcher_for(error_line)
575
+ if error_line =~ /(.*):(\d+)/
576
+ file = $1
577
+ line = $2.to_i
578
+ [lines_around(file, line), line]
579
+ else
580
+ ["# Could not get matcher for #{error_line}", 1]
581
+ end
582
+ end
583
+
584
+ def lines_around(file, line)
585
+ if File.file?(file)
586
+ lines = File.open(file).read.split("\n")
587
+ min = [0, line-3].max
588
+ max = [line+1, lines.length-1].min
589
+ selected_lines = []
590
+ selected_lines.join("\n")
591
+ lines[min..max].join("\n")
592
+ else
593
+ "# Lucid was unable to get a matcher for #{file}"
594
+ end
595
+ end
596
+
597
+ def post_process(highlighted, offending_line)
598
+ new_lines = []
599
+ highlighted.split("\n").each_with_index do |line, i|
600
+ new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
601
+ new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
602
+ new_lines << new_line
603
+ end
604
+ new_lines.join("\n")
605
+ end
606
+
607
+ end
608
+ end
609
+ end
610
+ end