rspec-tracer 0.6.0 → 0.8.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.
@@ -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