rspec-tracer 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.generate(@all_examples))
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.generate(@flaky_examples.to_a))
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.generate(@failed_examples.to_a))
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.generate(@pending_examples.to_a))
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.generate(@all_files))
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.generate(@dependency))
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.generate(@reverse_dependency))
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.generate(@examples_coverage))
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.generate(last_run_data))
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(_example_groups)
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, example_groups = RSpecTracer.filter_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, 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(example_groups)
36
+ super(filtered_example_groups)
27
37
  end
28
38
  # rubocop:enable Metrics/AbcSize
29
39
  end
@@ -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
- source_file = RSpecTracer::SourceFile.from_path(file_path)
103
+ next if filtered_files.include?(file_path)
98
104
 
99
- next if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
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