Class: Cucumber::Formatter::SlimJSON

Inherits:
Object
  • Object
show all
Includes:
Console, FileUtils, Io
Defined in:
lib/cukable/slim_json_formatter.rb

Overview

FitNesse SliM JSON output formatter for Cucumber

Instance Method Summary collapse

Constructor Details

#initialize(step_mother, path_or_io, options) ⇒ SlimJSON

Create a new SlimJSON formatter, with the provided path_or_io (as given by the --out option) and any additional options passed to cucumber.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/cukable/slim_json_formatter.rb', line 20

def initialize(step_mother, path_or_io, options)
  @step_mother = step_mother

  # Output directory
  @out_dir = path_or_io
  ensure_dir(@out_dir, "FitNesse")

  # There should be no IO until we get a feature, and
  # create the output directory in before_feature
  @io = nil

  # Cache of data lines to write
  @data = []
  # Multi-line output (must be cached and printed after the step that
  # precedes it)
  @multiline = []
  # Expected/actual, to support table diffs
  @expected_row = []
  @actual_row = []

  # Not in background until we actually find one
  @in_background = false
end

Instance Method Details

#after_background(background) ⇒ Object

Called after a Background: block.



93
94
95
# File 'lib/cukable/slim_json_formatter.rb', line 93

def after_background(background)
  @in_background = false
end

#after_examples(examples) ⇒ Object

Called after an Examples: section. Outputs anything accumulated in @multiline, and empties it.



202
203
204
205
206
207
208
# File 'lib/cukable/slim_json_formatter.rb', line 202

def after_examples(examples)
  # Output any multiline args that followed this step
  @multiline.each do |row|
    @data << row
  end
  @multiline = []
end

#after_feature(feature) ⇒ Object

Called after each .feature is run. Write all @data to the JSON file, then closes the output file.



58
59
60
61
62
# File 'lib/cukable/slim_json_formatter.rb', line 58

def after_feature(feature)
  @io.puts JSON.pretty_generate(@data)
  @io.flush
  @io.close
end

#after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background) ⇒ Object

Called after a step has executed, and we have a result. Generates a single row of output in @data, including the status of the completed step, along with one row for each line accumulated in a multi-line argument (@multiline) if any were provided. Resets @multiline when done.



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/cukable/slim_json_formatter.rb', line 267

def after_step_result(keyword, step_match, multiline_arg, status,
                      exception, source_indent, background)
  return if @hide_this_step
  # One-line message to print
  message = ''
  # A bit of a hack here to support Scenario Outlines
  # Apparently, at the time of calling after_step_result, a StepMatch in
  # a Scenario Outline has a `name` attribute (and no arguments to
  # format, because they don't get pattern-matched until the Examples:
  # section that fills them in), whereas a StepMatch in a normal scenario
  # has no value set for its `name` attribute, and *does* have arguments
  # to format. This behavior is exploited here for the sake of replacing
  # the `<...>` angle brackets that appear in Scenario Outline steps.
  #
  # In other words, if `step_match` has a `name`, assume it's in a
  # Scenario Outline, and replace the angle brackets (so the bracketed
  # parameter can be displayed in an HTML page)
  if step_match.name
    step_name = keyword + step_match.name.gsub('<', '&lt;').gsub('>', '&gt;')
  # Otherwise, wrap the arguments in bold tags
  else
    step_name = keyword + step_match.format_args("<b>%s</b>")
  end

  # Output the step name with appropriate colorization
  stat = status_map(status)
  message = "#{stat}:#{step_name}"

  # Add the source file and line number where this step was defined
  message += source_message(step_match.file_colon_line)

  # Include additional info for undefined and failed steps
  if status == :undefined
    message += "<br/>(Undefined Step)"
  elsif status == :failed && exception
    message += backtrace(exception)
  end

  # Output the final message for this step
  @data << [message]

  # Output any multiline args that followed this step
  @multiline.each do |row|
    @data << row
  end
  @multiline = []
end

#after_table_row(table_row) ⇒ Object

Called after a table row is done being read. Appends @table_row to @multiline, which will be output in after_step_result.

There is some special handling here for handling table diffs; when doing a table diff, and a row doesn't match, two rows are generated. These need to be merged into a single row in the JSON output, to maintain the 1:1 mapping between FitNesse table and the returned results.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/cukable/slim_json_formatter.rb', line 129

def after_table_row(table_row)
  return if @hide_this_step

  # If we have an @expected_row and @actual_row at this point,
  # merge them into a single row and append to @multiline_arg
  if !@expected_row.empty? && !@actual_row.empty?
    cell_diff = []
    @expected_row.zip(@actual_row) do |expect, actual|
      expect.gsub!(/^ignore:/, '')
      actual.gsub!(/^error:/, '')
      # If we got what we wanted in this cell, consider it passed
      if actual == expect
        cell_diff << "pass:#{actual}"
      # Otherwise, show expected vs. actual as a failure
      else
        cell_diff << "fail:Expected: '#{expect}'<br/>Actual: '#{actual}'"
      end
    end
    @multiline << ["report: "] + cell_diff
    # Reset for upcoming rows
    @expected_row = []
    @actual_row = []
  end

  # Row with all cells having status == :comment (ignore)?
  # This row was part of a table diff, and contains the values
  # that were expected to be in the row.
  if @table_row.all? { |cell| cell =~ /^ignore:/ }
    @expected_row = @table_row

  # Row with all cells having status == :undefined (error)?
  # This row was part of a table diff, and contains the values
  # that actually appeared in the row.
  elsif @table_row.all? { |cell| cell =~ /^error:/ }
    @actual_row = @table_row

  # For any other row, append to multiline normally
  else
    # If an exception occurred in a table row, put the exception
    # message in the first cell (which is normally empty). This
    # allows us to show the exception without adding extra rows
    # (which messes up the original table's formatting)
    if table_row.exception
      @multiline << ["fail:#{backtrace(table_row.exception)}"] + @table_row
    # Otherwise, output an empty report: cell in the first column
    else
      @multiline << ["report: "] + @table_row
    end
  end

end

#background_name(keyword, name, file_colon_line, source_indent) ⇒ Object

Called when a Background: line is read. Generates a single row of output in @data with the Background: line.



87
88
89
# File 'lib/cukable/slim_json_formatter.rb', line 87

def background_name(keyword, name, file_colon_line, source_indent)
  @data << [section_message(keyword, name, file_colon_line)]
end

#backtrace(exception) ⇒ Object

Return an exception message and backtrace



355
356
357
358
359
360
361
# File 'lib/cukable/slim_json_formatter.rb', line 355

def backtrace(exception)
  message = "<br/>" + sanitize(exception.message) + "<br/>"
  message += exception.backtrace.collect { |line|
    sanitize(line)
  }.join("<br/>")
  return message
end

#before_background(background) ⇒ Object

Called before a Background: block.



80
81
82
# File 'lib/cukable/slim_json_formatter.rb', line 80

def before_background(background)
  @in_background = true
end

#before_examples(examples) ⇒ Object

Called before an Examples: section in a Scenario Outline. An Examples: table works similarly to a multiline argument, except there is no associated step to output them. The after_table_row method will still accumulate the table rows, but we need to rely on after_examples to output them. Thus, we will be accumulating these rows in the multi-purpose @multiline variable, initialized here.



188
189
190
# File 'lib/cukable/slim_json_formatter.rb', line 188

def before_examples(examples)
  @multiline = []
end

#before_feature(feature) ⇒ Object

Called before each .feature is run. Creates a new output file for the results in @out_dir, and empties @data.



47
48
49
50
51
52
53
# File 'lib/cukable/slim_json_formatter.rb', line 47

def before_feature(feature)
  file = File.join(@out_dir, "#{feature.file}.json")
  dir = File.dirname(file)
  mkdir_p(dir) unless File.directory?(dir)
  @io = ensure_file(file, "FitNesse")
  @data = []
end

#before_multiline_arg(multiline_arg) ⇒ Object

Start a new multiline arg (such as a table or Py-string). Initializes @multiline and related arrays.



100
101
102
103
104
# File 'lib/cukable/slim_json_formatter.rb', line 100

def before_multiline_arg(multiline_arg)
  @multiline = []
  @expected_row = []
  @actual_row = []
end

#before_step(step) ⇒ Object

Called after a step has been executed, but before any output from that step has been done.



213
214
215
# File 'lib/cukable/slim_json_formatter.rb', line 213

def before_step(step)
  @current_step = step
end

#before_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background) ⇒ Object

Called before any output from a step result. To avoid redundant output, we want to show the results of Background steps only once, within the Background section (unless a background step somehow failed when it was executed at the top of a Scenario). Here, background is true if the step was defined in the Background section, and @in_background is true if we are actually inside the Background section during execution. In short, if a step was defined in the Background section, but we are not within the Background section now, we want to hide the step's output.



252
253
254
255
256
257
258
259
# File 'lib/cukable/slim_json_formatter.rb', line 252

def before_step_result(keyword, step_match, multiline_arg, status,
                       exception, source_indent, background)
  if status != :failed && @in_background ^ background
    @hide_this_step = true
  else
    @hide_this_step = false
  end
end

#before_table_row(table_row) ⇒ Object

Called before a table row is read. Starts a new @table_row.



108
109
110
# File 'lib/cukable/slim_json_formatter.rb', line 108

def before_table_row(table_row)
  @table_row = []
end

#examples_name(keyword, name) ⇒ Object

Called when the Examples: line is read. Outputs the Examples: line to @data.



195
196
197
# File 'lib/cukable/slim_json_formatter.rb', line 195

def examples_name(keyword, name)
  @data << ["report:#{keyword}: #{name}"]
end

#feature_name(keyword, name) ⇒ Object

Called when Feature: <name> is read. Generates a single row of output in @data with the feature name.



67
68
69
# File 'lib/cukable/slim_json_formatter.rb', line 67

def feature_name(keyword, name)
  @data << [section_message(keyword, name)]
end

#py_string(string) ⇒ Object

Called when a multi-line string argument is read. Generates a row of output for each line in the multi-line string (including the """ opening and closing lines), colored based on the status of the current step. The output is accumulated in @multiline, for output in after_step_result.



223
224
225
226
227
228
229
230
231
# File 'lib/cukable/slim_json_formatter.rb', line 223

def py_string(string)
  return if @hide_this_step
  status = status_map(@current_step.status)
  @multiline << [status + ':"""']
  string.split("\n").each do |line|
    @multiline << ["#{status}:#{line}"]
  end
  @multiline << [status + ':"""']
end

#sanitize(text) ⇒ Object

Return text with any HTML-specific characters sanitized



336
337
338
# File 'lib/cukable/slim_json_formatter.rb', line 336

def sanitize(text)
  text.gsub('<', '&lt;').gsub('>', '&gt;')
end

#scenario_name(keyword, name, file_colon_line, source_indent) ⇒ Object

Called when Scenario: <name> is read. Generates a single row of output in @data with the scenario name.



74
75
76
# File 'lib/cukable/slim_json_formatter.rb', line 74

def scenario_name(keyword, name, file_colon_line, source_indent)
  @data << [section_message(keyword, name, file_colon_line)]
end

#section_message(keyword, name, file_colon_line = '') ⇒ Object

Return a string suitable for use as a section heading for "Feature:", "Scenario:" or "Scenario Outline:" output rows.



349
350
351
# File 'lib/cukable/slim_json_formatter.rb', line 349

def section_message(keyword, name, file_colon_line='')
  "report:#{keyword}: #{name}" + source_message(file_colon_line)
end

#source_message(file_colon_line) ⇒ Object

Return a string for outputting the source filename and line number



342
343
344
# File 'lib/cukable/slim_json_formatter.rb', line 342

def source_message(file_colon_line)
  return " <span class=\"source_file\">" + file_colon_line + '</span>'
end

#status_map(status) ⇒ Object

Map Cucumber status strings to FitNesse status strings



322
323
324
325
326
327
328
329
330
331
332
# File 'lib/cukable/slim_json_formatter.rb', line 322

def status_map(status)
  case status
    when nil        then 'pass'
    when :passed    then 'pass'
    when :failed    then 'fail'
    when :undefined then 'error'
    when :skipped   then 'ignore'
    when :comment   then 'ignore'
    else 'pass'
  end
end

#table_cell_value(value, status) ⇒ Object

Called when a table cell value is read. Appends to @table_row.



114
115
116
117
118
# File 'lib/cukable/slim_json_formatter.rb', line 114

def table_cell_value(value, status)
  return if @hide_this_step
  stat = status_map(status)
  @table_row << "#{stat}:#{value}"
end

#tag_name(tag_name) ⇒ Object

Called when a tag name is found. Generates a single row of output in @data with the tag name. (Note that this will only work properly if there is only one tag per line; otherwise, too many lines may be output.)



238
239
240
# File 'lib/cukable/slim_json_formatter.rb', line 238

def tag_name(tag_name)
  @data << ["ignore:#{tag_name}"]
end