rspec-tracer 0.6.0 → 0.8.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
@@ -229,56 +229,56 @@ module RSpecTracer
229
229
  def write_all_examples_report
230
230
  file_name = File.join(@cache_dir, 'all_examples.json')
231
231
 
232
- File.write(file_name, JSON.generate(@all_examples))
232
+ File.write(file_name, JSON.pretty_generate(@all_examples))
233
233
  end
234
234
 
235
235
  def write_flaky_examples_report
236
236
  file_name = File.join(@cache_dir, 'flaky_examples.json')
237
237
 
238
- File.write(file_name, JSON.generate(@flaky_examples.to_a))
238
+ File.write(file_name, JSON.pretty_generate(@flaky_examples.to_a))
239
239
  end
240
240
 
241
241
  def write_failed_examples_report
242
242
  file_name = File.join(@cache_dir, 'failed_examples.json')
243
243
 
244
- File.write(file_name, JSON.generate(@failed_examples.to_a))
244
+ File.write(file_name, JSON.pretty_generate(@failed_examples.to_a))
245
245
  end
246
246
 
247
247
  def write_pending_examples_report
248
248
  file_name = File.join(@cache_dir, 'pending_examples.json')
249
249
 
250
- File.write(file_name, JSON.generate(@pending_examples.to_a))
250
+ File.write(file_name, JSON.pretty_generate(@pending_examples.to_a))
251
251
  end
252
252
 
253
253
  def write_all_files_report
254
254
  file_name = File.join(@cache_dir, 'all_files.json')
255
255
 
256
- File.write(file_name, JSON.generate(@all_files))
256
+ File.write(file_name, JSON.pretty_generate(@all_files))
257
257
  end
258
258
 
259
259
  def write_dependency_report
260
260
  file_name = File.join(@cache_dir, 'dependency.json')
261
261
 
262
- File.write(file_name, JSON.generate(@dependency))
262
+ File.write(file_name, JSON.pretty_generate(@dependency))
263
263
  end
264
264
 
265
265
  def write_reverse_dependency_report
266
266
  file_name = File.join(@cache_dir, 'reverse_dependency.json')
267
267
 
268
- File.write(file_name, JSON.generate(@reverse_dependency))
268
+ File.write(file_name, JSON.pretty_generate(@reverse_dependency))
269
269
  end
270
270
 
271
271
  def write_examples_coverage_report
272
272
  file_name = File.join(@cache_dir, 'examples_coverage.json')
273
273
 
274
- File.write(file_name, JSON.generate(@examples_coverage))
274
+ File.write(file_name, JSON.pretty_generate(@examples_coverage))
275
275
  end
276
276
 
277
277
  def write_last_run_report
278
278
  file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
279
279
  last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
280
280
 
281
- File.write(file_name, JSON.generate(last_run_data))
281
+ File.write(file_name, JSON.pretty_generate(last_run_data))
282
282
  end
283
283
  end
284
284
  end
@@ -90,16 +90,15 @@ module RSpecTracer
90
90
  # rubocop:enable Metrics/AbcSize
91
91
 
92
92
  def register_dependency(examples_coverage)
93
+ filtered_files = Set.new
94
+
93
95
  examples_coverage.each_pair do |example_id, example_coverage|
94
96
  register_example_files_dependency(example_id)
95
97
 
96
98
  example_coverage.each_key do |file_path|
97
- source_file = RSpecTracer::SourceFile.from_path(file_path)
98
-
99
- next if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
99
+ next if filtered_files.include?(file_path)
100
100
 
101
- @reporter.register_source_file(source_file)
102
- @reporter.register_dependency(example_id, source_file[:file_name])
101
+ filtered_files << file_path unless register_file_dependency(example_id, file_path)
103
102
  end
104
103
  end
105
104
 
@@ -140,7 +139,7 @@ module RSpecTracer
140
139
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
141
140
  elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
142
141
 
143
- puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elpased})"
142
+ puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elpased})" if RSpecTracer.verbose?
144
143
  end
145
144
 
146
145
  @reporter.write_reports
@@ -162,7 +161,7 @@ module RSpecTracer
162
161
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
163
162
  elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
164
163
 
165
- puts "RSpec tracer processed cache (took #{elpased})"
164
+ puts "RSpec tracer processed cache (took #{elpased})" if RSpecTracer.verbose?
166
165
  end
167
166
 
168
167
  def filter_by_example_status
@@ -260,6 +259,17 @@ module RSpecTracer
260
259
  @reporter.register_dependency(example_id, file_name)
261
260
  end
262
261
 
262
+ def register_file_dependency(example_id, file_path)
263
+ source_file = RSpecTracer::SourceFile.from_path(file_path)
264
+
265
+ return false if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
266
+
267
+ @reporter.register_source_file(source_file)
268
+ @reporter.register_dependency(example_id, source_file[:file_name])
269
+
270
+ true
271
+ end
272
+
263
273
  def generate_examples_status_report
264
274
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
265
275
 
@@ -270,7 +280,7 @@ module RSpecTracer
270
280
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
271
281
  elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
272
282
 
273
- puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elpased})"
283
+ puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elpased})" if RSpecTracer.verbose?
274
284
  end
275
285
 
276
286
  def generate_all_files_report
@@ -21,11 +21,10 @@ module RSpecTracer
21
21
  next unless seconds.positive?
22
22
 
23
23
  seconds, remainder = seconds.divmod(count)
24
- remainder = format_duration(remainder)
25
24
 
26
25
  next if remainder.zero?
27
26
 
28
- duration << pluralize(remainder, unit)
27
+ duration << pluralize(format_duration(remainder), unit)
29
28
  end
30
29
 
31
30
  formatted_duration.reverse.join(' ')
@@ -36,17 +35,21 @@ module RSpecTracer
36
35
 
37
36
  precision = duration < 1 ? SECONDS_PRECISION : DEFAULT_PRECISION
38
37
 
39
- format("%<duration>0.#{precision}f", duration: duration)
38
+ strip_trailing_zeroes(format("%<duration>0.#{precision}f", duration: duration))
39
+ end
40
+
41
+ def strip_trailing_zeroes(formatted_duration)
42
+ formatted_duration.sub(/(?:(\..*[^0])0+|\.0+)$/, '\1')
40
43
  end
41
44
 
42
45
  def pluralize(duration, unit)
43
- if duration == 1
46
+ if (duration.to_f - 1).abs < Float::EPSILON
44
47
  "#{duration} #{unit}"
45
48
  else
46
49
  "#{duration} #{unit}s"
47
50
  end
48
51
  end
49
52
 
50
- private_class_method :format_duration, :pluralize
53
+ private_class_method :format_duration, :strip_trailing_zeroes, :pluralize
51
54
  end
52
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '0.6.0'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/rspec_tracer.rb CHANGED
@@ -201,7 +201,7 @@ module RSpecTracer
201
201
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
202
202
  elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
203
203
 
204
- puts "RSpec tracer processed dependency (took #{elpased})"
204
+ puts "RSpec tracer processed dependency (took #{elpased})" if RSpecTracer.verbose?
205
205
  end
206
206
 
207
207
  def process_coverage
@@ -214,7 +214,7 @@ module RSpecTracer
214
214
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
215
215
  elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
216
216
 
217
- puts "RSpec tracer processed coverage (took #{elpased})"
217
+ puts "RSpec tracer processed coverage (took #{elpased})" if RSpecTracer.verbose?
218
218
  end
219
219
 
220
220
  def run_simplecov_exit_task
@@ -250,7 +250,7 @@ module RSpecTracer
250
250
  }
251
251
  }
252
252
 
253
- File.write(file_name, JSON.generate(report))
253
+ File.write(file_name, JSON.pretty_generate(report))
254
254
  end
255
255
 
256
256
  def print_coverage_stats(file_name, elpased)
metadata CHANGED
@@ -1,55 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhimanyu Singh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-05 00:00:00.000000000 Z
11
+ date: 2021-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docile
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.1.0
20
17
  - - "~>"
21
18
  - !ruby/object:Gem::Version
22
19
  version: '1.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 1.1.0
30
27
  - - "~>"
31
28
  - !ruby/object:Gem::Version
32
29
  version: '1.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.0
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rspec-core
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: 3.6.0
40
37
  - - "~>"
41
38
  - !ruby/object:Gem::Version
42
39
  version: '3.6'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.6.0
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 3.6.0
50
47
  - - "~>"
51
48
  - !ruby/object:Gem::Version
52
49
  version: '3.6'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.6.0
53
53
  description: RSpec Tracer is a specs dependency analysis tool and a test skipper for
54
54
  RSpec. It maintains a list of files for each test, enabling itself to skip tests
55
55
  in the subsequent runs if none of the dependent files are changed.
@@ -69,6 +69,7 @@ files:
69
69
  - lib/rspec_tracer/defaults.rb
70
70
  - lib/rspec_tracer/example.rb
71
71
  - lib/rspec_tracer/filter.rb
72
+ - lib/rspec_tracer/html_reporter/Rakefile
72
73
  - lib/rspec_tracer/html_reporter/assets/javascripts/application.js
73
74
  - lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js
74
75
  - lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js
@@ -92,8 +93,11 @@ files:
92
93
  - lib/rspec_tracer/html_reporter/views/files_dependency.erb
93
94
  - lib/rspec_tracer/html_reporter/views/flaky_examples.erb
94
95
  - lib/rspec_tracer/html_reporter/views/layout.erb
96
+ - lib/rspec_tracer/remote_cache/Rakefile
97
+ - lib/rspec_tracer/remote_cache/aws.rb
95
98
  - lib/rspec_tracer/remote_cache/cache.rb
96
- - lib/rspec_tracer/remote_cache/git.rb
99
+ - lib/rspec_tracer/remote_cache/repo.rb
100
+ - lib/rspec_tracer/remote_cache/validator.rb
97
101
  - lib/rspec_tracer/reporter.rb
98
102
  - lib/rspec_tracer/rspec_reporter.rb
99
103
  - lib/rspec_tracer/rspec_runner.rb
@@ -107,7 +111,7 @@ licenses:
107
111
  - MIT
108
112
  metadata:
109
113
  homepage_uri: https://github.com/avmnu-sng/rspec-tracer
110
- source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.6.0
114
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.8.0
111
115
  changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
112
116
  bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
113
117
  post_install_message:
@@ -125,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
129
  - !ruby/object:Gem::Version
126
130
  version: '0'
127
131
  requirements: []
128
- rubygems_version: 3.0.9
132
+ rubygems_version: 3.2.26
129
133
  signing_key:
130
134
  specification_version: 4
131
135
  summary: RSpec Tracer is a specs dependency analysis tool and a test skipper for RSpec