report_builder 0.1.4 → 0.1.5

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,646 @@
1
+ require 'json'
2
+ require 'builder'
3
+ require 'base64'
4
+ require 'report_builder/core-ext/hash'
5
+
6
+ module ReportBuilder
7
+ class Builder
8
+
9
+ attr_accessor :options
10
+
11
+
12
+ # colors corresponding to status
13
+ COLOR = {
14
+ passed: '#90ed7d',
15
+ working: '#90ed7d',
16
+ failed: '#f45b5b',
17
+ broken: '#f45b5b',
18
+ undefined: '#e4d354',
19
+ incomplete: '#e7a35c',
20
+ pending: '#f7a35c',
21
+ skipped: '#7cb5ec',
22
+ output: '#007fff'
23
+ }
24
+
25
+ def build_report(opts = nil)
26
+
27
+ options = default_options.marshal_dump unless options
28
+ options.merge! opts if opts.is_a? Hash
29
+
30
+ raise 'Error: Invalid report_types Use: [:json, :html]' unless options[:report_types].is_a? Array
31
+ raise 'Error: Invalid report_tabs Use: [:overview, :features, :scenarios, :errors]' unless options[:report_tabs].is_a? Array
32
+
33
+ options[:report_types].map!(&:to_s).map!(&:upcase)
34
+ options[:report_tabs].map!(&:to_s).map!(&:downcase)
35
+
36
+ input = files options[:json_path]
37
+ all_features = features input rescue (raise 'ReportBuilderParsingError')
38
+
39
+ report_name = options[:json_report_path] || options[:report_path]
40
+ File.open(report_name + '.json', 'w') do |file|
41
+ file.write JSON.pretty_generate all_features
42
+ end if options[:report_types].include? 'JSON'
43
+
44
+ all_scenarios = scenarios all_features
45
+ all_steps = steps all_scenarios
46
+ all_tags = tags all_scenarios
47
+ total_time = total_time all_features
48
+ feature_data = data all_features
49
+ scenario_data = data all_scenarios
50
+ step_data = data all_steps
51
+
52
+ report_name = options[:html_report_path] || options[:report_path]
53
+ File.open(report_name + '.html', 'w:UTF-8') do |file|
54
+ @builder = ::Builder::XmlMarkup.new(target: file, indent: 0)
55
+ @builder.declare!(:DOCTYPE, :html)
56
+ @builder << '<html>'
57
+
58
+ @builder.head do
59
+ @builder.meta(charset: 'UTF-8')
60
+ @builder.title options[:report_title]
61
+
62
+ @builder.style(type: 'text/css') do
63
+ @builder << File.read(File.dirname(__FILE__) + '/../../vendor/assets/stylesheets/jquery-ui.min.css')
64
+ COLOR.each do |color|
65
+ @builder << ".#{color[0].to_s}{background:#{color[1]};color:#434348;padding:2px}"
66
+ end
67
+ @builder << '.summary{margin-bottom:4px;border: 1px solid #c5c5c5;border-radius:4px;background:#f1f1f1;color:#434348;padding:4px;overflow:hidden;vertical-align:bottom;}'
68
+ @builder << '.summary .results{text-align:right;float:right;}'
69
+ @builder << '.summary .info{text-align:left;float:left;}'
70
+ @builder << '.data_table{border-collapse: collapse;} .data_table td{padding: 5px; border: 1px solid #ddd;}'
71
+ @builder << '.ui-tooltip{background: black; color: white; font-size: 12px; padding: 2px 4px; border-radius: 20px; box-shadow: 0 0 7px black;}'
72
+ end
73
+
74
+ @builder.script(type: 'text/javascript') do
75
+ %w(jquery-min jquery-ui.min highcharts highcharts-3d).each do |js|
76
+ @builder << File.read(File.dirname(__FILE__) + '/../../vendor/assets/javascripts/' + js + '.js')
77
+ end
78
+ @builder << '$(function(){$("#results").tabs();});'
79
+ @builder << "$(function(){$('#features').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
80
+ (0..all_features.size).each do |n|
81
+ @builder << "$(function(){$('#feature#{n}').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
82
+ end
83
+ @builder << "$(function(){$('#status').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
84
+ scenario_data.each do |data|
85
+ @builder << "$(function(){$('##{data[:name]}').accordion({collapsible: true, heightStyle: 'content', active: false, icons: false});});"
86
+ end
87
+ @builder << '$(function() {$(document).tooltip({track: true});});'
88
+ end
89
+ end
90
+
91
+ @builder << '<body>'
92
+
93
+ @builder.div(class: 'summary') do
94
+ @builder.span(class: 'info') do
95
+ info = options[:additional_info].empty?
96
+ @builder << '<br/>&nbsp;&nbsp;&nbsp;' if info
97
+ @builder.span(style: "font-size:#{info ? 36 : 18 }px;font-weight: bold;") do
98
+ @builder << options[:report_title]
99
+ end
100
+ options[:additional_info].each do |l|
101
+ @builder << '<br/>' + l[0].to_s.capitalize + ' : ' + l[1].to_s
102
+ end
103
+ end if options[:additional_info].is_a? Hash
104
+ @builder.span(class: 'results') do
105
+ s = all_features.size
106
+ @builder << s.to_s + " feature#{'s' if s > 1} ("
107
+ feature_data.each do |data|
108
+ @builder << ' ' + data[:count].to_s + ' ' + data[:name]
109
+ end
110
+ s = all_scenarios.size
111
+ @builder << ')<br/>' + s.to_s + " scenario#{'s' if s > 1} ("
112
+ scenario_data.each do |data|
113
+ @builder << ' ' + data[:count].to_s + ' ' + data[:name]
114
+ end
115
+ s = all_steps.size
116
+ @builder << ')<br/>' + s.to_s + " step#{'s' if s > 1} ("
117
+ step_data.each do |data|
118
+ @builder << ' ' + data[:count].to_s + ' ' + data[:name]
119
+ end
120
+ @builder << ')<br/>&#128336; ' + duration(total_time).to_s
121
+ end
122
+ end
123
+
124
+ @builder.div(id: 'results') do
125
+ build_menu options[:report_tabs]
126
+
127
+ @builder.div(id: 'overviewTab') do
128
+ @builder << "<div id='featurePieChart' style=\"float:left;width:33%\"></div>"
129
+ @builder << "<div id='scenarioPieChart' style=\"display:inline-block;width:33%\"></div>"
130
+ @builder << "<div id='stepPieChart' style=\"float:right;width:33%\"></div>"
131
+ end if options[:report_tabs].include? 'overview'
132
+
133
+ @builder.div(id: 'featuresTab') do
134
+ build_tags_drop_down(all_tags)
135
+ @builder.div(id: 'features') do
136
+ all_features.each_with_index do |feature, n|
137
+ @builder.h3(style: "background:#{COLOR[feature['status'].to_sym]}") do
138
+ @builder.span(class: feature['status']) do
139
+ @builder << "<strong>#{feature['keyword']}</strong> #{feature['name']} (#{duration(feature['duration'])})"
140
+ end
141
+ end
142
+ @builder.div do
143
+ @builder.div(id: "feature#{n}") do
144
+ feature['elements'].each {|scenario| build_scenario scenario}
145
+ end
146
+ end
147
+ end
148
+ end
149
+ @builder << "<div id='featureTabPieChart'></div>"
150
+ end if options[:report_tabs].include? 'features'
151
+
152
+ @builder.div(id: 'scenariosTab') do
153
+ build_tags_drop_down(all_tags)
154
+ @builder.div(id: 'status') do
155
+ all_scenarios.group_by {|scenario| scenario['status']}.each do |data|
156
+ @builder.h3(style: "background:#{COLOR[data[0].to_sym]}") do
157
+ @builder.span(class: data[0]) do
158
+ @builder << "<strong>#{data[0].capitalize} scenarios (Count: <span id='count'>#{data[1].size}</span>)</strong>"
159
+ end
160
+ end
161
+ @builder.div do
162
+ @builder.div(id: data[0]) do
163
+ data[1].sort_by {|scenario| scenario['name']}.each {|scenario| build_scenario scenario}
164
+ end
165
+ end
166
+ end
167
+ end
168
+ @builder << "<div id='scenarioTabPieChart'></div>"
169
+ end if options[:report_tabs].include? 'scenarios'
170
+
171
+ @builder.div(id: 'errorsTab') do
172
+ @builder.ol do
173
+ all_scenarios.each {|scenario| build_error_list scenario}
174
+ end
175
+ end if options[:report_tabs].include? 'errors'
176
+ end
177
+
178
+ @builder.script(type: 'text/javascript') do
179
+ @builder << pie_chart_js('featurePieChart', 'Features', feature_data) if options[:report_tabs].include? 'overview'
180
+ @builder << donut_js('featureTabPieChart', 'Features', feature_data) if options[:report_tabs].include? 'features'
181
+ @builder << pie_chart_js('scenarioPieChart', 'Scenarios', scenario_data) if options[:report_tabs].include? 'overview'
182
+ @builder << donut_js('scenarioTabPieChart', 'Scenarios', scenario_data) if options[:report_tabs].include? 'scenarios'
183
+ @builder << pie_chart_js('stepPieChart', 'Steps', step_data) if options[:report_tabs].include? 'overview'
184
+ unless all_tags.empty?
185
+ @builder << '$("#featuresTab .select-tags").change(function(){
186
+ $("#featuresTab .scenario-all").hide().next().hide().parent().hide().parent().hide().prev().hide();
187
+ $("#featuresTab ." + $(this).val()).show().parent().show().parent().prev().show();});' if options[:report_tabs].include? 'features'
188
+ @builder << '$("#scenariosTab .select-tags").change(function(){var val = $(this).val();$("#scenariosTab .scenario-all").hide().next().hide();
189
+ $("#scenariosTab ." + val).show();$("#scenariosTab #count").each(function(){status = $(this).parent().parent().prop("className");
190
+ count = $("#scenariosTab #" + status + " ." + val).length;countElement = $("#scenariosTab ." + status + " #count");
191
+ countElement.parent().parent().parent().show();if(count==0){countElement.parent().parent().parent().hide().next().hide();}
192
+ countElement.html(count);});});' if options[:report_tabs].include? 'scenarios'
193
+ end
194
+ end
195
+
196
+ @builder << '</body>'
197
+ @builder << '</html>'
198
+
199
+ end if options[:report_types].include? 'HTML'
200
+
201
+ report_name = options[:retry_report_path] || options[:report_path]
202
+ File.open(report_name + '.retry', 'w:UTF-8') do |file|
203
+ all_features.each do |feature|
204
+ if feature['status'] == 'broken'
205
+ feature['elements'].each {|scenario| file.puts "#{feature['uri']}:#{scenario['line']}" if scenario['status'] == 'failed'}
206
+ end
207
+ end
208
+ end if options[:report_types].include? 'RETRY'
209
+
210
+ [total_time, feature_data, scenario_data, step_data]
211
+ end
212
+
213
+ def default_options
214
+ OpenStruct.new(
215
+ json_path: nil, # [String] / [Array] Input json file, array of json files/path or json files path, (Default current directory)
216
+ report_path: 'test_report', # [String] Output file path with name
217
+ report_types: [:html], # [Array] Output file types to build, [:json, :html] or ['html', 'json']
218
+ report_tabs: [:overview, :features], # [Array] Tabs to build, [:overview, :features, :scenarios, :errors] or ['overview', 'features', 'scenarios', 'errors']
219
+ report_title: 'Test Results', # [String] Report and html title
220
+ compress_images: false, # [Boolean] Set true to reducing the size of HTML report, Note: If true, takes more time to build report
221
+ additional_info: {} # [Hash] Additional info for report summary
222
+ )
223
+ end
224
+
225
+ private
226
+
227
+ def build_menu(tabs)
228
+ @builder.ul do
229
+ tabs.each do |tab|
230
+ @builder.li do
231
+ @builder.a(href: "##{tab}Tab") do
232
+ @builder << tab.capitalize
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ def build_scenario(scenario)
240
+ tags = (scenario['tags'] ? scenario['tags'].map {|tag| tag['name']}.join(' ') : '')
241
+ @builder.h3(style: "background:#{COLOR[scenario['status'].to_sym]}", title: tags, class: 'scenario-all ' + tags.gsub('@', 'tag-')) do
242
+ @builder.span(class: scenario['status']) do
243
+ @builder << "<strong>#{scenario['keyword']}</strong> #{scenario['name']} (#{duration(scenario['duration'])})"
244
+ end
245
+ end
246
+ @builder.div do
247
+ scenario['before'].each do |before|
248
+ build_hook_error before
249
+ end
250
+ scenario['steps'].each do |step|
251
+ build_step step, scenario['keyword']
252
+ end
253
+ scenario['after'].each do |after|
254
+ build_output after['output']
255
+ build_hook_error after
256
+ build_embedding after['embeddings']
257
+ end
258
+ end
259
+ end
260
+
261
+ def build_step(step, scenario_keyword)
262
+ @builder.div(class: step['status']) do
263
+ @builder << "<strong>#{step['keyword']}</strong> #{step['name']} (#{duration(step['duration'])})"
264
+ end
265
+ build_data_table step['rows']
266
+ build_output step['output']
267
+ build_step_error step
268
+ build_embedding step['embeddings']
269
+ step['after'].each do |after|
270
+ build_output after['output']
271
+ build_step_hook_error after, scenario_keyword
272
+ build_embedding after['embeddings']
273
+ end if step['after']
274
+ end
275
+
276
+ def build_data_table(rows)
277
+ @builder.table(class: 'data_table', style: 'margin: 10px') do
278
+ rows.each do |row|
279
+ @builder.tr do
280
+ row['cells'].each do |cell|
281
+ @builder << "<td> #{cell} </td>"
282
+ end
283
+ end
284
+ end
285
+ end if rows.is_a? Array
286
+ end
287
+
288
+ def build_output(outputs)
289
+ outputs.each do |output|
290
+ @builder << "<span style='color:#{COLOR[:output]}'>#{output.to_s.gsub("\n", '</br>').gsub("\t", '&nbsp;&nbsp;').gsub(' ', '&nbsp;')}</span><br/>"
291
+ end if outputs.is_a?(Array)
292
+ end
293
+
294
+ def build_tags_drop_down(tags)
295
+ @builder.div(style: 'text-align:center;padding:5px;') do
296
+ @builder << '<strong>Tag: </strong>'
297
+ @builder.select(class: 'select-tags') do
298
+ @builder.option(value: 'scenario-all') do
299
+ @builder << 'All'
300
+ end
301
+ tags.sort.each do |tag|
302
+ @builder.option(value: tag.gsub('@', 'tag-')) do
303
+ @builder << tag
304
+ end
305
+ end
306
+ end
307
+ end if tags.is_a?(Array)
308
+ end
309
+
310
+ def build_step_error(step)
311
+ if step['status'] == 'failed' && step['result']['error_message']
312
+ @builder << "<strong style=color:#{COLOR[:failed]}>Error: </strong>"
313
+ error = step['result']['error_message'].split("\n")
314
+ @builder.span(style: "color:#{COLOR[:failed]}") do
315
+ error[0..-3].each do |line|
316
+ @builder << line + '<br/>'
317
+ end
318
+ end
319
+ @builder << "<strong>SD: </strong>#{error[-2]} <br/>"
320
+ @builder << "<strong>FF: </strong>#{error[-1]}<br/>"
321
+ end
322
+ end
323
+
324
+ def build_hook_error(hook)
325
+ if hook['status'] == 'failed'
326
+ @builder << "<strong style=color:#{COLOR[:failed]}>Error: </strong>"
327
+ error = hook['result']['error_message'].split("\n")
328
+ @builder.span(style: "color:#{COLOR[:failed]}") do
329
+ error[0..-2].each do |line|
330
+ @builder << line + '<br/>'
331
+ end
332
+ end
333
+ @builder << "<strong>Hook: </strong>#{error[-1]}<br/>"
334
+ end
335
+ end
336
+
337
+ def build_step_hook_error(hook, scenario_keyword)
338
+ if hook['result']['error_message']
339
+ @builder << "<strong style=color:#{COLOR[:failed]}>Error: </strong>"
340
+ error = hook['result']['error_message'].split("\n")
341
+ @builder.span(style: "color:#{COLOR[:failed]}") do
342
+ (scenario_keyword == 'Scenario Outline' ? error[0..-8] : error[0..-5]).each do |line|
343
+ @builder << line + '<br/>'
344
+ end
345
+ end
346
+ @builder << "<strong>Hook: </strong>#{scenario_keyword == 'Scenario Outline' ? error[-7] : error[-4]} <br/>"
347
+ @builder << "<strong>FF: </strong>#{error[-2]}<br/>"
348
+ end
349
+ end
350
+
351
+ def build_embedding(embeddings)
352
+ @embedding_count ||= 0
353
+ embeddings.each do |embedding|
354
+ src = Base64.decode64(embedding['data'])
355
+ id = "embedding_#{@embedding_count}"
356
+ if embedding['mime_type'] =~ /^image\/(png|gif|jpg|jpeg)/
357
+ begin
358
+ @builder.span(class: 'image') do
359
+ @builder.a(href: '', style: 'text-decoration: none;', onclick: "img=document.getElementById('#{id}');img.style.display = (img.style.display == 'none' ? 'block' : 'none');return false") do
360
+ @builder.span(style: "color: #{COLOR[:output]}; font-weight: bold; border-bottom: 1px solid #{COLOR[:output]};") do
361
+ @builder << "Screenshot ##{@embedding_count}"
362
+ end
363
+ end
364
+ @builder << '<br/>'
365
+ options[:compress_images] ? build_unique_image(embedding, id) : build_image(embedding, id)
366
+ end
367
+ rescue => e
368
+ puts 'Image embedding failed!'
369
+ puts [e.class, e.message, e.backtrace[0..10].join("\n")].join("\n")
370
+ end
371
+ elsif embedding['mime_type'] =~ /^text\/plain/
372
+ begin
373
+ if src.include?('|||')
374
+ title, link = src.split('|||')
375
+ @builder.span(class: 'link') do
376
+ @builder.a(id: id, style: 'text-decoration: none;', href: link, title: title) do
377
+ @builder.span(style: "color: #{COLOR[:output]}; font-weight: bold; border-bottom: 1px solid #{COLOR[:output]};") do
378
+ @builder << title
379
+ end
380
+ end
381
+ @builder << '<br/>'
382
+ end
383
+ else
384
+ @builder.span(class: 'info') do
385
+ @builder << src
386
+ @builder << '<br/>'
387
+ end
388
+ end
389
+ rescue => e
390
+ puts('Link embedding skipped!')
391
+ puts [e.class, e.message, e.backtrace[0..10].join("\n")].join("\n")
392
+ end
393
+ end
394
+ @embedding_count += 1
395
+ end if embeddings.is_a?(Array)
396
+ end
397
+
398
+ def build_unique_image(image, id)
399
+ @images ||= []
400
+ index = @images.find_index image
401
+ if index
402
+ klass = "image_#{index}"
403
+ else
404
+ @images << image
405
+ klass = "image_#{@images.size - 1}"
406
+ @builder.style(type: 'text/css') do
407
+ begin
408
+ src = Base64.decode64(image['data'])
409
+ src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
410
+ @builder << "img.#{klass} {content: url(#{src});}"
411
+ rescue
412
+ src = image['data']
413
+ src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
414
+ @builder << "img.#{klass} {content: url(#{src});}"
415
+ end
416
+ end
417
+ end
418
+ @builder << %{<img id='#{id}' class='#{klass}' style='display: none; border: 1px solid #{COLOR[:output]};' />}
419
+ end
420
+
421
+ def build_image(image, id)
422
+ begin
423
+ src = Base64.decode64(image['data'])
424
+ src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
425
+ @builder << %{<img id='#{id}' style='display: none; border: 1px solid #{COLOR[:output]};' src='#{src}'/>}
426
+ rescue
427
+ src = image['data']
428
+ src = 'data:' + image['mime_type'] + ';base64,' + src unless src =~ /^data:image\/(png|gif|jpg|jpeg);base64,/
429
+ @builder << %{<img id='#{id}' style='display: none; border: 1px solid #{COLOR[:output]};' src='#{src}'/>}
430
+ end
431
+ end
432
+
433
+ def build_error_list(scenario)
434
+ scenario['before'].each do |before|
435
+ next unless before['status'] == 'failed'
436
+ @builder.li do
437
+ error = before['result']['error_message'].split("\n")
438
+ @builder.span(style: "color:#{COLOR[:failed]}") do
439
+ error[0..-2].each do |line|
440
+ @builder << line + '<br/>'
441
+ end
442
+ end
443
+ @builder << "<strong>Hook: </strong>#{error[-1]} <br/>"
444
+ @builder << "<strong>Scenario: </strong>#{scenario['name']} <br/><hr/>"
445
+ end
446
+ end
447
+ scenario['steps'].each do |step|
448
+ step['after'].each do |after|
449
+ next unless after['status'] == 'failed'
450
+ @builder.li do
451
+ error = after['result']['error_message'].split("\n")
452
+ @builder.span(style: "color:#{COLOR[:failed]}") do
453
+ (scenario['keyword'] == 'Scenario Outline' ? error[0..-8] : error[0..-5]).each do |line|
454
+ @builder << line + '<br/>'
455
+ end
456
+ end
457
+ @builder << "<strong>Hook: </strong>#{scenario['keyword'] == 'Scenario Outline' ? error[-7] : error[-4]} <br/>"
458
+ @builder << "<strong>FF: </strong>#{error[-2]} <br/><hr/>"
459
+ end
460
+ end if step['after']
461
+ next unless step['status'] == 'failed' && step['result']['error_message']
462
+ @builder.li do
463
+ error = step['result']['error_message'].split("\n")
464
+ @builder.span(style: "color:#{COLOR[:failed]}") do
465
+ error[0..-3].each do |line|
466
+ @builder << line + '<br/>'
467
+ end
468
+ end
469
+ @builder << "<strong>SD: </strong>#{error[-2]} <br/>"
470
+ @builder << "<strong>FF: </strong>#{error[-1]} <br/><hr/>"
471
+ end
472
+ end
473
+ scenario['after'].each do |after|
474
+ next unless after['status'] == 'failed'
475
+ @builder.li do
476
+ error = after['result']['error_message'].split("\n")
477
+ @builder.span(style: "color:#{COLOR[:failed]}") do
478
+ error[0..-2].each do |line|
479
+ @builder << line + '<br/>'
480
+ end
481
+ end
482
+ @builder << "<strong>Hook: </strong>#{error[-1]} <br/>"
483
+ @builder << "<strong>Scenario: </strong>#{scenario['name']} <br/><hr/>"
484
+ end
485
+ end
486
+ end
487
+
488
+ def features(files)
489
+ files.each_with_object([]) {|file, features|
490
+ data = File.read(file)
491
+ next if data.empty?
492
+ features << JSON.parse(data)
493
+ }.flatten.group_by {|feature|
494
+ feature['uri']+feature['id']+feature['line'].to_s
495
+ }.values.each_with_object([]) {|group, features|
496
+ features << group.first.except('elements').merge('elements' => group.map {|feature| feature['elements']}.flatten)
497
+ }.sort_by! {|feature| feature['name']}.each {|feature|
498
+ if feature['elements'][0]['type'] == 'background'
499
+ (0..feature['elements'].size-1).step(2) do |i|
500
+ feature['elements'][i]['steps'] ||= []
501
+ feature['elements'][i]['steps'].each {|step| step['name']+=(' ('+feature['elements'][i]['keyword']+')')}
502
+ feature['elements'][i+1]['steps'] = feature['elements'][i]['steps'] + feature['elements'][i+1]['steps']
503
+ feature['elements'][i+1]['before'] = feature['elements'][i]['before'] if feature['elements'][i]['before']
504
+ end
505
+ feature['elements'].reject! {|element| element['type'] == 'background'}
506
+ end
507
+ feature['elements'].each {|scenario|
508
+ scenario['before'] ||= []
509
+ scenario['before'].each {|before|
510
+ before['result']['duration'] ||= 0
511
+ before.merge! 'status' => before['result']['status'], 'duration' => before['result']['duration']
512
+ }
513
+ scenario['steps'] ||= []
514
+ scenario['steps'].each {|step|
515
+ step['result']['duration'] ||= 0
516
+ duration = step['result']['duration']
517
+ status = step['result']['status']
518
+ step['after'].each {|after|
519
+ after['result']['duration'] ||= 0
520
+ duration += after['result']['duration']
521
+ status = 'failed' if after['result']['status'] == 'failed'
522
+ after.merge! 'status' => after['result']['status'], 'duration' => after['result']['duration']
523
+ } if step['after']
524
+ step.merge! 'status' => status, 'duration' => duration
525
+ }
526
+ scenario['after'] ||= []
527
+ scenario['after'].each {|after|
528
+ after['result']['duration'] ||= 0
529
+ after.merge! 'status' => after['result']['status'], 'duration' => after['result']['duration']
530
+ }
531
+ scenario.merge! 'status' => scenario_status(scenario), 'duration' => total_time(scenario['before']) + total_time(scenario['steps']) + total_time(scenario['after'])
532
+ }
533
+ feature.merge! 'status' => feature_status(feature), 'duration' => total_time(feature['elements'])
534
+ }
535
+ end
536
+
537
+ def feature_status(feature)
538
+ feature_status = 'working'
539
+ feature['elements'].each do |scenario|
540
+ status = scenario['status']
541
+ return 'broken' if status == 'failed'
542
+ feature_status = 'incomplete' if %w(undefined pending).include?(status)
543
+ end
544
+ feature_status
545
+ end
546
+
547
+ def scenarios(features)
548
+ features.map do |feature|
549
+ feature['elements']
550
+ end.flatten
551
+ end
552
+
553
+ def scenario_status(scenario)
554
+ (scenario['before'] + scenario['steps'] + scenario['after']).each do |step|
555
+ status = step['status']
556
+ return status unless status == 'passed'
557
+ end
558
+ 'passed'
559
+ end
560
+
561
+ def steps(scenarios)
562
+ scenarios.map do |scenario|
563
+ scenario['steps']
564
+ end.flatten
565
+ end
566
+
567
+ def tags(scenarios)
568
+ scenarios.map do |scenario|
569
+ scenario['tags'] ? scenario['tags'].map {|t| t['name']} : []
570
+ end.flatten.uniq
571
+ end
572
+
573
+ def files(path)
574
+ files = if path.is_a? String
575
+ (path =~ /\.json$/) ? [path] : Dir.glob("#{path}/*.json")
576
+ elsif path.nil?
577
+ Dir.glob('*.json')
578
+ elsif path.is_a? Array
579
+ path.map do |file|
580
+ (file =~ /\.json$/) ? file : Dir.glob("#{file}/*.json")
581
+ end.flatten
582
+ else
583
+ raise 'InvalidInput'
584
+ end
585
+ raise 'InvalidOrNoInputFile' if files.empty?
586
+ files.uniq
587
+ end
588
+
589
+ def data(all_data)
590
+ all_data.group_by {|db| db['status']}.map do |data|
591
+ {name: data[0],
592
+ count: data[1].size,
593
+ color: COLOR[data[0].to_sym]}
594
+ end
595
+ end
596
+
597
+ def total_time(data)
598
+ total_time = 0
599
+ data.each {|item| total_time += item['duration']}
600
+ total_time
601
+ end
602
+
603
+ def duration(seconds)
604
+ seconds = seconds.to_f/1000000000
605
+ m, s = seconds.divmod(60)
606
+ "#{m}m #{'%.3f' % s}s"
607
+ end
608
+
609
+ def pie_chart_js(id, title, data)
610
+ data = data.each_with_object('') do |h, s|
611
+ s << "{name: '#{h[:name].capitalize}'"
612
+ s << ",y: #{h[:count]}"
613
+ s << ',sliced: true' if h[:sliced]
614
+ s << ',selected: true' if h[:selected]
615
+ s << ",color: '#{h[:color]}'" if h[:color]
616
+ s << '},'
617
+ end.chop
618
+ "$(function (){$('##{id}').highcharts({credits: {enabled: false}, chart: {type: 'pie',
619
+ options3d: {enabled: true, alpha: 45, beta: 0}}, title: {text: '#{title}'},
620
+ tooltip: {pointFormat: 'Count: <b>{point.y}</b><br/>Percentage: <b>{point.percentage:.1f}%</b>'},
621
+ plotOptions: {pie: {allowPointSelect: true, cursor: 'pointer', depth: 35, dataLabels: {enabled: true,
622
+ format: '{point.name}'}}}, series: [{type: 'pie', name: 'Results', data: [#{data}]}]});});"
623
+ end
624
+
625
+ def donut_js(id, title, data)
626
+ data = data.each_with_object('') do |h, s|
627
+ s << "{name: '#{h[:name].capitalize}'"
628
+ s << ",y: #{h[:count]}"
629
+ s << ',sliced: true' if h[:sliced]
630
+ s << ',selected: true' if h[:selected]
631
+ s << ",color: '#{h[:color]}'" if h[:color]
632
+ s << '},'
633
+ end.chop
634
+ "$(function (){$('##{id}').highcharts({credits: {enabled: false},
635
+ chart: {plotBackgroundColor: null, plotBorderWidth: 0, plotShadow: false, width: $(document).width()-80},
636
+ title: {text: '#{title}', align: 'center', verticalAlign: 'middle', y: 40},
637
+ tooltip: {pointFormat: 'Count: <b>{point.y}</b><br/>Percentage: <b>{point.percentage:.1f}%</b>'},
638
+ plotOptions: {pie: {dataLabels: {enabled: true, distance: -50,
639
+ style: {fontWeight: 'bold', color: 'white', textShadow: '0px 1px 2px black'}},
640
+ startAngle: -90, endAngle: 90, center: ['50%', '75%']}},
641
+ series: [{type: 'pie', innerSize: '50%', name: 'Results', data: [#{data}]}]});});"
642
+ end
643
+
644
+ end
645
+
646
+ end
@@ -0,0 +1,10 @@
1
+ class Hash
2
+ def except(*keys)
3
+ dup.except!(*keys)
4
+ end
5
+
6
+ def except!(*keys)
7
+ keys.each { |key| delete(key) }
8
+ self
9
+ end
10
+ end