report_builder 1.4 → 1.9
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.
- 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>
|