rspec-tracer 0.6.1 → 0.9.0
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 +26 -0
- data/README.md +333 -260
- data/lib/rspec_tracer/configuration.rb +12 -23
- data/lib/rspec_tracer/coverage_reporter.rb +7 -3
- data/lib/rspec_tracer/html_reporter/Rakefile +18 -0
- data/lib/rspec_tracer/remote_cache/Rakefile +38 -0
- data/lib/rspec_tracer/remote_cache/aws.rb +178 -0
- data/lib/rspec_tracer/remote_cache/cache.rb +36 -144
- data/lib/rspec_tracer/remote_cache/repo.rb +175 -0
- data/lib/rspec_tracer/remote_cache/validator.rb +52 -0
- data/lib/rspec_tracer/reporter.rb +61 -9
- data/lib/rspec_tracer/rspec_runner.rb +14 -4
- data/lib/rspec_tracer/runner.rb +19 -5
- data/lib/rspec_tracer/version.rb +1 -1
- data/lib/rspec_tracer.rb +17 -14
- metadata +28 -21
- data/lib/rspec_tracer/remote_cache/git.rb +0 -113
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpecTracer
|
4
|
+
module RemoteCache
|
5
|
+
class Repo
|
6
|
+
class RepoError < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :branch_name, :branch_ref, :branch_refs, :ancestry_refs, :cache_refs
|
9
|
+
|
10
|
+
def initialize(aws)
|
11
|
+
@aws = aws
|
12
|
+
@branch_name = ENV['GIT_BRANCH'].chomp
|
13
|
+
|
14
|
+
raise RepoError, 'GIT_BRANCH environment variable is not set' if @branch_name.nil?
|
15
|
+
|
16
|
+
fetch_head_ref
|
17
|
+
fetch_branch_ref
|
18
|
+
fetch_ancestry_refs
|
19
|
+
fetch_branch_refs
|
20
|
+
generate_cache_refs
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def fetch_head_ref
|
26
|
+
@head_ref = `git rev-parse HEAD`.chomp
|
27
|
+
|
28
|
+
raise RepoError, 'Could not find HEAD commit sha' unless $CHILD_STATUS.success?
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch_branch_ref
|
32
|
+
@merged_parents = []
|
33
|
+
@ignored_refs = []
|
34
|
+
|
35
|
+
unless merged?
|
36
|
+
@branch_ref = @head_ref
|
37
|
+
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
@ignored_refs << @head_ref
|
42
|
+
|
43
|
+
fetch_merged_parents
|
44
|
+
fetch_merged_branch_ref
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch_ancestry_refs
|
48
|
+
ref_list = `git rev-list --max-count=25 #{@branch_ref}`.chomp.split
|
49
|
+
|
50
|
+
raise RepoError, 'Could not find ancestry refs' unless $CHILD_STATUS.success?
|
51
|
+
|
52
|
+
ref_list = ref_list.to_set - @ignored_refs
|
53
|
+
@ancestry_refs = refs_committer_timestamp(ref_list.to_a)
|
54
|
+
|
55
|
+
return if @ancestry_refs.empty?
|
56
|
+
|
57
|
+
print_refs(@ancestry_refs, 'ancestry')
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_branch_refs
|
61
|
+
unless @aws.branch_refs?(@branch_name)
|
62
|
+
puts "No branch refs for #{@branch_name} branch found in S3"
|
63
|
+
|
64
|
+
@branch_refs = {}
|
65
|
+
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
download_branch_refs
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_cache_refs
|
73
|
+
ref_list = @ancestry_refs.merge(@branch_refs)
|
74
|
+
|
75
|
+
if ref_list.empty?
|
76
|
+
@cache_refs = {}
|
77
|
+
|
78
|
+
return
|
79
|
+
end
|
80
|
+
|
81
|
+
@cache_refs = ref_list.sort_by { |_, timestamp| -timestamp }.to_h
|
82
|
+
|
83
|
+
print_refs(@cache_refs, 'cache')
|
84
|
+
end
|
85
|
+
|
86
|
+
def merged?
|
87
|
+
system('git', 'rev-parse', 'HEAD^2', out: File::NULL, err: File::NULL)
|
88
|
+
end
|
89
|
+
|
90
|
+
def fetch_merged_parents
|
91
|
+
first_parent = `git rev-parse HEAD^1`.chomp
|
92
|
+
@merged_parents << first_parent if $CHILD_STATUS.success?
|
93
|
+
|
94
|
+
second_parent = `git rev-parse HEAD^2`.chomp
|
95
|
+
@merged_parents << second_parent if $CHILD_STATUS.success?
|
96
|
+
|
97
|
+
raise RepoError, 'Could not find merged commit parents' if @merged_parents.length != 2
|
98
|
+
end
|
99
|
+
|
100
|
+
def fetch_merged_branch_ref
|
101
|
+
@origin_head_ref = `git rev-parse origin/HEAD`.chomp
|
102
|
+
@branch_ref = nil
|
103
|
+
|
104
|
+
if @merged_parents.first != @origin_head_ref
|
105
|
+
@branch_ref = @head_ref
|
106
|
+
@ignored_refs = []
|
107
|
+
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
@branch_ref = @merged_parents.last
|
112
|
+
@ignored_refs = @ignored_refs.to_set | `git rev-list #{@branch_ref}..origin/HEAD`.chomp.split
|
113
|
+
|
114
|
+
raise RepoError, 'Could not find ignored refs' unless $CHILD_STATUS.success?
|
115
|
+
end
|
116
|
+
|
117
|
+
def refs_committer_timestamp(ref_list)
|
118
|
+
return {} if ref_list.empty?
|
119
|
+
|
120
|
+
command = <<-COMMAND.strip.gsub(/\s+/, ' ')
|
121
|
+
git show
|
122
|
+
--no-patch
|
123
|
+
--format="%H %ct"
|
124
|
+
#{ref_list.join(' ')}
|
125
|
+
COMMAND
|
126
|
+
|
127
|
+
ref_list = `#{command}`.chomp
|
128
|
+
|
129
|
+
raise RepoError, 'Could not find ancestry refs' unless $CHILD_STATUS.success?
|
130
|
+
|
131
|
+
ref_list.split("\n").map(&:split).to_h.transform_values(&:to_i)
|
132
|
+
end
|
133
|
+
|
134
|
+
def download_branch_refs
|
135
|
+
file_name = File.join(RSpecTracer.cache_path, 'branch_refs.json')
|
136
|
+
|
137
|
+
if @aws.download_branch_refs(branch_name, file_name)
|
138
|
+
@branch_refs = JSON.parse(File.read(file_name)).transform_values(&:to_i)
|
139
|
+
|
140
|
+
return if @branch_refs.empty?
|
141
|
+
|
142
|
+
filter_branch_refs
|
143
|
+
print_refs(@branch_refs, 'branch')
|
144
|
+
else
|
145
|
+
@branch_refs = {}
|
146
|
+
|
147
|
+
File.rm_f(file_name)
|
148
|
+
|
149
|
+
puts "Failed to fetch branch refs for #{@branch_name} branch"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def filter_branch_refs
|
154
|
+
if @ancestry_refs.empty?
|
155
|
+
@branch_refs = @branch_refs.sort_by { |_, timestamp| -timestamp }.first(25).to_h
|
156
|
+
|
157
|
+
return
|
158
|
+
end
|
159
|
+
|
160
|
+
oldest_ancestry_time = @ancestry_refs.values.min
|
161
|
+
|
162
|
+
@branch_refs = @branch_refs
|
163
|
+
.select { |_, timestamp| timestamp >= oldest_ancestry_time }
|
164
|
+
.sort_by { |_, timestamp| -timestamp }
|
165
|
+
.first(25)
|
166
|
+
.to_h
|
167
|
+
end
|
168
|
+
|
169
|
+
def print_refs(refs, type)
|
170
|
+
puts "Fetched the following #{type} refs for #{@branch_name} branch:"
|
171
|
+
puts refs.map { |ref, timestamp| " * #{ref} (commit timestamp: #{timestamp})" }.join("\n")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpecTracer
|
4
|
+
module RemoteCache
|
5
|
+
class Validator
|
6
|
+
CACHE_FILES_PER_TEST_SUITE = 8
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@test_suite_id = ENV['TEST_SUITE_ID']
|
10
|
+
@test_suites = ENV['TEST_SUITES']
|
11
|
+
|
12
|
+
if @test_suite_id.nil? ^ @test_suites.nil?
|
13
|
+
raise(
|
14
|
+
ValidationError,
|
15
|
+
'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
setup
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?(ref, cache_files)
|
23
|
+
last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
|
24
|
+
|
25
|
+
return false if cache_files.count { |file| file.match?(last_run_regex) } != @last_run_files_count
|
26
|
+
|
27
|
+
cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
|
28
|
+
|
29
|
+
cache_files.count { |file| file.match?(cache_regex) } == @cached_files_count
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def setup
|
35
|
+
if @test_suites.nil?
|
36
|
+
@last_run_files_count = 1
|
37
|
+
@last_run_files_regex = '/%<ref>s/last_run.json$'
|
38
|
+
@cached_files_count = CACHE_FILES_PER_TEST_SUITE
|
39
|
+
@cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
|
40
|
+
else
|
41
|
+
@test_suites = @test_suites.to_i
|
42
|
+
@test_suites_regex = (1..@test_suites).to_a.join('|')
|
43
|
+
|
44
|
+
@last_run_files_count = @test_suites
|
45
|
+
@last_run_files_regex = "/%<ref>s/(#{@test_suites_regex})/last_run.json$"
|
46
|
+
@cached_files_count = CACHE_FILES_PER_TEST_SUITE * @test_suites
|
47
|
+
@cached_files_regex = "/%<ref>s/(#{@test_suites_regex})/[0-9a-f]{32}/.+.json$"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -15,6 +15,7 @@ module RSpecTracer
|
|
15
15
|
|
16
16
|
def register_example(example)
|
17
17
|
@all_examples[example[:example_id]] = example
|
18
|
+
@duplicate_examples[example[:example_id]] << example
|
18
19
|
end
|
19
20
|
|
20
21
|
def on_example_skipped(example_id)
|
@@ -106,6 +107,17 @@ module RSpecTracer
|
|
106
107
|
file_deleted?(file_name) || file_modified?(file_name)
|
107
108
|
end
|
108
109
|
|
110
|
+
def incorrect_analysis?
|
111
|
+
@duplicate_examples.select! { |_, examples| examples.count > 1 }
|
112
|
+
|
113
|
+
return false if @duplicate_examples.empty?
|
114
|
+
|
115
|
+
print_not_use_notice
|
116
|
+
print_duplicate_examples
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
109
121
|
def register_dependency(example_id, file_name)
|
110
122
|
@dependency[example_id] << file_name
|
111
123
|
end
|
@@ -169,6 +181,7 @@ module RSpecTracer
|
|
169
181
|
|
170
182
|
def initialize_examples
|
171
183
|
@all_examples = {}
|
184
|
+
@duplicate_examples = Hash.new { |examples, example_id| examples[example_id] = [] }
|
172
185
|
@passed_examples = Set.new
|
173
186
|
@possibly_flaky_examples = Set.new
|
174
187
|
@flaky_examples = Set.new
|
@@ -226,59 +239,98 @@ module RSpecTracer
|
|
226
239
|
@reverse_dependency = report.to_h
|
227
240
|
end
|
228
241
|
|
242
|
+
def print_not_use_notice
|
243
|
+
justify = ' ' * 4
|
244
|
+
four_justify = justify * 4
|
245
|
+
|
246
|
+
puts '=' * 80
|
247
|
+
puts "#{four_justify}IMPORTANT NOTICE -- DO NOT USE RSPEC TRACER"
|
248
|
+
puts '=' * 80
|
249
|
+
puts "#{justify}It would be best to make changes so that the RSpec tracer can uniquely"
|
250
|
+
puts "#{justify}identify all the examples, and then you can enable the RSpec tracer back."
|
251
|
+
puts '=' * 80
|
252
|
+
puts
|
253
|
+
end
|
254
|
+
|
255
|
+
# rubocop:disable Metrics/AbcSize
|
256
|
+
def print_duplicate_examples
|
257
|
+
total = @duplicate_examples.sum { |_, examples| examples.length }
|
258
|
+
|
259
|
+
puts "RSpec tracer could not uniquely identify the following #{total} examples:"
|
260
|
+
|
261
|
+
justify = ' ' * 2
|
262
|
+
nested_justify = justify * 3
|
263
|
+
|
264
|
+
@duplicate_examples.each_pair do |example_id, examples|
|
265
|
+
puts "#{justify}- Example ID: #{example_id} (#{examples.count} examples)"
|
266
|
+
|
267
|
+
examples.each do |example|
|
268
|
+
description = example[:full_description].strip
|
269
|
+
file_name = example[:rerun_file_name].sub(%r{^/}, '')
|
270
|
+
line_number = example[:rerun_line_number]
|
271
|
+
location = "#{file_name}:#{line_number}"
|
272
|
+
|
273
|
+
puts "#{nested_justify}* #{description} (#{location})"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
puts
|
278
|
+
end
|
279
|
+
# rubocop:enable Metrics/AbcSize
|
280
|
+
|
229
281
|
def write_all_examples_report
|
230
282
|
file_name = File.join(@cache_dir, 'all_examples.json')
|
231
283
|
|
232
|
-
File.write(file_name, JSON.
|
284
|
+
File.write(file_name, JSON.pretty_generate(@all_examples))
|
233
285
|
end
|
234
286
|
|
235
287
|
def write_flaky_examples_report
|
236
288
|
file_name = File.join(@cache_dir, 'flaky_examples.json')
|
237
289
|
|
238
|
-
File.write(file_name, JSON.
|
290
|
+
File.write(file_name, JSON.pretty_generate(@flaky_examples.to_a))
|
239
291
|
end
|
240
292
|
|
241
293
|
def write_failed_examples_report
|
242
294
|
file_name = File.join(@cache_dir, 'failed_examples.json')
|
243
295
|
|
244
|
-
File.write(file_name, JSON.
|
296
|
+
File.write(file_name, JSON.pretty_generate(@failed_examples.to_a))
|
245
297
|
end
|
246
298
|
|
247
299
|
def write_pending_examples_report
|
248
300
|
file_name = File.join(@cache_dir, 'pending_examples.json')
|
249
301
|
|
250
|
-
File.write(file_name, JSON.
|
302
|
+
File.write(file_name, JSON.pretty_generate(@pending_examples.to_a))
|
251
303
|
end
|
252
304
|
|
253
305
|
def write_all_files_report
|
254
306
|
file_name = File.join(@cache_dir, 'all_files.json')
|
255
307
|
|
256
|
-
File.write(file_name, JSON.
|
308
|
+
File.write(file_name, JSON.pretty_generate(@all_files))
|
257
309
|
end
|
258
310
|
|
259
311
|
def write_dependency_report
|
260
312
|
file_name = File.join(@cache_dir, 'dependency.json')
|
261
313
|
|
262
|
-
File.write(file_name, JSON.
|
314
|
+
File.write(file_name, JSON.pretty_generate(@dependency))
|
263
315
|
end
|
264
316
|
|
265
317
|
def write_reverse_dependency_report
|
266
318
|
file_name = File.join(@cache_dir, 'reverse_dependency.json')
|
267
319
|
|
268
|
-
File.write(file_name, JSON.
|
320
|
+
File.write(file_name, JSON.pretty_generate(@reverse_dependency))
|
269
321
|
end
|
270
322
|
|
271
323
|
def write_examples_coverage_report
|
272
324
|
file_name = File.join(@cache_dir, 'examples_coverage.json')
|
273
325
|
|
274
|
-
File.write(file_name, JSON.
|
326
|
+
File.write(file_name, JSON.pretty_generate(@examples_coverage))
|
275
327
|
end
|
276
328
|
|
277
329
|
def write_last_run_report
|
278
330
|
file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
|
279
331
|
last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
|
280
332
|
|
281
|
-
File.write(file_name, JSON.
|
333
|
+
File.write(file_name, JSON.pretty_generate(last_run_data))
|
282
334
|
end
|
283
335
|
end
|
284
336
|
end
|
@@ -3,13 +3,23 @@
|
|
3
3
|
module RSpecTracer
|
4
4
|
module RSpecRunner
|
5
5
|
# rubocop:disable Metrics/AbcSize
|
6
|
-
def run_specs(
|
6
|
+
def run_specs(example_groups)
|
7
7
|
actual_count = RSpec.world.example_count
|
8
|
+
RSpecTracer.no_examples = actual_count.zero?
|
9
|
+
|
10
|
+
if RSpecTracer.no_examples
|
11
|
+
RSpecTracer.running = true
|
12
|
+
|
13
|
+
super(example_groups)
|
14
|
+
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
8
18
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
9
|
-
filtered_examples,
|
19
|
+
filtered_examples, filtered_example_groups = RSpecTracer.filter_examples
|
10
20
|
|
11
21
|
RSpec.world.instance_variable_set(:@filtered_examples, filtered_examples)
|
12
|
-
RSpec.world.instance_variable_set(:@example_groups,
|
22
|
+
RSpec.world.instance_variable_set(:@example_groups, filtered_example_groups)
|
13
23
|
|
14
24
|
current_count = RSpec.world.example_count
|
15
25
|
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -23,7 +33,7 @@ module RSpecTracer
|
|
23
33
|
|
24
34
|
RSpecTracer.running = true
|
25
35
|
|
26
|
-
super(
|
36
|
+
super(filtered_example_groups)
|
27
37
|
end
|
28
38
|
# rubocop:enable Metrics/AbcSize
|
29
39
|
end
|
data/lib/rspec_tracer/runner.rb
CHANGED
@@ -63,6 +63,10 @@ module RSpecTracer
|
|
63
63
|
@reporter.register_deleted_examples(@cache.all_examples)
|
64
64
|
end
|
65
65
|
|
66
|
+
def incorrect_analysis?
|
67
|
+
@reporter.incorrect_analysis?
|
68
|
+
end
|
69
|
+
|
66
70
|
# rubocop:disable Metrics/AbcSize
|
67
71
|
def generate_missed_coverage
|
68
72
|
missed_coverage = Hash.new do |files_coverage, file_path|
|
@@ -90,16 +94,15 @@ module RSpecTracer
|
|
90
94
|
# rubocop:enable Metrics/AbcSize
|
91
95
|
|
92
96
|
def register_dependency(examples_coverage)
|
97
|
+
filtered_files = Set.new
|
98
|
+
|
93
99
|
examples_coverage.each_pair do |example_id, example_coverage|
|
94
100
|
register_example_files_dependency(example_id)
|
95
101
|
|
96
102
|
example_coverage.each_key do |file_path|
|
97
|
-
|
103
|
+
next if filtered_files.include?(file_path)
|
98
104
|
|
99
|
-
|
100
|
-
|
101
|
-
@reporter.register_source_file(source_file)
|
102
|
-
@reporter.register_dependency(example_id, source_file[:file_name])
|
105
|
+
filtered_files << file_path unless register_file_dependency(example_id, file_path)
|
103
106
|
end
|
104
107
|
end
|
105
108
|
|
@@ -260,6 +263,17 @@ module RSpecTracer
|
|
260
263
|
@reporter.register_dependency(example_id, file_name)
|
261
264
|
end
|
262
265
|
|
266
|
+
def register_file_dependency(example_id, file_path)
|
267
|
+
source_file = RSpecTracer::SourceFile.from_path(file_path)
|
268
|
+
|
269
|
+
return false if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
|
270
|
+
|
271
|
+
@reporter.register_source_file(source_file)
|
272
|
+
@reporter.register_dependency(example_id, source_file[:file_name])
|
273
|
+
|
274
|
+
true
|
275
|
+
end
|
276
|
+
|
263
277
|
def generate_examples_status_report
|
264
278
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
265
279
|
|