rspec-tracer 0.3.0 → 0.6.1
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 +4 -4
- data/CHANGELOG.md +34 -2
- data/README.md +146 -12
- data/lib/rspec_tracer/cache.rb +27 -3
- data/lib/rspec_tracer/configuration.rb +6 -0
- data/lib/rspec_tracer/defaults.rb +7 -1
- data/lib/rspec_tracer/html_reporter/reporter.rb +28 -6
- data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +38 -0
- data/lib/rspec_tracer/html_reporter/views/layout.erb +3 -0
- data/lib/rspec_tracer/remote_cache/cache.rb +187 -0
- data/lib/rspec_tracer/remote_cache/git.rb +113 -0
- data/lib/rspec_tracer/reporter.rb +40 -11
- data/lib/rspec_tracer/rspec_runner.rb +6 -1
- data/lib/rspec_tracer/runner.rb +84 -34
- data/lib/rspec_tracer/time_formatter.rb +55 -0
- data/lib/rspec_tracer/version.rb +1 -1
- data/lib/rspec_tracer.rb +30 -7
- metadata +7 -3
@@ -0,0 +1,38 @@
|
|
1
|
+
<div class="report_container" id="<%= title_id %>">
|
2
|
+
<h2>
|
3
|
+
<span class="group_name"><%= title %></span>
|
4
|
+
(
|
5
|
+
<span class="blue">
|
6
|
+
<strong><%= flaky_examples.count %></strong>
|
7
|
+
</span> examples
|
8
|
+
)
|
9
|
+
</h2>
|
10
|
+
|
11
|
+
<a name="<%= title_id %>"></a>
|
12
|
+
|
13
|
+
<div class="report-table--responsive">
|
14
|
+
<table class="report-table">
|
15
|
+
<thead>
|
16
|
+
<tr>
|
17
|
+
<th>ID</th>
|
18
|
+
<th>Description</th>
|
19
|
+
<th>Location</th>
|
20
|
+
<th>Result</th>
|
21
|
+
<th>Run At</th>
|
22
|
+
</tr>
|
23
|
+
</thead>
|
24
|
+
|
25
|
+
<tbody>
|
26
|
+
<% flaky_examples.each do |example| %>
|
27
|
+
<tr>
|
28
|
+
<td><%= example[:id] %></td>
|
29
|
+
<td><%= example[:description] %></td>
|
30
|
+
<td><%= example[:location] %></td>
|
31
|
+
<td><strong class="<%= example_result_css_class(example[:result]) %>"><%= example[:result] %></strong></td>
|
32
|
+
<td width="8%"><%= example[:last_run] %></td>
|
33
|
+
</tr>
|
34
|
+
<% end %>
|
35
|
+
</tbody>
|
36
|
+
</table>
|
37
|
+
</div>
|
38
|
+
</div>
|
@@ -19,6 +19,9 @@
|
|
19
19
|
|
20
20
|
<div id="content">
|
21
21
|
<%= formatted_examples('Examples', examples.values) %>
|
22
|
+
<% unless flaky_examples.empty? %>
|
23
|
+
<%= formatted_flaky_examples('Flaky Examples', flaky_examples) %>
|
24
|
+
<% end %>
|
22
25
|
<%= formatted_examples_dependency('Examples Dependency', examples_dependency) %>
|
23
26
|
<%= formatted_files_dependency("Files Dependency", files_dependency) %>
|
24
27
|
</div>
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'git'
|
4
|
+
|
5
|
+
module RSpecTracer
|
6
|
+
module RemoteCache
|
7
|
+
class Cache
|
8
|
+
class CacheDownloadError < StandardError; end
|
9
|
+
|
10
|
+
class CacheUploadError < StandardError; end
|
11
|
+
|
12
|
+
class LocalCacheNotFoundError < StandardError; end
|
13
|
+
|
14
|
+
CACHE_FILES_PER_TEST_SUITE = 8
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@s3_uri = ENV['RSPEC_TRACER_S3_URI']
|
18
|
+
@aws_s3 = if ENV.fetch('LOCAL_AWS', 'false') == 'true'
|
19
|
+
'awslocal'
|
20
|
+
else
|
21
|
+
'aws'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def download
|
26
|
+
if @s3_uri.nil?
|
27
|
+
puts 'S3 URI is not configured'
|
28
|
+
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
prepare_for_download
|
33
|
+
|
34
|
+
if @cache_sha.nil?
|
35
|
+
puts 'Could not find a suitable cache sha to download'
|
36
|
+
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
download_files
|
41
|
+
|
42
|
+
puts "Downloaded cache from #{@download_prefix} to #{@download_path}"
|
43
|
+
rescue StandardError => e
|
44
|
+
puts "Errored: #{e.message}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def upload
|
48
|
+
if @s3_uri.nil?
|
49
|
+
puts 'S3 URI is not configured'
|
50
|
+
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
prepare_for_upload
|
55
|
+
upload_files
|
56
|
+
|
57
|
+
puts "Uploaded cache from #{@upload_path} to #{@upload_prefix}"
|
58
|
+
rescue CacheUploadError => e
|
59
|
+
puts "Errored: #{e.message}"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def prepare_for_download
|
65
|
+
@test_suite_id = ENV['TEST_SUITE_ID']
|
66
|
+
@test_suites = ENV['TEST_SUITES']
|
67
|
+
|
68
|
+
if @test_suite_id.nil? ^ @test_suites.nil?
|
69
|
+
raise(
|
70
|
+
CacheDownloadError,
|
71
|
+
'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
@git = RSpecTracer::RemoteCache::Git.new
|
76
|
+
@git.prepare_for_download
|
77
|
+
|
78
|
+
generate_cached_files_count_and_regex
|
79
|
+
|
80
|
+
@cache_sha = nearest_cache_sha
|
81
|
+
end
|
82
|
+
|
83
|
+
def generate_cached_files_count_and_regex
|
84
|
+
if @test_suites.nil?
|
85
|
+
@last_run_files_count = 1
|
86
|
+
@last_run_files_regex = '/%<ref>s/last_run.json$'
|
87
|
+
@cached_files_count = CACHE_FILES_PER_TEST_SUITE
|
88
|
+
@cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
|
89
|
+
else
|
90
|
+
@test_suites = @test_suites.to_i
|
91
|
+
@test_suites_regex = (1..@test_suites).to_a.join('|')
|
92
|
+
|
93
|
+
@last_run_files_count = @test_suites
|
94
|
+
@last_run_files_regex = "/%<ref>s/(#{@test_suites_regex})/last_run.json$"
|
95
|
+
@cached_files_count = CACHE_FILES_PER_TEST_SUITE * @test_suites.to_i
|
96
|
+
@cached_files_regex = "/%<ref>s/(#{@test_suites_regex})/[0-9a-f]{32}/.+.json$"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def nearest_cache_sha
|
101
|
+
@git.ref_list.detect do |ref|
|
102
|
+
prefix = "#{@s3_uri}/#{ref}/"
|
103
|
+
|
104
|
+
puts "Testing prefix #{prefix}"
|
105
|
+
|
106
|
+
objects = `#{@aws_s3} s3 ls #{prefix} --recursive`.chomp.split("\n")
|
107
|
+
|
108
|
+
last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
|
109
|
+
|
110
|
+
next if objects.count { |object| object.match?(last_run_regex) } != @last_run_files_count
|
111
|
+
|
112
|
+
cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
|
113
|
+
|
114
|
+
objects.count { |object| object.match?(cache_regex) } == @cached_files_count
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def download_files
|
119
|
+
@download_prefix = "#{@s3_uri}/#{@cache_sha}/#{@test_suite_id}/".sub(%r{/+$}, '/')
|
120
|
+
@download_path = RSpecTracer.cache_path
|
121
|
+
|
122
|
+
raise CacheDownloadError, 'Failed to download cache files' unless system(
|
123
|
+
@aws_s3, 's3', 'cp',
|
124
|
+
File.join(@download_prefix, 'last_run.json'),
|
125
|
+
@download_path,
|
126
|
+
out: File::NULL, err: File::NULL
|
127
|
+
)
|
128
|
+
|
129
|
+
@run_id = last_run_id
|
130
|
+
|
131
|
+
return if system(
|
132
|
+
@aws_s3, 's3', 'cp',
|
133
|
+
File.join(@download_prefix, @run_id),
|
134
|
+
File.join(@download_path, @run_id),
|
135
|
+
'--recursive',
|
136
|
+
out: File::NULL, err: File::NULL
|
137
|
+
)
|
138
|
+
|
139
|
+
FileUtils.rm_rf(@download_path)
|
140
|
+
|
141
|
+
raise CacheDownloadError, 'Failed to download cache files'
|
142
|
+
end
|
143
|
+
|
144
|
+
def prepare_for_upload
|
145
|
+
@git = RSpecTracer::RemoteCache::Git.new
|
146
|
+
@test_suite_id = ENV['TEST_SUITE_ID']
|
147
|
+
@upload_prefix = if @test_suite_id.nil?
|
148
|
+
"#{@s3_uri}/#{@git.branch_ref}/"
|
149
|
+
else
|
150
|
+
"#{@s3_uri}/#{@git.branch_ref}/#{@test_suite_id}/"
|
151
|
+
end
|
152
|
+
|
153
|
+
@upload_path = RSpecTracer.cache_path
|
154
|
+
@run_id = last_run_id
|
155
|
+
end
|
156
|
+
|
157
|
+
def upload_files
|
158
|
+
return if system(
|
159
|
+
@aws_s3, 's3', 'cp',
|
160
|
+
File.join(@upload_path, 'last_run.json'),
|
161
|
+
@upload_prefix,
|
162
|
+
out: File::NULL, err: File::NULL
|
163
|
+
) && system(
|
164
|
+
@aws_s3, 's3', 'cp',
|
165
|
+
File.join(@upload_path, @run_id),
|
166
|
+
File.join(@upload_prefix, @run_id),
|
167
|
+
'--recursive',
|
168
|
+
out: File::NULL, err: File::NULL
|
169
|
+
)
|
170
|
+
|
171
|
+
raise CacheUploadError, 'Failed to upload cache files'
|
172
|
+
end
|
173
|
+
|
174
|
+
def last_run_id
|
175
|
+
file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
|
176
|
+
|
177
|
+
return unless File.file?(file_name)
|
178
|
+
|
179
|
+
run_id = JSON.parse(File.read(file_name))['run_id']
|
180
|
+
|
181
|
+
raise LocalCacheNotFoundError, 'Could not find any local cache to upload' if run_id.nil?
|
182
|
+
|
183
|
+
run_id
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpecTracer
|
4
|
+
module RemoteCache
|
5
|
+
class Git
|
6
|
+
class GitOperationError < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :branch_ref, :ref_list
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
fetch_head_ref
|
12
|
+
fetch_branch_ref
|
13
|
+
end
|
14
|
+
|
15
|
+
def prepare_for_download
|
16
|
+
fetch_unreachable_refs
|
17
|
+
fetch_ancestry_refs
|
18
|
+
fetch_ordered_refs
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fetch_head_ref
|
24
|
+
@head_ref = `git rev-parse HEAD`.chomp
|
25
|
+
|
26
|
+
raise GitOperationError, 'Could not find HEAD commit sha' unless $CHILD_STATUS.success?
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch_branch_ref
|
30
|
+
@merged_parents = []
|
31
|
+
@ignored_refs = []
|
32
|
+
|
33
|
+
unless merged?
|
34
|
+
@branch_ref = @head_ref
|
35
|
+
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
@ignored_refs << @head_ref
|
40
|
+
|
41
|
+
fetch_merged_parents
|
42
|
+
fetch_merged_branch_ref
|
43
|
+
end
|
44
|
+
|
45
|
+
def merged?
|
46
|
+
system('git', 'rev-parse', 'HEAD^2', out: File::NULL, err: File::NULL)
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_merged_parents
|
50
|
+
first_parent = `git rev-parse HEAD^1`.chomp
|
51
|
+
@merged_parents << first_parent if $CHILD_STATUS.success?
|
52
|
+
|
53
|
+
second_parent = `git rev-parse HEAD^2`.chomp
|
54
|
+
@merged_parents << second_parent if $CHILD_STATUS.success?
|
55
|
+
|
56
|
+
raise GitOperationError, 'Could not find merged commit parents' if @merged_parents.length != 2
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_merged_branch_ref
|
60
|
+
@origin_head_ref = `git rev-parse origin/HEAD`.chomp
|
61
|
+
@branch_ref = nil
|
62
|
+
|
63
|
+
if @merged_parents.first != @origin_head_ref
|
64
|
+
@branch_ref = @head_ref
|
65
|
+
@ignored_refs = []
|
66
|
+
|
67
|
+
return
|
68
|
+
end
|
69
|
+
|
70
|
+
@branch_ref = @merged_parents.last
|
71
|
+
@ignored_refs = @ignored_refs.to_set | `git rev-list #{@branch_ref}..origin/HEAD`.chomp.split
|
72
|
+
|
73
|
+
raise GitOperationError, 'Could not find ignored refs' unless $CHILD_STATUS.success?
|
74
|
+
end
|
75
|
+
|
76
|
+
def fetch_unreachable_refs
|
77
|
+
command = <<-COMMAND.strip.gsub(/\s+/, ' ')
|
78
|
+
git fsck
|
79
|
+
--no-progress
|
80
|
+
--unreachable
|
81
|
+
--connectivity-only #{@branch_ref}
|
82
|
+
| awk '/commit/ { print $3 }'
|
83
|
+
| head -n 25
|
84
|
+
COMMAND
|
85
|
+
|
86
|
+
@unreachable_refs = `#{command}`.chomp.split
|
87
|
+
|
88
|
+
raise GitOperationError, 'Could not find unreachable refs' unless $CHILD_STATUS.success?
|
89
|
+
end
|
90
|
+
|
91
|
+
def fetch_ancestry_refs
|
92
|
+
@ancestry_refs = `git rev-list --max-count=25 #{@branch_ref}`.chomp.split
|
93
|
+
|
94
|
+
raise GitOperationError, 'Could not find ancestry refs' unless $CHILD_STATUS.success?
|
95
|
+
end
|
96
|
+
|
97
|
+
def fetch_ordered_refs
|
98
|
+
unordered_refs = (@unreachable_refs.to_set | @ancestry_refs) - @ignored_refs
|
99
|
+
|
100
|
+
command = <<-COMMAND.strip.gsub(/\s+/, ' ')
|
101
|
+
git rev-list
|
102
|
+
--topo-order
|
103
|
+
--no-walk=sorted
|
104
|
+
#{unordered_refs.to_a.join(' ')}
|
105
|
+
COMMAND
|
106
|
+
|
107
|
+
@ref_list = `#{command}`.chomp.split
|
108
|
+
|
109
|
+
raise GitOperationError, 'Could not find refs to download cache' unless $CHILD_STATUS.success?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -2,8 +2,9 @@
|
|
2
2
|
|
3
3
|
module RSpecTracer
|
4
4
|
class Reporter
|
5
|
-
attr_reader :all_examples, :
|
6
|
-
:
|
5
|
+
attr_reader :all_examples, :possibly_flaky_examples, :flaky_examples, :pending_examples,
|
6
|
+
:all_files, :modified_files, :deleted_files, :dependency, :reverse_dependency,
|
7
|
+
:examples_coverage, :last_run
|
7
8
|
|
8
9
|
def initialize
|
9
10
|
initialize_examples
|
@@ -21,6 +22,7 @@ module RSpecTracer
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def on_example_passed(example_id, result)
|
25
|
+
@passed_examples << example_id
|
24
26
|
@all_examples[example_id][:execution_result] = formatted_execution_result(result)
|
25
27
|
end
|
26
28
|
|
@@ -44,6 +46,14 @@ module RSpecTracer
|
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
49
|
+
def register_possibly_flaky_example(example_id)
|
50
|
+
@possibly_flaky_examples << example_id
|
51
|
+
end
|
52
|
+
|
53
|
+
def register_flaky_example(example_id)
|
54
|
+
@flaky_examples << example_id
|
55
|
+
end
|
56
|
+
|
47
57
|
def register_failed_example(example_id)
|
48
58
|
@failed_examples << example_id
|
49
59
|
end
|
@@ -52,6 +62,10 @@ module RSpecTracer
|
|
52
62
|
@pending_examples << example_id
|
53
63
|
end
|
54
64
|
|
65
|
+
def example_passed?(example_id)
|
66
|
+
@passed_examples.include?(example_id)
|
67
|
+
end
|
68
|
+
|
55
69
|
def example_skipped?(example_id)
|
56
70
|
@skipped_examples.include?(example_id)
|
57
71
|
end
|
@@ -126,6 +140,8 @@ module RSpecTracer
|
|
126
140
|
end
|
127
141
|
|
128
142
|
def write_reports
|
143
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
144
|
+
|
129
145
|
@run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
|
130
146
|
@cache_dir = File.join(RSpecTracer.cache_path, @run_id)
|
131
147
|
|
@@ -133,6 +149,7 @@ module RSpecTracer
|
|
133
149
|
|
134
150
|
%i[
|
135
151
|
all_examples
|
152
|
+
flaky_examples
|
136
153
|
failed_examples
|
137
154
|
pending_examples
|
138
155
|
all_files
|
@@ -142,13 +159,19 @@ module RSpecTracer
|
|
142
159
|
last_run
|
143
160
|
].each { |report_type| send("write_#{report_type}_report") }
|
144
161
|
|
145
|
-
|
162
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
163
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
164
|
+
|
165
|
+
puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
|
146
166
|
end
|
147
167
|
|
148
168
|
private
|
149
169
|
|
150
170
|
def initialize_examples
|
151
171
|
@all_examples = {}
|
172
|
+
@passed_examples = Set.new
|
173
|
+
@possibly_flaky_examples = Set.new
|
174
|
+
@flaky_examples = Set.new
|
152
175
|
@failed_examples = Set.new
|
153
176
|
@skipped_examples = Set.new
|
154
177
|
@pending_examples = Set.new
|
@@ -206,50 +229,56 @@ module RSpecTracer
|
|
206
229
|
def write_all_examples_report
|
207
230
|
file_name = File.join(@cache_dir, 'all_examples.json')
|
208
231
|
|
209
|
-
File.write(file_name, JSON.
|
232
|
+
File.write(file_name, JSON.generate(@all_examples))
|
233
|
+
end
|
234
|
+
|
235
|
+
def write_flaky_examples_report
|
236
|
+
file_name = File.join(@cache_dir, 'flaky_examples.json')
|
237
|
+
|
238
|
+
File.write(file_name, JSON.generate(@flaky_examples.to_a))
|
210
239
|
end
|
211
240
|
|
212
241
|
def write_failed_examples_report
|
213
242
|
file_name = File.join(@cache_dir, 'failed_examples.json')
|
214
243
|
|
215
|
-
File.write(file_name, JSON.
|
244
|
+
File.write(file_name, JSON.generate(@failed_examples.to_a))
|
216
245
|
end
|
217
246
|
|
218
247
|
def write_pending_examples_report
|
219
248
|
file_name = File.join(@cache_dir, 'pending_examples.json')
|
220
249
|
|
221
|
-
File.write(file_name, JSON.
|
250
|
+
File.write(file_name, JSON.generate(@pending_examples.to_a))
|
222
251
|
end
|
223
252
|
|
224
253
|
def write_all_files_report
|
225
254
|
file_name = File.join(@cache_dir, 'all_files.json')
|
226
255
|
|
227
|
-
File.write(file_name, JSON.
|
256
|
+
File.write(file_name, JSON.generate(@all_files))
|
228
257
|
end
|
229
258
|
|
230
259
|
def write_dependency_report
|
231
260
|
file_name = File.join(@cache_dir, 'dependency.json')
|
232
261
|
|
233
|
-
File.write(file_name, JSON.
|
262
|
+
File.write(file_name, JSON.generate(@dependency))
|
234
263
|
end
|
235
264
|
|
236
265
|
def write_reverse_dependency_report
|
237
266
|
file_name = File.join(@cache_dir, 'reverse_dependency.json')
|
238
267
|
|
239
|
-
File.write(file_name, JSON.
|
268
|
+
File.write(file_name, JSON.generate(@reverse_dependency))
|
240
269
|
end
|
241
270
|
|
242
271
|
def write_examples_coverage_report
|
243
272
|
file_name = File.join(@cache_dir, 'examples_coverage.json')
|
244
273
|
|
245
|
-
File.write(file_name, JSON.
|
274
|
+
File.write(file_name, JSON.generate(@examples_coverage))
|
246
275
|
end
|
247
276
|
|
248
277
|
def write_last_run_report
|
249
278
|
file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
|
250
279
|
last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
|
251
280
|
|
252
|
-
File.write(file_name, JSON.
|
281
|
+
File.write(file_name, JSON.generate(last_run_data))
|
253
282
|
end
|
254
283
|
end
|
255
284
|
end
|