report_builder 1.4 → 1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +35 -1
- data/README.md +68 -53
- data/bin/report_builder +18 -16
- data/lib/report_builder.rb +388 -26
- data/lib/report_builder/builder.rb +103 -79
- data/report_builder.gemspec +4 -4
- data/template/error.erb +6 -6
- data/template/feature.erb +7 -9
- data/template/footer.erb +30 -4
- data/template/group_errors.erb +1 -1
- data/template/group_features.erb +2 -2
- data/template/group_report.erb +5 -5
- data/template/group_summary.erb +1 -1
- data/template/head.erb +2 -2
- data/template/header.erb +9 -9
- data/template/hook.erb +4 -1
- data/template/report.erb +5 -5
- data/template/step.erb +4 -0
- data/template/summary.erb +7 -1
- data/template/summary_row.erb +12 -2
- metadata +17 -18
@@ -7,30 +7,26 @@ require 'ostruct'
|
|
7
7
|
require 'report_builder/core-ext/hash'
|
8
8
|
|
9
9
|
module ReportBuilder
|
10
|
+
|
10
11
|
##
|
11
12
|
# ReportBuilder Main class
|
12
13
|
#
|
13
14
|
class Builder
|
14
15
|
|
15
|
-
attr_accessor :options
|
16
|
-
|
17
16
|
##
|
18
17
|
# ReportBuilder Main method
|
19
18
|
#
|
20
|
-
def build_report
|
21
|
-
options =
|
22
|
-
options.merge! opts if opts.is_a? Hash
|
19
|
+
def build_report
|
20
|
+
options = ReportBuilder.options
|
23
21
|
|
24
|
-
fail 'Error:: Invalid report_types. Use: [:json, :html]' unless options[:report_types].is_a? Array
|
25
|
-
options[:report_types].map!(&:to_s).map!(&:upcase)
|
26
|
-
|
27
|
-
options[:input_path] ||= options[:json_path] || Dir.pwd
|
28
22
|
groups = get_groups options[:input_path]
|
29
23
|
|
30
24
|
json_report_path = options[:json_report_path] || options[:report_path]
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
if options[:report_types].include? 'JSON'
|
26
|
+
File.open(json_report_path + '.json', 'w') do |file|
|
27
|
+
file.write JSON.pretty_generate(groups.size > 1 ? groups : groups.first['features'])
|
28
|
+
end
|
29
|
+
end
|
34
30
|
|
35
31
|
if options[:additional_css] and Pathname.new(options[:additional_css]).file?
|
36
32
|
options[:additional_css] = File.read(options[:additional_css])
|
@@ -41,42 +37,29 @@ module ReportBuilder
|
|
41
37
|
end
|
42
38
|
|
43
39
|
html_report_path = options[:html_report_path] || options[:report_path]
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
if options[:report_types].include? 'HTML'
|
41
|
+
File.open(html_report_path + '.html', 'w') do |file|
|
42
|
+
file.write get(groups.size > 1 ? 'group_report' : 'report').result(binding)
|
43
|
+
end
|
44
|
+
end
|
47
45
|
|
48
46
|
retry_report_path = options[:retry_report_path] || options[:report_path]
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
if options[:report_types].include? 'RETRY'
|
48
|
+
File.open(retry_report_path + '.retry', 'w') do |file|
|
49
|
+
groups.each do |group|
|
50
|
+
group['features'].each do |feature|
|
51
|
+
if feature['status'] == 'broken'
|
52
|
+
feature['elements'].each do |scenario|
|
53
|
+
file.puts "#{feature['uri']}:#{scenario['line']}" if scenario['status'] == 'failed'
|
54
|
+
end
|
55
|
+
end
|
54
56
|
end
|
55
57
|
end
|
56
58
|
end
|
57
|
-
end
|
59
|
+
end
|
58
60
|
[json_report_path, html_report_path, retry_report_path]
|
59
61
|
end
|
60
62
|
|
61
|
-
##
|
62
|
-
# ReportBuilder default configuration
|
63
|
-
#
|
64
|
-
def default_options
|
65
|
-
OpenStruct.new(json_path: nil,
|
66
|
-
input_path: nil,
|
67
|
-
report_types: [:html],
|
68
|
-
report_title: 'Test Results',
|
69
|
-
include_images: true,
|
70
|
-
additional_info: {},
|
71
|
-
report_path: 'test_report',
|
72
|
-
json_report_path: nil,
|
73
|
-
html_report_path: nil,
|
74
|
-
retry_report_path: nil,
|
75
|
-
additional_css: nil,
|
76
|
-
additional_js: nil
|
77
|
-
)
|
78
|
-
end
|
79
|
-
|
80
63
|
private
|
81
64
|
|
82
65
|
def get(template)
|
@@ -90,13 +73,17 @@ module ReportBuilder
|
|
90
73
|
input_path.each do |group_name, group_path|
|
91
74
|
files = get_files group_path
|
92
75
|
puts "Error:: No file(s) found at #{group_path}" if files.empty?
|
93
|
-
groups << {'name' => group_name, 'features' => get_features(files)}
|
76
|
+
groups << {'name' => group_name, 'features' => get_features(files)}
|
94
77
|
end
|
95
|
-
fail 'Error:: Invalid Input File(s). Please provide valid cucumber JSON output file(s)' if groups.empty?
|
96
78
|
else
|
97
79
|
files = get_files input_path
|
98
|
-
|
99
|
-
groups << {'features' => get_features(files)}
|
80
|
+
raise "Error:: No file(s) found at #{input_path}" if files.empty?
|
81
|
+
groups << {'features' => get_features(files)}
|
82
|
+
end
|
83
|
+
groups.each do |this_group|
|
84
|
+
this_group['features'].each do |feature|
|
85
|
+
feature['elements'] = feature['elements'].sort_by { |v| v['line']}
|
86
|
+
end
|
100
87
|
end
|
101
88
|
groups
|
102
89
|
end
|
@@ -126,63 +113,100 @@ module ReportBuilder
|
|
126
113
|
end
|
127
114
|
|
128
115
|
def get_features(files)
|
129
|
-
files.each_with_object([])
|
116
|
+
files.each_with_object([]) do |file, features|
|
130
117
|
data = File.read(file)
|
131
118
|
next if data.empty?
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
119
|
+
begin
|
120
|
+
features << JSON.parse(data)
|
121
|
+
rescue StandardError
|
122
|
+
puts 'Warning:: Invalid Input File ' + file
|
123
|
+
puts 'JSON Error:: ' + $!.to_s
|
124
|
+
next
|
125
|
+
end
|
126
|
+
end.flatten.group_by do |feature|
|
127
|
+
feature['uri'] + feature['id'] + feature['line'].to_s
|
128
|
+
end.values.each_with_object([]) do |group, features|
|
136
129
|
features << group.first.except('elements').merge('elements' => group.map {|feature| feature['elements']}.flatten)
|
137
|
-
|
130
|
+
end.sort_by! do |feature|
|
131
|
+
feature['name']
|
132
|
+
end.each do |feature|
|
133
|
+
feature['name'] = ERB::Util.html_escape feature['name']
|
138
134
|
if feature['elements'][0]['type'] == 'background'
|
139
135
|
(0..feature['elements'].size-1).step(2) do |i|
|
140
136
|
feature['elements'][i]['steps'] ||= []
|
141
|
-
feature['elements'][i]['steps'].each {|step| step['name']+=(' ('+feature['elements'][i]['keyword']+')')}
|
137
|
+
feature['elements'][i]['steps'].each {|step| step['name'] += (' (' + feature['elements'][i]['keyword'] + ')')}
|
142
138
|
if feature['elements'][i+1]
|
143
139
|
feature['elements'][i+1]['steps'] = feature['elements'][i]['steps'] + feature['elements'][i+1]['steps']
|
144
140
|
feature['elements'][i+1]['before'] = feature['elements'][i]['before'] if feature['elements'][i]['before']
|
145
141
|
end
|
146
142
|
end
|
147
|
-
feature['elements'].reject!
|
143
|
+
feature['elements'].reject! do |element|
|
144
|
+
element['type'] == 'background'
|
145
|
+
end
|
148
146
|
end
|
149
|
-
feature['elements'].each
|
147
|
+
feature['elements'].each do |scenario|
|
148
|
+
scenario['name'] = ERB::Util.html_escape scenario['name']
|
150
149
|
scenario['before'] ||= []
|
151
|
-
scenario['before'].each
|
150
|
+
scenario['before'].each do |before|
|
152
151
|
before['result']['duration'] ||= 0
|
152
|
+
if before['embeddings']
|
153
|
+
before['embeddings'].map! do |embedding|
|
154
|
+
decode_embedding(embedding)
|
155
|
+
end
|
156
|
+
end
|
153
157
|
before.merge! 'status' => before['result']['status'], 'duration' => before['result']['duration']
|
154
|
-
|
158
|
+
end
|
155
159
|
scenario['steps'] ||= []
|
156
|
-
scenario['steps'].each
|
160
|
+
scenario['steps'].each do |step|
|
161
|
+
step['name'] = ERB::Util.html_escape step['name']
|
157
162
|
step['result']['duration'] ||= 0
|
158
163
|
duration = step['result']['duration']
|
159
164
|
status = step['result']['status']
|
160
|
-
step['after']
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
+
if step['after']
|
166
|
+
step['after'].each do |after|
|
167
|
+
after['result']['duration'] ||= 0
|
168
|
+
duration += after['result']['duration']
|
169
|
+
status = 'failed' if after['result']['status'] == 'failed'
|
170
|
+
if after['embeddings']
|
171
|
+
after['embeddings'].map! do |embedding|
|
172
|
+
decode_embedding(embedding)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
after.merge! 'status' => after['result']['status'], 'duration' => after['result']['duration']
|
176
|
+
end
|
177
|
+
end
|
178
|
+
if step['embeddings']
|
179
|
+
step['embeddings'].map! do |embedding|
|
165
180
|
decode_embedding(embedding)
|
166
|
-
|
167
|
-
|
168
|
-
} if step['after']
|
169
|
-
step['embeddings'].map! { |embedding|
|
170
|
-
decode_embedding(embedding)
|
171
|
-
} if step['embeddings']
|
181
|
+
end
|
182
|
+
end
|
172
183
|
step.merge! 'status' => status, 'duration' => duration
|
173
|
-
|
184
|
+
end
|
174
185
|
scenario['after'] ||= []
|
175
|
-
scenario['after'].each
|
186
|
+
scenario['after'].each do |after|
|
176
187
|
after['result']['duration'] ||= 0
|
177
|
-
after['embeddings']
|
178
|
-
|
179
|
-
|
188
|
+
if after['embeddings']
|
189
|
+
after['embeddings'].map! do |embedding|
|
190
|
+
decode_embedding(embedding)
|
191
|
+
end
|
192
|
+
end
|
180
193
|
after.merge! 'status' => after['result']['status'], 'duration' => after['result']['duration']
|
181
|
-
|
194
|
+
end
|
182
195
|
scenario.merge! 'status' => scenario_status(scenario), 'duration' => total_time(scenario['before']) + total_time(scenario['steps']) + total_time(scenario['after'])
|
183
|
-
|
196
|
+
end
|
197
|
+
feature['elements'] = feature['elements'].group_by do |scenario|
|
198
|
+
scenario['id'] + ':' + scenario['line'].to_s
|
199
|
+
end.values.map do |scenario_group|
|
200
|
+
the_scenario = scenario_group.find do |scenario|
|
201
|
+
scenario['status'] == 'passed'
|
202
|
+
end || scenario_group.last
|
203
|
+
if scenario_group.size > 1
|
204
|
+
the_scenario['name'] += " (x#{scenario_group.size})"
|
205
|
+
end
|
206
|
+
the_scenario
|
207
|
+
end
|
184
208
|
feature.merge! 'status' => feature_status(feature), 'duration' => total_time(feature['elements'])
|
185
|
-
|
209
|
+
end
|
186
210
|
end
|
187
211
|
|
188
212
|
def feature_status(feature)
|
@@ -204,9 +228,9 @@ module ReportBuilder
|
|
204
228
|
end
|
205
229
|
|
206
230
|
def decode_image(data)
|
207
|
-
base64 =
|
231
|
+
base64 = %r{^([A-Za-z0-9+\/]{4})*([A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)$}
|
208
232
|
if data =~ base64
|
209
|
-
data_base64 = Base64.
|
233
|
+
data_base64 = Base64.urlsafe_decode64(data).gsub(%r{^data:image\/(png|gif|jpg|jpeg)\;base64,}, '') rescue data
|
210
234
|
if data_base64 =~ base64
|
211
235
|
data_base64
|
212
236
|
else
|
@@ -218,13 +242,13 @@ module ReportBuilder
|
|
218
242
|
end
|
219
243
|
|
220
244
|
def decode_text(data)
|
221
|
-
Base64.
|
245
|
+
Base64.urlsafe_decode64 data rescue ''
|
222
246
|
end
|
223
247
|
|
224
248
|
def decode_embedding(embedding)
|
225
249
|
if embedding['mime_type'] =~ /^image\/(png|gif|jpg|jpeg)/
|
226
250
|
embedding['data'] = decode_image(embedding['data'])
|
227
|
-
elsif embedding['mime_type'] =~ /^text\/plain/
|
251
|
+
elsif embedding['mime_type'] =~ /^text\/(plain|html)/
|
228
252
|
embedding['data'] = decode_text(embedding['data'])
|
229
253
|
end
|
230
254
|
embedding
|
data/report_builder.gemspec
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'report_builder'
|
3
|
-
s.version = '1.
|
3
|
+
s.version = '1.9'
|
4
4
|
s.bindir = 'bin'
|
5
5
|
s.summary = 'ReportBuilder'
|
6
6
|
s.description = 'Ruby gem to merge Cucumber JSON reports and build mobile-friendly HTML Test Report, JSON report and retry file.'
|
7
7
|
s.post_install_message = 'Happy reporting!'
|
8
8
|
s.authors = ['Rajat Thareja']
|
9
9
|
s.email = 'rajat.thareja.1990@gmail.com'
|
10
|
-
s.homepage = '
|
10
|
+
s.homepage = 'https://reportbuilder.rajatthareja.com'
|
11
11
|
s.license = 'MIT'
|
12
|
-
s.required_ruby_version = '>=
|
12
|
+
s.required_ruby_version = '>= 2.0.0'
|
13
13
|
s.requirements << 'Cucumber >= 2.1.0 test results in JSON format'
|
14
14
|
|
15
15
|
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(sample/|css/|js/|pkg/|testing/|coverage/|CNAME|.gitignore|appveyor.yml|.travis.yml|_config.yml|Gemfile|Rakefile|rb.ico)}) }
|
16
16
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
17
|
s.test_files = s.files.grep(%r{^(testing)/})
|
18
18
|
|
19
|
-
s.add_runtime_dependency 'json', '>=
|
19
|
+
s.add_runtime_dependency 'json', '>= 2.3.0'
|
20
20
|
|
21
21
|
s.add_development_dependency 'rake', '< 13.0'
|
22
22
|
s.add_development_dependency 'rspec', '< 4.0'
|
data/template/error.erb
CHANGED
@@ -3,19 +3,19 @@
|
|
3
3
|
<i class="material-icons">bug_report</i><%= error %>
|
4
4
|
</div>
|
5
5
|
|
6
|
-
<div class="collapsible-body
|
7
|
-
<
|
6
|
+
<div class="collapsible-body <%= options[:color] %> lighten-4">
|
7
|
+
<div class="collection failedScenarioList">
|
8
8
|
<% features.each_with_index do |feature, f| %>
|
9
9
|
<% feature['elements'].each_with_index do |scenario, s| %>
|
10
10
|
<% if scenario['error'] %>
|
11
11
|
<% if scenario['error'] == error %>
|
12
|
-
<
|
13
|
-
<i class="material-icons">highlight_off</i> 
|
14
|
-
</
|
12
|
+
<a class="collection-item failedScenario <%= options[:color] %> lighten-5 red-text modal-trigger waves-effect waves-light" href="#<%= "#{gid}f#{f}s#{s}" %>">
|
13
|
+
<i class="material-icons">highlight_off</i> <%= scenario['name'] %>
|
14
|
+
</a>
|
15
15
|
<% end %>
|
16
16
|
<% end %>
|
17
17
|
<% end %>
|
18
18
|
<% end %>
|
19
|
-
</
|
19
|
+
</div>
|
20
20
|
</div>
|
21
21
|
</li>
|
data/template/feature.erb
CHANGED
@@ -1,18 +1,16 @@
|
|
1
1
|
<li class="feature <%= feature['status'] %>">
|
2
|
-
<div class="collapsible-header
|
2
|
+
<div class="collapsible-header <%= options[:color] %> lighten-1 waves-effect waves-light">
|
3
3
|
<i class="material-icons">featured_play_list</i>
|
4
4
|
<b><%= feature['keyword'] %></b> <%= feature['name'] %> (<%= duration(feature['duration']) %>)
|
5
5
|
</div>
|
6
6
|
|
7
|
-
<div class="collapsible-body
|
8
|
-
<
|
7
|
+
<div class="collapsible-body <%= options[:color] %> lighten-4">
|
8
|
+
<div class="collection scenarioList <%= options[:color] %>">
|
9
9
|
<% feature['elements'].each_with_index do |scenario, s| %>
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
</a>
|
14
|
-
</li>
|
10
|
+
<a class="collection-item scenario modal-trigger waves-effect waves-light white-text <%= scenario['status'] %>" href="#<%= "#{fid}s#{s}" %>">
|
11
|
+
<b><%= scenario['keyword'] %></b> <%= scenario['name'] %> (<%= duration(scenario['duration']) %>)
|
12
|
+
</a>
|
15
13
|
<% end %>
|
16
|
-
</
|
14
|
+
</div>
|
17
15
|
</div>
|
18
16
|
</li>
|
data/template/footer.erb
CHANGED
@@ -1,13 +1,39 @@
|
|
1
|
-
<footer class="page-footer
|
2
|
-
<div class="footer-copyright
|
1
|
+
<footer class="page-footer <%= options[:color] %> lighten-4">
|
2
|
+
<div class="footer-copyright <%= options[:color] %> lighten-1">
|
3
3
|
<div class="container">
|
4
4
|
Happy Reporting!
|
5
|
-
<a class="white-text text-lighten-4 right" href="
|
5
|
+
<a class="white-text text-lighten-4 right" href="https://reportbuilder.rajatthareja.com">Generated by Report Builder</a>
|
6
6
|
</div>
|
7
7
|
</div>
|
8
8
|
</footer>
|
9
9
|
|
10
|
-
<script type="text/javascript" src="https://cdn.rawgit.com/rajatthareja/ReportBuilder/v1.
|
10
|
+
<script type="text/javascript" src="https://cdn.rawgit.com/rajatthareja/ReportBuilder/v1.9/js/report.builder.min.js"></script>
|
11
|
+
|
12
|
+
<% if options[:voice_commands] %>
|
13
|
+
<script src="//cdnjs.cloudflare.com/ajax/libs/annyang/2.6.0/annyang.min.js"></script>
|
14
|
+
<script>
|
15
|
+
var commands = {
|
16
|
+
'show :tab': function(tab) {
|
17
|
+
if (tab === "overview") {
|
18
|
+
$("a[data-tooltip='Results Overview']").click();
|
19
|
+
} else if (tab === "features" || tab === "feature") {
|
20
|
+
$("a[data-tooltip='Scenarios by Features']").click();
|
21
|
+
} else if (tab === "summary") {
|
22
|
+
$("a[data-tooltip='Scenarios Summary Table']").click();
|
23
|
+
} else if (tab === "errors" || tab === "error" || tab === "failed" || tab === "bugs") {
|
24
|
+
$("a[data-tooltip='Failed Scenarios']").click();
|
25
|
+
}
|
26
|
+
},
|
27
|
+
'search *search': function (search) {
|
28
|
+
$("a[data-tooltip='Scenarios Summary Table']").click();
|
29
|
+
$("input[type='search']").val(search);
|
30
|
+
$('table#summaryTable').DataTable().search(search).draw();
|
31
|
+
}
|
32
|
+
};
|
33
|
+
annyang.addCommands(commands);
|
34
|
+
annyang.start();
|
35
|
+
</script>
|
36
|
+
<% end %>
|
11
37
|
|
12
38
|
<% if options[:additional_js] %>
|
13
39
|
<% if options[:additional_js] =~ /^http(|s):\/\/.*\.js$/ %>
|
data/template/group_errors.erb
CHANGED
data/template/group_features.erb
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
<% features = group['features'] %>
|
6
6
|
<li class="group">
|
7
7
|
|
8
|
-
<div class="collapsible-header
|
8
|
+
<div class="collapsible-header <%= options[:color] %> lighten-1 waves-effect waves-light">
|
9
9
|
<i class="material-icons">group_work</i>
|
10
10
|
<b><%= group['name'] %></b> (<%= duration(total_time(features)) %>)
|
11
11
|
</div>
|
12
12
|
|
13
|
-
<div class="collapsible-body
|
13
|
+
<div class="collapsible-body <%= options[:color] %> lighten-4">
|
14
14
|
<%= get('features').result(binding) %>
|
15
15
|
</div>
|
16
16
|
</li>
|
data/template/group_report.erb
CHANGED
@@ -12,21 +12,21 @@
|
|
12
12
|
<%= get('scenarios').result(binding) %>
|
13
13
|
<% end %>
|
14
14
|
|
15
|
-
<main class="
|
15
|
+
<main class="<%= options[:color] %> lighten-5">
|
16
16
|
<div class="row">
|
17
|
-
<div id="overview" class="col s12
|
17
|
+
<div id="overview" class="col s12 <%= options[:color] %> lighten-5">
|
18
18
|
<%= get('group_overview').result(binding) %>
|
19
19
|
</div>
|
20
20
|
|
21
|
-
<div id="features" class="col s12
|
21
|
+
<div id="features" class="col s12 <%= options[:color] %> lighten-5 white-text">
|
22
22
|
<%= get('group_features').result(binding) %>
|
23
23
|
</div>
|
24
24
|
|
25
|
-
<div id="summary" class="col s12
|
25
|
+
<div id="summary" class="col s12 <%= options[:color] %> lighten-5">
|
26
26
|
<%= get('group_summary').result(binding) %>
|
27
27
|
</div>
|
28
28
|
|
29
|
-
<div id="errors" class="col s12
|
29
|
+
<div id="errors" class="col s12 <%= options[:color] %> lighten-5">
|
30
30
|
<%= get('group_errors').result(binding) %>
|
31
31
|
</div>
|
32
32
|
</div>
|