ugly_face 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/ChangeLog +2 -0
- data/Gemfile +13 -0
- data/Guardfile +19 -0
- data/LICENSE +21 -0
- data/README.md +25 -0
- data/Rakefile +19 -0
- data/autotrader.png +0 -0
- data/cucumber.yml +4 -0
- data/features/feature_pages.feature +110 -0
- data/features/pretty_face_report.feature +81 -0
- data/features/step_definitions/report_steps.rb +36 -0
- data/features/support/_feature_header.erb +2 -0
- data/features/support/_suite_header.erb +2 -0
- data/features/support/env.rb +20 -0
- data/features/support/error_display.rb +16 -0
- data/features/support/hooks.rb +11 -0
- data/features/support/logo.png +0 -0
- data/fixtures/advanced.feature +57 -0
- data/fixtures/background.feature +10 -0
- data/fixtures/basic.feature +20 -0
- data/fixtures/failing_background.feature +7 -0
- data/fixtures/more/more.feature +8 -0
- data/fixtures/onemore/deep/more.feature +8 -0
- data/fixtures/onemore/more.feature +8 -0
- data/fixtures/step_definitions/advanced_steps.rb +34 -0
- data/fixtures/step_definitions/basic_steps.rb +25 -0
- data/fixtures/support/env.rb +3 -0
- data/lib/.DS_Store +0 -0
- data/lib/ugly_face/.DS_Store +0 -0
- data/lib/ugly_face/formatter/html.rb +289 -0
- data/lib/ugly_face/formatter/report.rb +285 -0
- data/lib/ugly_face/formatter/view_helper.rb +61 -0
- data/lib/ugly_face/templates/_main_header.erb +1 -0
- data/lib/ugly_face/templates/_page_header.erb +2 -0
- data/lib/ugly_face/templates/_step.erb +39 -0
- data/lib/ugly_face/templates/debug.png +0 -0
- data/lib/ugly_face/templates/failed.png +0 -0
- data/lib/ugly_face/templates/feature.erb +143 -0
- data/lib/ugly_face/templates/logo.png +0 -0
- data/lib/ugly_face/templates/main.erb +162 -0
- data/lib/ugly_face/templates/passed.png +0 -0
- data/lib/ugly_face/templates/pending.png +0 -0
- data/lib/ugly_face/templates/screenshot.png +0 -0
- data/lib/ugly_face/templates/skipped.png +0 -0
- data/lib/ugly_face/templates/style.css +346 -0
- data/lib/ugly_face/templates/table_failed.png +0 -0
- data/lib/ugly_face/templates/table_passed.png +0 -0
- data/lib/ugly_face/templates/table_pending.png +0 -0
- data/lib/ugly_face/templates/table_skipped.png +0 -0
- data/lib/ugly_face/templates/table_undefined.png +0 -0
- data/lib/ugly_face/templates/undefined.png +0 -0
- data/lib/ugly_face/version.rb +3 -0
- data/lib/ugly_face.rb +45 -0
- data/spec/lib/customization_spec.rb +23 -0
- data/spec/lib/html_formatter_spec.rb +140 -0
- data/spec/spec_helper.rb +5 -0
- data/ugly_face.gemspec +29 -0
- metadata +199 -0
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'cucumber/formatter/io'
|
4
|
+
require 'cucumber/formatter/duration'
|
5
|
+
require 'cucumber/formatter/console'
|
6
|
+
require 'cucumber/ast/scenario'
|
7
|
+
require 'cucumber/ast/table'
|
8
|
+
require 'cucumber/ast/outline_table'
|
9
|
+
require File.join(File.dirname(__FILE__), 'view_helper')
|
10
|
+
require File.join(File.dirname(__FILE__), 'report')
|
11
|
+
|
12
|
+
# Starting with ActionPack 4.1.1, the module Mime doesn't get initialized before it's needed by UglyFace and so
|
13
|
+
# it would blow up with errors about uninitialized constants. We need to explicitly load it to prevent this problem.
|
14
|
+
require 'action_dispatch/http/mime_type'
|
15
|
+
|
16
|
+
module UglyFace
|
17
|
+
module Formatter
|
18
|
+
|
19
|
+
class Html
|
20
|
+
include Cucumber::Formatter::Io
|
21
|
+
include Cucumber::Formatter::Duration
|
22
|
+
include Cucumber::Formatter::Console
|
23
|
+
include ViewHelper
|
24
|
+
|
25
|
+
attr_reader :report, :logo
|
26
|
+
|
27
|
+
def initialize(step_mother, path_or_io, options)
|
28
|
+
@path = path_or_io
|
29
|
+
set_path_and_file(path_or_io)
|
30
|
+
@path_to_erb = File.join(File.dirname(__FILE__), '..', 'templates')
|
31
|
+
@step_mother = step_mother
|
32
|
+
@options = options
|
33
|
+
# The expand option is set to true by RubyMine and cannot be turned off using the IDE. This option causes
|
34
|
+
# a test run while using this gem to terminate.
|
35
|
+
@options[:expand] = false unless @options.nil?
|
36
|
+
@report = Report.new
|
37
|
+
@img_id = 0
|
38
|
+
@logo = 'logo.png'
|
39
|
+
@delayed_messages = []
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_path_and_file(path_or_io)
|
43
|
+
return if path_or_io.nil?
|
44
|
+
dir = File.dirname(path_or_io)
|
45
|
+
FileUtils.mkdir_p dir unless File.directory? dir
|
46
|
+
@io = ensure_io(path_or_io, 'html')
|
47
|
+
end
|
48
|
+
|
49
|
+
def embed(src, mime_type, label)
|
50
|
+
case(mime_type)
|
51
|
+
when /^image\/(png|gif|jpg|jpeg)/
|
52
|
+
embed_image(src, label)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def embed_image(src, label)
|
57
|
+
@report.current_scenario.image << src.split(separator).last
|
58
|
+
@report.current_scenario.image_label << label
|
59
|
+
@report.current_scenario.image_id << "img_#{@img_id}"
|
60
|
+
@img_id += 1
|
61
|
+
filename = "#{File.dirname(@path)}#{separator}images"
|
62
|
+
FileUtils.cp src, filename
|
63
|
+
end
|
64
|
+
|
65
|
+
def before_features(features)
|
66
|
+
make_output_directories
|
67
|
+
@tests_started = Time.now
|
68
|
+
end
|
69
|
+
|
70
|
+
def features_summary_file
|
71
|
+
parts = @io.path.split(separator)
|
72
|
+
parts[parts.length - 1]
|
73
|
+
end
|
74
|
+
|
75
|
+
def before_feature(feature)
|
76
|
+
@report.add_feature ReportFeature.new(feature, features_summary_file)
|
77
|
+
end
|
78
|
+
|
79
|
+
def after_feature(feature)
|
80
|
+
@report.current_feature.close(feature)
|
81
|
+
end
|
82
|
+
|
83
|
+
def before_background(background)
|
84
|
+
@report.begin_background
|
85
|
+
end
|
86
|
+
|
87
|
+
def after_background(background)
|
88
|
+
@report.end_background
|
89
|
+
@report.current_feature.background << ReportStep.new(background)
|
90
|
+
end
|
91
|
+
|
92
|
+
def before_feature_element(feature_element)
|
93
|
+
unless scenario_outline? feature_element
|
94
|
+
@report.add_scenario ReportScenario.new(feature_element)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def after_feature_element(feature_element)
|
99
|
+
unless scenario_outline?(feature_element)
|
100
|
+
process_scenario(feature_element)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def before_table_row(example_row)
|
105
|
+
@report.add_scenario ReportScenario.new(example_row) unless info_row?(example_row)
|
106
|
+
end
|
107
|
+
|
108
|
+
def after_table_row(example_row)
|
109
|
+
unless info_row?(example_row)
|
110
|
+
@report.current_scenario.populate(example_row)
|
111
|
+
build_scenario_outline_steps(example_row)
|
112
|
+
end
|
113
|
+
populate_cells(example_row) if example_row.instance_of? Cucumber::Ast::Table::Cells
|
114
|
+
end
|
115
|
+
|
116
|
+
def before_step(step)
|
117
|
+
@step_timer = Time.now
|
118
|
+
end
|
119
|
+
|
120
|
+
def after_step(step)
|
121
|
+
step = process_step(step) unless step_belongs_to_outline? step
|
122
|
+
if @cells
|
123
|
+
step.table = @cells
|
124
|
+
@cells = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def after_features(features)
|
129
|
+
@features = features
|
130
|
+
@duration = format_duration(Time.now - @tests_started)
|
131
|
+
copy_images
|
132
|
+
copy_stylesheets
|
133
|
+
generate_report
|
134
|
+
end
|
135
|
+
|
136
|
+
def features
|
137
|
+
@report.features
|
138
|
+
end
|
139
|
+
|
140
|
+
def custom_suite_header?
|
141
|
+
return false unless customization_directory
|
142
|
+
|
143
|
+
Dir.foreach(customization_directory) do |file|
|
144
|
+
return true if file == '_suite_header.erb'
|
145
|
+
end
|
146
|
+
false
|
147
|
+
end
|
148
|
+
|
149
|
+
def custom_feature_header?
|
150
|
+
return false unless customization_directory
|
151
|
+
|
152
|
+
Dir.foreach(customization_directory) do |file|
|
153
|
+
return true if file == '_feature_header.erb'
|
154
|
+
end
|
155
|
+
false
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def generate_report
|
161
|
+
paths = [@path_to_erb, customization_directory.to_s]
|
162
|
+
renderer = ActionView::Base.new(paths)
|
163
|
+
filename = File.join(@path_to_erb, 'main')
|
164
|
+
@io.puts renderer.render(:file => filename, :locals => {:report => self, :logo => @logo})
|
165
|
+
features.each do |feature|
|
166
|
+
write_feature_file(feature)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def write_feature_file(feature)
|
171
|
+
paths = [@path_to_erb, customization_directory.to_s]
|
172
|
+
renderer = ActionView::Base.new(paths)
|
173
|
+
filename = File.join(@path_to_erb, 'feature')
|
174
|
+
output_file = "#{File.dirname(@path)}#{separator}#{feature.file}"
|
175
|
+
to_cut = output_file.split(separator).last
|
176
|
+
directory = output_file.sub("#{separator}#{to_cut}", '')
|
177
|
+
FileUtils.mkdir_p directory unless File.directory? directory
|
178
|
+
file = File.new(output_file, Cucumber.file_mode('w'))
|
179
|
+
file.puts renderer.render(:file => filename, :locals => {:feature => feature, :logo => @logo, :customize => custom_feature_header?})
|
180
|
+
file.flush
|
181
|
+
file.close
|
182
|
+
end
|
183
|
+
|
184
|
+
def make_output_directories
|
185
|
+
make_directory 'images'
|
186
|
+
make_directory 'stylesheets'
|
187
|
+
end
|
188
|
+
|
189
|
+
def make_directory(dir)
|
190
|
+
path = "#{File.dirname(@path)}#{separator}#{dir}"
|
191
|
+
FileUtils.mkdir_p path unless File.directory? path
|
192
|
+
end
|
193
|
+
|
194
|
+
def copy_directory(dir, file_names, file_extension)
|
195
|
+
path = "#{File.dirname(@path)}#{separator}#{dir}"
|
196
|
+
file_names.each do |file|
|
197
|
+
copy_file File.join(File.dirname(__FILE__), '..', 'templates', "#{file}.#{file_extension}"), path
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def copy_file(source, destination)
|
202
|
+
FileUtils.cp source, destination
|
203
|
+
end
|
204
|
+
|
205
|
+
def copy_images
|
206
|
+
copy_directory 'images', %w(debug screenshot failed passed pending undefined skipped table_failed table_passed table_pending table_undefined table_skipped), "png"
|
207
|
+
logo = logo_file
|
208
|
+
copy_file logo, "#{File.join(File.dirname(@path), 'images')}" if logo
|
209
|
+
copy_directory 'images', ['custom_logo'], 'png' unless logo
|
210
|
+
end
|
211
|
+
|
212
|
+
def copy_stylesheets
|
213
|
+
copy_directory 'stylesheets', ['style'], 'css'
|
214
|
+
end
|
215
|
+
|
216
|
+
def logo_file
|
217
|
+
dir = customization_directory
|
218
|
+
Dir.foreach(dir) do |file|
|
219
|
+
if file =~ /^logo\.(png|gif|jpg|jpeg)$/
|
220
|
+
@logo = file
|
221
|
+
return File.join(dir, file)
|
222
|
+
end
|
223
|
+
end if dir
|
224
|
+
end
|
225
|
+
|
226
|
+
def customization_directory
|
227
|
+
dir = File.join(File.expand_path('features'), 'support', 'ugly_face')
|
228
|
+
return dir if File.exists? dir
|
229
|
+
end
|
230
|
+
|
231
|
+
def process_scenario(scenario)
|
232
|
+
@report.current_scenario.populate(scenario)
|
233
|
+
end
|
234
|
+
|
235
|
+
def process_step(step, status=nil)
|
236
|
+
duration = Time.now - @step_timer
|
237
|
+
report_step = ReportStep.new(step)
|
238
|
+
report_step.duration = duration
|
239
|
+
report_step.status = status unless status.nil?
|
240
|
+
if step.background?
|
241
|
+
@report.current_feature.background << report_step if @report.processing_background_steps?
|
242
|
+
else
|
243
|
+
@report.add_step report_step
|
244
|
+
end
|
245
|
+
report_step
|
246
|
+
end
|
247
|
+
|
248
|
+
def scenario_outline?(feature_element)
|
249
|
+
feature_element.is_a? Cucumber::Ast::ScenarioOutline
|
250
|
+
end
|
251
|
+
|
252
|
+
def info_row?(example_row)
|
253
|
+
return example_row.scenario_outline.nil? if example_row.respond_to? :scenario_outline
|
254
|
+
return true if example_row.instance_of? Cucumber::Ast::Table::Cells
|
255
|
+
false
|
256
|
+
end
|
257
|
+
|
258
|
+
def step_belongs_to_outline?(step)
|
259
|
+
scenario = step.instance_variable_get "@feature_element"
|
260
|
+
not scenario.nil?
|
261
|
+
end
|
262
|
+
|
263
|
+
def build_scenario_outline_steps(example_row)
|
264
|
+
si = example_row.instance_variable_get :@step_invocations
|
265
|
+
si.each do |row|
|
266
|
+
process_step(row, row.status)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def step_error(exception)
|
271
|
+
return nil if exception.nil?
|
272
|
+
exception.backtrace[-1] =~ /^#{step.file_colon_line}/ ? exception : nil
|
273
|
+
end
|
274
|
+
|
275
|
+
def populate_cells(example_row)
|
276
|
+
@cells ||= []
|
277
|
+
values = []
|
278
|
+
example_row.to_a.each do |cell|
|
279
|
+
values << cell.value
|
280
|
+
end
|
281
|
+
@cells << values
|
282
|
+
end
|
283
|
+
|
284
|
+
def separator
|
285
|
+
File::ALT_SEPARATOR || File::SEPARATOR
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
module UglyFace
|
2
|
+
module Formatter
|
3
|
+
|
4
|
+
module Formatting
|
5
|
+
def summary_percent(number, total)
|
6
|
+
percent = (number.to_f / total) * 100
|
7
|
+
"#{number} <span class=\"percentage\">(#{'%.1f' % percent}%)</span>"
|
8
|
+
end
|
9
|
+
|
10
|
+
def formatted_duration(duration)
|
11
|
+
m, s = duration.divmod(60)
|
12
|
+
"#{m}m#{'%.3f' % s}s"
|
13
|
+
rescue
|
14
|
+
"N m Ns"
|
15
|
+
end
|
16
|
+
|
17
|
+
def image_tag_for(status, source=nil)
|
18
|
+
dir = "#{directory_prefix_for(source)}images"
|
19
|
+
"<img src=\"#{dir}/#{status}.png\" alt=\"#{status}\" title=\"#{status}\">"
|
20
|
+
end
|
21
|
+
|
22
|
+
def table_image_for(status, source=nil)
|
23
|
+
dir = "#{directory_prefix_for(source)}images"
|
24
|
+
"<img src=\"#{dir}/table_#{status}.png\" alt=\"#{status}\" title=\"#{status}\">"
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def directory_prefix_for(source=nil)
|
29
|
+
dir = ''
|
30
|
+
back_dir = source.count(separator) if source
|
31
|
+
back_dir.times do
|
32
|
+
dir += "..#{separator}"
|
33
|
+
end
|
34
|
+
dir
|
35
|
+
end
|
36
|
+
|
37
|
+
def separator
|
38
|
+
File::ALT_SEPARATOR || File::SEPARATOR
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class Report
|
44
|
+
attr_reader :features
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@features = []
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_feature
|
51
|
+
@features.last
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_scenario
|
55
|
+
current_feature.scenarios.last
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_feature(feature)
|
59
|
+
@features << feature
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_scenario(scenario)
|
63
|
+
current_feature.scenarios << scenario
|
64
|
+
end
|
65
|
+
|
66
|
+
def begin_background
|
67
|
+
@processing_background = true
|
68
|
+
end
|
69
|
+
|
70
|
+
def end_background
|
71
|
+
@processing_background = false
|
72
|
+
end
|
73
|
+
|
74
|
+
def processing_background_steps?
|
75
|
+
@processing_background
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_step(step)
|
79
|
+
current_scenario.steps << step
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class ReportFeature
|
84
|
+
include Formatting
|
85
|
+
attr_accessor :scenarios, :background, :description
|
86
|
+
attr_reader :title, :file, :start_time, :duration, :parent_filename
|
87
|
+
|
88
|
+
def initialize(feature, parent_filename)
|
89
|
+
@scenarios = []
|
90
|
+
@background = []
|
91
|
+
@start_time = Time.now
|
92
|
+
@description = feature.description
|
93
|
+
@parent_filename = parent_filename
|
94
|
+
end
|
95
|
+
|
96
|
+
def close(feature)
|
97
|
+
@title = feature.title
|
98
|
+
@duration = Time.now - start_time
|
99
|
+
a_file = feature.file.sub(/\.feature/, '.html')
|
100
|
+
to_cut = a_file.split(separator).first
|
101
|
+
@file = a_file.sub("#{to_cut}#{separator}", '')
|
102
|
+
end
|
103
|
+
|
104
|
+
def steps
|
105
|
+
steps = []
|
106
|
+
scenarios.each { |scenario| steps += scenario.steps }
|
107
|
+
steps
|
108
|
+
end
|
109
|
+
|
110
|
+
def background_title
|
111
|
+
title = @background.find { |step| step.keyword.nil? }
|
112
|
+
end
|
113
|
+
|
114
|
+
def background_steps
|
115
|
+
@background.find_all { |step| step.keyword }
|
116
|
+
end
|
117
|
+
|
118
|
+
def scenarios_for(status)
|
119
|
+
scenarios.find_all { |scenario| scenario.status == status }
|
120
|
+
end
|
121
|
+
|
122
|
+
def scenario_summary_for(status)
|
123
|
+
scenarios_with_status = scenarios_for(status)
|
124
|
+
summary_percent(scenarios_with_status.length, scenarios.length)
|
125
|
+
end
|
126
|
+
|
127
|
+
def step_summary_for(status)
|
128
|
+
steps_with_status = steps.find_all { |step| step.status == status }
|
129
|
+
summary_percent(steps_with_status.length, steps.length)
|
130
|
+
end
|
131
|
+
|
132
|
+
def scenario_average_duration
|
133
|
+
durations = scenarios.collect { |scenario| scenario.duration }
|
134
|
+
formatted_duration(durations.reduce(:+).to_f / durations.size)
|
135
|
+
end
|
136
|
+
|
137
|
+
def step_average_duration
|
138
|
+
steps = scenarios.collect { |scenario| scenario.steps }
|
139
|
+
durations = steps.flatten.collect { |step| step.duration }
|
140
|
+
formatted_duration(durations.reduce(:+).to_f / durations.size)
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_binding
|
144
|
+
binding
|
145
|
+
end
|
146
|
+
|
147
|
+
def description?
|
148
|
+
!description.nil? && !description.empty?
|
149
|
+
end
|
150
|
+
|
151
|
+
def has_background?
|
152
|
+
background.length > 0
|
153
|
+
end
|
154
|
+
|
155
|
+
def file
|
156
|
+
@file.split("features#{separator}").last
|
157
|
+
end
|
158
|
+
|
159
|
+
def parent_filename
|
160
|
+
@parent_filename.split(separator).last
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class ReportScenario
|
165
|
+
attr_accessor :name, :file_colon_line, :status, :steps, :duration, :image, :image_label, :image_id
|
166
|
+
|
167
|
+
def initialize(scenario)
|
168
|
+
@steps = []
|
169
|
+
@image = []
|
170
|
+
@image_label = []
|
171
|
+
@image_id = []
|
172
|
+
@start = Time.now
|
173
|
+
end
|
174
|
+
|
175
|
+
def populate(scenario)
|
176
|
+
@duration = Time.now - @start
|
177
|
+
if scenario.instance_of? Cucumber::Ast::Scenario
|
178
|
+
@name = scenario.name
|
179
|
+
@file_colon_line = scenario.file_colon_line
|
180
|
+
elsif scenario.instance_of? Cucumber::Ast::OutlineTable::ExampleRow
|
181
|
+
@name = scenario.scenario_outline.name
|
182
|
+
@file_colon_line = scenario.backtrace_line
|
183
|
+
end
|
184
|
+
@status = scenario.status
|
185
|
+
end
|
186
|
+
|
187
|
+
def has_image?
|
188
|
+
not image.nil?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class ReportStep
|
193
|
+
attr_accessor :name, :keyword, :file_colon_line, :status, :duration, :table, :multiline_arg, :error
|
194
|
+
|
195
|
+
def initialize(step)
|
196
|
+
@name = step.name
|
197
|
+
@file_colon_line = step.file_colon_line
|
198
|
+
unless step.instance_of? Cucumber::Ast::Background
|
199
|
+
if step.respond_to? :actual_keyword
|
200
|
+
@keyword = step.actual_keyword
|
201
|
+
else
|
202
|
+
@keyword = step.keyword
|
203
|
+
end
|
204
|
+
@status = step.status
|
205
|
+
@multiline_arg = step.multiline_arg
|
206
|
+
@error = step.exception
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def failed_with_error?
|
211
|
+
status == :failed && !error.nil?
|
212
|
+
end
|
213
|
+
|
214
|
+
def has_table?
|
215
|
+
not table.nil?
|
216
|
+
end
|
217
|
+
|
218
|
+
def has_multiline_arg?
|
219
|
+
!multiline_arg.nil? && !has_table?
|
220
|
+
end
|
221
|
+
|
222
|
+
def file_with_error(file_colon_line)
|
223
|
+
@snippet_extractor ||= SnippetExtractor.new
|
224
|
+
file, line = @snippet_extractor.file_name_and_line(file_colon_line)
|
225
|
+
file
|
226
|
+
end
|
227
|
+
|
228
|
+
#from cucumber ===================
|
229
|
+
def extra_failure_content(file_colon_line)
|
230
|
+
@snippet_extractor ||= SnippetExtractor.new
|
231
|
+
@snippet_extractor.snippet(file_colon_line)
|
232
|
+
end
|
233
|
+
|
234
|
+
class SnippetExtractor
|
235
|
+
require 'syntax/convertors/html';
|
236
|
+
@@converter = Syntax::Convertors::HTML.for_syntax "ruby"
|
237
|
+
|
238
|
+
def file_name_and_line(error_line)
|
239
|
+
if error_line =~ /(.*):(\d+)/
|
240
|
+
[$1, $2.to_i]
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def snippet(error)
|
245
|
+
raw_code, line, file = snippet_for(error[0])
|
246
|
+
highlighted = @@converter.convert(raw_code, false)
|
247
|
+
|
248
|
+
"<pre class=\"ruby\"><strong>#{file + "\n"}</strong><code>#{post_process(highlighted, line)}</code></pre>"
|
249
|
+
end
|
250
|
+
|
251
|
+
def snippet_for(error_line)
|
252
|
+
file, line = file_name_and_line(error_line)
|
253
|
+
if file
|
254
|
+
[lines_around(file, line), line, file]
|
255
|
+
else
|
256
|
+
["# Couldn't get snippet for #{error_line}", 1, 'File Unknown']
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def lines_around(file, line)
|
261
|
+
if File.file?(file)
|
262
|
+
# lines = File.open(file).read.split("\n")
|
263
|
+
lines = File.readlines(file)
|
264
|
+
min = [0, line-3].max
|
265
|
+
max = [line+1, lines.length-1].min
|
266
|
+
# lines[min..max].join("\n")
|
267
|
+
lines[min..max].join
|
268
|
+
else
|
269
|
+
"# Couldn't get snippet for #{file}"
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def post_process(highlighted, offending_line)
|
274
|
+
new_lines = []
|
275
|
+
highlighted.split("\n").each_with_index do |line, i|
|
276
|
+
new_line = "<span class=\"linenum\">#{offending_line+i-2}</span>#{line}"
|
277
|
+
new_line = "<span class=\"offending\">#{new_line}</span>" if i == 2
|
278
|
+
new_lines << new_line
|
279
|
+
end
|
280
|
+
new_lines.join("\n")
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'cucumber/ast/scenario_outline'
|
2
|
+
|
3
|
+
module UglyFace
|
4
|
+
module Formatter
|
5
|
+
module ViewHelper
|
6
|
+
|
7
|
+
def start_time
|
8
|
+
@tests_started.strftime("%a %B %-d, %Y at %H:%M:%S")
|
9
|
+
end
|
10
|
+
|
11
|
+
def step_count
|
12
|
+
@step_mother.steps.length
|
13
|
+
end
|
14
|
+
|
15
|
+
def scenario_count
|
16
|
+
@step_mother.scenarios.length
|
17
|
+
end
|
18
|
+
|
19
|
+
def total_duration
|
20
|
+
@duration
|
21
|
+
end
|
22
|
+
|
23
|
+
def step_average_duration(features)
|
24
|
+
scenarios = features.collect { |feature| feature.scenarios }
|
25
|
+
steps = scenarios.flatten.collect { |scenario| scenario.steps }
|
26
|
+
durations = steps.flatten.collect { |step| step.duration }
|
27
|
+
format_duration get_average_from_float_array durations
|
28
|
+
end
|
29
|
+
|
30
|
+
def scenario_average_duration(features)
|
31
|
+
scenarios = features.collect { |feature| feature.scenarios }
|
32
|
+
durations = scenarios.flatten.collect { |scenario| scenario.duration }
|
33
|
+
format_duration get_average_from_float_array durations
|
34
|
+
end
|
35
|
+
|
36
|
+
def scenarios_summary_for(status)
|
37
|
+
summary_percent(@step_mother.scenarios(status).length, scenario_count)
|
38
|
+
end
|
39
|
+
|
40
|
+
def steps_summary_for(status)
|
41
|
+
summary_percent(@step_mother.steps(status).length, step_count)
|
42
|
+
end
|
43
|
+
|
44
|
+
def failed_scenario?(scenario)
|
45
|
+
scenario.status == :failed
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def get_average_from_float_array(arr)
|
52
|
+
arr.reduce(:+).to_f / arr.size
|
53
|
+
end
|
54
|
+
|
55
|
+
def summary_percent(number, total)
|
56
|
+
percent = (number.to_f / total) * 100
|
57
|
+
"#{number} <span class=\"percentage\">(#{'%.1f' % percent}%)</span>"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h2 class="results">Run Results</h2>
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<%= "#{step.keyword} #{step.name}" %>
|
2
|
+
<% if step.has_table? %>
|
3
|
+
<br />
|
4
|
+
<table border="1" class="param_table">
|
5
|
+
<% step.table.each do |row| %>
|
6
|
+
<tr>
|
7
|
+
<% row.each_with_index do |column, index| %>
|
8
|
+
<% if index == 0 %>
|
9
|
+
<th><%= column %></th>
|
10
|
+
<% else %>
|
11
|
+
<td><%= column %></td>
|
12
|
+
<% end %>
|
13
|
+
<% end %>
|
14
|
+
</tr>
|
15
|
+
<% end %>
|
16
|
+
</table>
|
17
|
+
<% end %>
|
18
|
+
<% if step.has_multiline_arg? %>
|
19
|
+
<br />
|
20
|
+
<table border="1" class="multiline_arg">
|
21
|
+
<tr>
|
22
|
+
<td><pre><%= step.multiline_arg %></pre></td>
|
23
|
+
</tr>
|
24
|
+
</table>
|
25
|
+
<% end %>
|
26
|
+
<% if step.failed_with_error? %>
|
27
|
+
<table>
|
28
|
+
<tr class="error">
|
29
|
+
<td class="message">
|
30
|
+
<strong><pre><%= "#{step.error.message} (#{step.error.class})" %></pre></strong>
|
31
|
+
</td>
|
32
|
+
</tr>
|
33
|
+
<tr>
|
34
|
+
<td>
|
35
|
+
<%= raw(step.extra_failure_content(step.error.backtrace)) %>
|
36
|
+
</td>
|
37
|
+
</tr>
|
38
|
+
</table>
|
39
|
+
<% end %>
|
Binary file
|
Binary file
|