cukable 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,366 @@
1
+ # slim_json_formatter.rb
2
+
3
+ require 'cucumber/formatter/console'
4
+ require 'cucumber/formatter/io'
5
+ require 'fileutils'
6
+ require 'json'
7
+
8
+
9
+ module Cucumber
10
+ module Formatter
11
+ # FitNesse SliM JSON output formatter for Cucumber
12
+ class SlimJSON
13
+ include Console
14
+ include Io
15
+ include FileUtils
16
+
17
+ # Create a new SlimJSON formatter, with the provided `path_or_io` (as
18
+ # given by the `--out` option) and any additional options passed to
19
+ # cucumber.
20
+ def initialize(step_mother, path_or_io, options)
21
+ @step_mother = step_mother
22
+
23
+ # Output directory
24
+ @out_dir = path_or_io
25
+ ensure_dir(@out_dir, "FitNesse")
26
+
27
+ # There should be no IO until we get a feature, and
28
+ # create the output directory in before_feature
29
+ @io = nil
30
+
31
+ # Cache of data lines to write
32
+ @data = []
33
+ # Multi-line output (must be cached and printed after the step that
34
+ # precedes it)
35
+ @multiline = []
36
+ # Expected/actual, to support table diffs
37
+ @expected_row = []
38
+ @actual_row = []
39
+
40
+ # Not in background until we actually find one
41
+ @in_background = false
42
+ end
43
+
44
+
45
+ # Called before each `.feature` is run. Creates a new output file for the
46
+ # results in `@out_dir`, and empties `@data`.
47
+ def before_feature(feature)
48
+ file = File.join(@out_dir, "#{feature.file}.json")
49
+ dir = File.dirname(file)
50
+ mkdir_p(dir) unless File.directory?(dir)
51
+ @io = ensure_file(file, "FitNesse")
52
+ @data = []
53
+ end
54
+
55
+
56
+ # Called after each `.feature` is run. Write all `@data` to the JSON
57
+ # file, then closes the output file.
58
+ def after_feature(feature)
59
+ @io.puts JSON.pretty_generate(@data)
60
+ @io.flush
61
+ @io.close
62
+ end
63
+
64
+
65
+ # Called when `Feature: <name>` is read. Generates a single row of output
66
+ # in `@data` with the feature name.
67
+ def feature_name(keyword, name)
68
+ @data << [section_message(keyword, name)]
69
+ end
70
+
71
+
72
+ # Called when `Scenario: <name>` is read. Generates a single row of
73
+ # output in `@data` with the scenario name.
74
+ def scenario_name(keyword, name, file_colon_line, source_indent)
75
+ @data << [section_message(keyword, name, file_colon_line)]
76
+ end
77
+
78
+
79
+ # Called before a `Background:` block.
80
+ def before_background(background)
81
+ @in_background = true
82
+ end
83
+
84
+
85
+ # Called when a `Background:` line is read. Generates a single row of
86
+ # output in `@data` with the `Background:` line.
87
+ def background_name(keyword, name, file_colon_line, source_indent)
88
+ @data << [section_message(keyword, name, file_colon_line)]
89
+ end
90
+
91
+
92
+ # Called after a `Background:` block.
93
+ def after_background(background)
94
+ @in_background = false
95
+ end
96
+
97
+
98
+ # Start a new multiline arg (such as a table or Py-string). Initializes
99
+ # `@multiline` and related arrays.
100
+ def before_multiline_arg(multiline_arg)
101
+ @multiline = []
102
+ @expected_row = []
103
+ @actual_row = []
104
+ end
105
+
106
+
107
+ # Called before a table row is read. Starts a new `@table_row`.
108
+ def before_table_row(table_row)
109
+ @table_row = []
110
+ end
111
+
112
+
113
+ # Called when a table cell value is read. Appends to `@table_row`.
114
+ def table_cell_value(value, status)
115
+ return if @hide_this_step
116
+ stat = status_map(status)
117
+ @table_row << "#{stat}:#{value}"
118
+ end
119
+
120
+
121
+ # Called after a table row is done being read. Appends `@table_row`
122
+ # to `@multiline`, which will be output in `after_step_result`.
123
+ #
124
+ # There is some special handling here for handling table diffs;
125
+ # when doing a table diff, and a row doesn't match, two rows are
126
+ # generated. These need to be merged into a single row in the JSON
127
+ # output, to maintain the 1:1 mapping between FitNesse table and
128
+ # the returned results.
129
+ def after_table_row(table_row)
130
+ return if @hide_this_step
131
+
132
+ # If we have an @expected_row and @actual_row at this point,
133
+ # merge them into a single row and append to @multiline_arg
134
+ if !@expected_row.empty? && !@actual_row.empty?
135
+ cell_diff = []
136
+ @expected_row.zip(@actual_row) do |expect, actual|
137
+ expect.gsub!(/^ignore:/, '')
138
+ actual.gsub!(/^error:/, '')
139
+ # If we got what we wanted in this cell, consider it passed
140
+ if actual == expect
141
+ cell_diff << "pass:#{actual}"
142
+ # Otherwise, show expected vs. actual as a failure
143
+ else
144
+ cell_diff << "fail:Expected: '#{expect}'<br/>Actual: '#{actual}'"
145
+ end
146
+ end
147
+ @multiline << ["report: "] + cell_diff
148
+ # Reset for upcoming rows
149
+ @expected_row = []
150
+ @actual_row = []
151
+ end
152
+
153
+ # Row with all cells having status == :comment (ignore)?
154
+ # This row was part of a table diff, and contains the values
155
+ # that were expected to be in the row.
156
+ if @table_row.all? { |cell| cell =~ /^ignore:/ }
157
+ @expected_row = @table_row
158
+
159
+ # Row with all cells having status == :undefined (error)?
160
+ # This row was part of a table diff, and contains the values
161
+ # that actually appeared in the row.
162
+ elsif @table_row.all? { |cell| cell =~ /^error:/ }
163
+ @actual_row = @table_row
164
+
165
+ # For any other row, append to multiline normally
166
+ else
167
+ # If an exception occurred in a table row, put the exception
168
+ # message in the first cell (which is normally empty). This
169
+ # allows us to show the exception without adding extra rows
170
+ # (which messes up the original table's formatting)
171
+ if table_row.exception
172
+ @multiline << ["fail:#{backtrace(table_row.exception)}"] + @table_row
173
+ # Otherwise, output an empty report: cell in the first column
174
+ else
175
+ @multiline << ["report: "] + @table_row
176
+ end
177
+ end
178
+
179
+ end
180
+
181
+
182
+ # Called before an `Examples:` section in a Scenario Outline. An
183
+ # `Examples:` table works similarly to a multiline argument, except there
184
+ # is no associated step to output them. The `after_table_row` method
185
+ # will still accumulate the table rows, but we need to rely on
186
+ # `after_examples` to output them. Thus, we will be accumulating these
187
+ # rows in the multi-purpose `@multiline` variable, initialized here.
188
+ def before_examples(examples)
189
+ @multiline = []
190
+ end
191
+
192
+
193
+ # Called when the `Examples:` line is read. Outputs the `Examples:` line
194
+ # to `@data`.
195
+ def examples_name(keyword, name)
196
+ @data << ["report:#{keyword}: #{name}"]
197
+ end
198
+
199
+
200
+ # Called after an `Examples:` section. Outputs anything accumulated in
201
+ # `@multiline`, and empties it.
202
+ def after_examples(examples)
203
+ # Output any multiline args that followed this step
204
+ @multiline.each do |row|
205
+ @data << row
206
+ end
207
+ @multiline = []
208
+ end
209
+
210
+
211
+ # Called *after* a step has been executed, but *before* any output from
212
+ # that step has been done.
213
+ def before_step(step)
214
+ @current_step = step
215
+ end
216
+
217
+
218
+ # Called when a multi-line string argument is read. Generates a row of
219
+ # output for each line in the multi-line string (including the `"""`
220
+ # opening and closing lines), colored based on the status of the current
221
+ # step. The output is accumulated in `@multiline`, for output in
222
+ # `after_step_result`.
223
+ def py_string(string)
224
+ return if @hide_this_step
225
+ status = status_map(@current_step.status)
226
+ @multiline << [status + ':"""']
227
+ string.split("\n").each do |line|
228
+ @multiline << ["#{status}:#{line}"]
229
+ end
230
+ @multiline << [status + ':"""']
231
+ end
232
+
233
+
234
+ # Called when a tag name is found. Generates a single row of output in
235
+ # `@data` with the tag name. (Note that this will only work properly if
236
+ # there is only one tag per line; otherwise, too many lines may be
237
+ # output.)
238
+ def tag_name(tag_name)
239
+ @data << ["ignore:#{tag_name}"]
240
+ end
241
+
242
+
243
+ # Called before any output from a step result. To avoid redundant output,
244
+ # we want to show the results of `Background` steps only once, within the
245
+ # `Background` section (unless a background step somehow failed when it
246
+ # was executed at the top of a Scenario). Here, `background` is true if
247
+ # the step was defined in the `Background` section, and `@in_background`
248
+ # is true if we are actually inside the `Background` section during
249
+ # execution. In short, if a step was defined in the `Background` section,
250
+ # but we are *not* within the `Background` section now, we want to hide
251
+ # the step's output.
252
+ def before_step_result(keyword, step_match, multiline_arg, status,
253
+ exception, source_indent, background)
254
+ if status != :failed && @in_background ^ background
255
+ @hide_this_step = true
256
+ else
257
+ @hide_this_step = false
258
+ end
259
+ end
260
+
261
+
262
+ # Called after a step has executed, and we have a result. Generates a
263
+ # single row of output in `@data`, including the status of the completed
264
+ # step, along with one row for each line accumulated in a multi-line
265
+ # argument (`@multiline`) if any were provided. Resets `@multiline` when
266
+ # done.
267
+ def after_step_result(keyword, step_match, multiline_arg, status,
268
+ exception, source_indent, background)
269
+ return if @hide_this_step
270
+ # One-line message to print
271
+ message = ''
272
+ # A bit of a hack here to support Scenario Outlines
273
+ # Apparently, at the time of calling after_step_result, a StepMatch in
274
+ # a Scenario Outline has a `name` attribute (and no arguments to
275
+ # format, because they don't get pattern-matched until the Examples:
276
+ # section that fills them in), whereas a StepMatch in a normal scenario
277
+ # has no value set for its `name` attribute, and *does* have arguments
278
+ # to format. This behavior is exploited here for the sake of replacing
279
+ # the `<...>` angle brackets that appear in Scenario Outline steps.
280
+ #
281
+ # In other words, if `step_match` has a `name`, assume it's in a
282
+ # Scenario Outline, and replace the angle brackets (so the bracketed
283
+ # parameter can be displayed in an HTML page)
284
+ if step_match.name
285
+ step_name = keyword + step_match.name.gsub('<', '&lt;').gsub('>', '&gt;')
286
+ # Otherwise, wrap the arguments in bold tags
287
+ else
288
+ step_name = keyword + step_match.format_args("<b>%s</b>")
289
+ end
290
+
291
+ # Output the step name with appropriate colorization
292
+ stat = status_map(status)
293
+ message = "#{stat}:#{step_name}"
294
+
295
+ # Add the source file and line number where this step was defined
296
+ message += source_message(step_match.file_colon_line)
297
+
298
+ # Include additional info for undefined and failed steps
299
+ if status == :undefined
300
+ message += "<br/>(Undefined Step)"
301
+ elsif status == :failed && exception
302
+ message += backtrace(exception)
303
+ end
304
+
305
+ # Output the final message for this step
306
+ @data << [message]
307
+
308
+ # Output any multiline args that followed this step
309
+ @multiline.each do |row|
310
+ @data << row
311
+ end
312
+ @multiline = []
313
+ end
314
+
315
+
316
+ # ------------------------
317
+ # Utility methods
318
+ # (not called by Cucumber)
319
+ # ------------------------
320
+
321
+ # Map Cucumber status strings to FitNesse status strings
322
+ def status_map(status)
323
+ case status
324
+ when nil then 'pass'
325
+ when :passed then 'pass'
326
+ when :failed then 'fail'
327
+ when :undefined then 'error'
328
+ when :skipped then 'ignore'
329
+ when :comment then 'ignore'
330
+ else 'pass'
331
+ end
332
+ end
333
+
334
+
335
+ # Return `text` with any HTML-specific characters sanitized
336
+ def sanitize(text)
337
+ text.gsub('<', '&lt;').gsub('>', '&gt;')
338
+ end
339
+
340
+
341
+ # Return a string for outputting the source filename and line number
342
+ def source_message(file_colon_line)
343
+ return " <span class=\"source_file\">" + file_colon_line + '</span>'
344
+ end
345
+
346
+
347
+ # Return a string suitable for use as a section heading for "Feature:",
348
+ # "Scenario:" or "Scenario Outline:" output rows.
349
+ def section_message(keyword, name, file_colon_line='')
350
+ "report:#{keyword}: #{name}" + source_message(file_colon_line)
351
+ end
352
+
353
+
354
+ # Return an exception message and backtrace
355
+ def backtrace(exception)
356
+ message = "<br/>" + sanitize(exception.message) + "<br/>"
357
+ message += exception.backtrace.collect { |line|
358
+ sanitize(line)
359
+ }.join("<br/>")
360
+ return message
361
+ end
362
+
363
+ end
364
+ end
365
+ end
366
+
@@ -0,0 +1,275 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Cukable::Conversion do
4
+
5
+ context "#feature_to_fitnesse" do
6
+ it "adds table markup" do
7
+ feature = [
8
+ 'Feature: User account',
9
+ ' Scenario: Login',
10
+ ' When I am on the login page',
11
+ ' And I fill in "Username" with "Eric"',
12
+ ' And I fill in "Password" with "foobar"',
13
+ ]
14
+ fitnesse = [
15
+ '!| Table: Cuke |',
16
+ '| Feature: User account |',
17
+ '| Scenario: Login |',
18
+ '| When I am on the login page |',
19
+ '| And I fill in "Username" with "Eric" |',
20
+ '| And I fill in "Password" with "foobar" |',
21
+ ]
22
+ feature_to_fitnesse(feature).should == fitnesse
23
+ end
24
+
25
+ it "includes unparsed text" do
26
+ feature = [
27
+ 'Feature: User account',
28
+ '',
29
+ ' As a user with an account',
30
+ ' I want to login to the website',
31
+ '',
32
+ ' Scenario: Login',
33
+ ' When I am on the login page',
34
+ ]
35
+ fitnesse = [
36
+ 'As a user with an account',
37
+ 'I want to login to the website',
38
+ '',
39
+ '!| Table: Cuke |',
40
+ '| Feature: User account |',
41
+ '| Scenario: Login |',
42
+ '| When I am on the login page |',
43
+ ]
44
+ feature_to_fitnesse(feature).should == fitnesse
45
+ end
46
+
47
+ it "correctly marks up table rows" do
48
+ feature = [
49
+ 'Feature: User account',
50
+ '',
51
+ ' Background:',
52
+ ' Given a user exists:',
53
+ ' | Username | Password |',
54
+ ' | Eric | foobar |',
55
+ ' | Ken | barfoo |',
56
+ '',
57
+ ' Scenario: Login',
58
+ ' When I am on the login page',
59
+ ]
60
+ fitnesse = [
61
+ '!| Table: Cuke |',
62
+ '| Feature: User account |',
63
+ '| Background: |',
64
+ '| Given a user exists: |',
65
+ '| | Username | Password |',
66
+ '| | Eric | foobar |',
67
+ '| | Ken | barfoo |',
68
+ '| Scenario: Login |',
69
+ '| When I am on the login page |',
70
+ ]
71
+ feature_to_fitnesse(feature).should == fitnesse
72
+ end
73
+
74
+ it "correctly marks up scenario outlines with examples" do
75
+ feature = [
76
+ 'Feature: Scenario outlines',
77
+ '',
78
+ ' Scenario: Different pages',
79
+ ' When I am on the <page> page',
80
+ ' Then I should see "<text>"',
81
+ '',
82
+ ' Examples:',
83
+ ' | page | text |',
84
+ ' | home | Relax |',
85
+ ' | office | Get to work |',
86
+ ]
87
+ fitnesse = [
88
+ '!| Table: Cuke |',
89
+ '| Feature: Scenario outlines |',
90
+ '| Scenario: Different pages |',
91
+ '| When I am on the <page> page |',
92
+ '| Then I should see "<text>" |',
93
+ '| Examples: |',
94
+ '| | page | text |',
95
+ '| | home | Relax |',
96
+ '| | office | Get to work |',
97
+ ]
98
+ feature_to_fitnesse(feature).should == fitnesse
99
+ end
100
+
101
+ it "correctly includes multi-line strings" do
102
+ feature = [
103
+ 'Feature: Strings',
104
+ '',
105
+ ' Scenario: Multi-line string',
106
+ ' Given a multi-line string:',
107
+ ' """',
108
+ ' Hello world',
109
+ ' Goodbye world',
110
+ ' """',
111
+ ]
112
+ fitnesse = [
113
+ '!| Table: Cuke |',
114
+ '| Feature: Strings |',
115
+ '| Scenario: Multi-line string |',
116
+ '| Given a multi-line string: |',
117
+ '| """ |',
118
+ '| Hello world |',
119
+ '| Goodbye world |',
120
+ '| """ |',
121
+ ]
122
+ feature_to_fitnesse(feature).should == fitnesse
123
+ end
124
+
125
+ it "outputs @tags on separate lines" do
126
+ feature = [
127
+ '@tag_a @tag_b',
128
+ 'Feature: Tags',
129
+ '',
130
+ ' @tag_c @tag_d @tag_e',
131
+ ' Scenario: Tags',
132
+ ' Given a scenario',
133
+ ]
134
+ fitnesse = [
135
+ '!| Table: Cuke |',
136
+ '| @tag_a |',
137
+ '| @tag_b |',
138
+ '| Feature: Tags |',
139
+ '| @tag_c |',
140
+ '| @tag_d |',
141
+ '| @tag_e |',
142
+ '| Scenario: Tags |',
143
+ '| Given a scenario |',
144
+ ]
145
+ feature_to_fitnesse(feature).should == fitnesse
146
+ end
147
+ end
148
+
149
+
150
+ context "#fitnesse_to_features" do
151
+ it "returns one table for each feature" do
152
+ fitnesse = [
153
+ '!| Table: Cuke |',
154
+ '| Feature: First |',
155
+ '| Scenario: Scenario 1A |',
156
+ '| Given a scenario |',
157
+ '| Scenario: Scenario 1B |',
158
+ '| Given a scenario |',
159
+ '',
160
+ '!| Table: Cuke |',
161
+ '| Feature: Second |',
162
+ '| Scenario: Scenario 2A |',
163
+ '| Given a scenario |',
164
+ '| Scenario: Scenario 2B |',
165
+ '| Given a scenario |',
166
+ ]
167
+ features = [
168
+ [
169
+ ['Feature: First'],
170
+ ['Scenario: Scenario 1A'],
171
+ ['Given a scenario'],
172
+ ['Scenario: Scenario 1B'],
173
+ ['Given a scenario'],
174
+ ],
175
+ [
176
+ ['Feature: Second'],
177
+ ['Scenario: Scenario 2A'],
178
+ ['Given a scenario'],
179
+ ['Scenario: Scenario 2B'],
180
+ ['Given a scenario'],
181
+ ],
182
+ ]
183
+ fitnesse_to_features(fitnesse).should == features
184
+ end
185
+
186
+ it "correctly interprets tables" do
187
+ fitnesse = [
188
+ '!| Table: Cuke |',
189
+ '| Feature: Tables |',
190
+ '| Scenario: Table rows |',
191
+ '| Given a table: |',
192
+ '| | First | Last |',
193
+ '| | Eric | Pierce |',
194
+ '| | Ken | Brazier |',
195
+ ]
196
+ features = [
197
+ [
198
+ ['Feature: Tables'],
199
+ ['Scenario: Table rows'],
200
+ ['Given a table:'],
201
+ ['', 'First', 'Last'],
202
+ ['', 'Eric', 'Pierce'],
203
+ ['', 'Ken', 'Brazier'],
204
+ ],
205
+ ]
206
+ fitnesse_to_features(fitnesse).should == features
207
+ end
208
+
209
+
210
+ it "correctly escapes brackets in Scenario Outlines" do
211
+ fitnesse = [
212
+ '!| Table: Cuke |',
213
+ '| Feature: Scenario Outlines |',
214
+ '| Scenario Outline: With tables |',
215
+ '| Given a user: |',
216
+ '| | First | Last |',
217
+ '| | <first> | <last> |',
218
+ '| Examples: |',
219
+ '| | first | last |',
220
+ '| | Eric | Pierce |',
221
+ '| | Ken | Brazier |',
222
+ ]
223
+ features = [
224
+ [
225
+ ['Feature: Scenario Outlines'],
226
+ ['Scenario Outline: With tables'],
227
+ ['Given a user:'],
228
+ ['', 'First', 'Last'],
229
+ ['', '<first>','<last>'],
230
+ ['Examples:'],
231
+ ['', 'first', 'last'],
232
+ ['', 'Eric', 'Pierce'],
233
+ ['', 'Ken', 'Brazier'],
234
+ ],
235
+ ]
236
+ fitnesse_to_features(fitnesse).should == features
237
+ end
238
+
239
+
240
+ it "ignores non-table lines" do
241
+ fitnesse = [
242
+ 'This text should be ignored',
243
+ '!| Table: Cuke |',
244
+ '| Feature: Extra text |',
245
+ '| Scenario: Ignore extra text|',
246
+ '| Given a scenario |',
247
+ 'This line should also be ignored',
248
+ 'And this one too',
249
+ ]
250
+ features = [
251
+ [
252
+ ['Feature: Extra text'],
253
+ ['Scenario: Ignore extra text'],
254
+ ['Given a scenario'],
255
+ ],
256
+ ]
257
+ fitnesse_to_features(fitnesse).should == features
258
+ end
259
+
260
+ it "returns an empty list if no Cuke tables are present" do
261
+ fitnesse = [
262
+ 'This wiki page has no Cuke tables',
263
+ 'So there are no tables to parse',
264
+ '| there is | this table |',
265
+ '| but it is not | a Cuke table |',
266
+ '| so it will also | be ignored |',
267
+ ]
268
+ features = []
269
+ fitnesse_to_features(fitnesse).should == features
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+