report_builder 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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