cukable 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+