rspec-tracer 1.0.0 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9aaeb8a00f4cebc311367e6a61d59526014849df9f7fe890455d64cef16d435f
4
- data.tar.gz: b803d5104a0a35f783bdd15b7a43252a1cfddcdd56d9f0a26c5a07bb8c36ef5e
3
+ metadata.gz: 57dd6b7ba2dafd73bbe30d91e42c36498c7e4fb505e02c405825cf45d8ac0787
4
+ data.tar.gz: e8886b116e2412f566ae908d3f0e2d9213df9ff70e0c88d54a8a4fb28dab9788
5
5
  SHA512:
6
- metadata.gz: 56acd78d6d4bf7b6270e8517a82d0bb1237dd78518a22d1a84a96de29e00c59379092cd725cc276d3f59ca2916306835c38426917747dcaf278e45d611c99389
7
- data.tar.gz: a4e026b2c6fdaad653bd48063c9d0b771e7e7bc8876f653cc2e32279930d6a043b546ab1c7a713e12aac934f1f069497decdb5856246d4ee8033e905577be1db
6
+ metadata.gz: 88ea3fbe3fef4c4ab2b2d2664ac5915b4e7eaa97b5ecc0d2b77dfeb03604e7f54faf4a567f65ff31235464f351a60e8817527d51f64f5a2717bb267f1cf72261
7
+ data.tar.gz: fbba1feed081e49a4cef7338c05b6d1022ecac5f013b499150b42b817e1cdb13bf2dc29ad993a502d8233bfe52dc89f991ad036eaeb513cde0b5d029b6413a46
data/CHANGELOG.md CHANGED
@@ -1,3 +1,113 @@
1
+ ## [1.0.2] - 2026-05-01
2
+
3
+ ### Fixed
4
+
5
+ - **Parallel-tests merge silently dropped peer caches and left worker
6
+ directories behind** when the spawned-worker count exceeded
7
+ `ENV['PARALLEL_TEST_GROUPS']`. The merge + purge call-sites in
8
+ `lib/rspec_tracer.rb` (`merge_parallel_tests_reports`,
9
+ `merge_parallel_tests_coverage_reports`,
10
+ `purge_parallel_tests_reports`) iterated `1..ENV['PARALLEL_TEST_GROUPS']`
11
+ to construct per-worker directory names. But parallel_tests sets
12
+ `PARALLEL_TEST_GROUPS = num_processes.to_s` for each child, where
13
+ `num_processes` is the user-requested process count
14
+ (`Parallel.processor_count` by default) — not the actual worker
15
+ count. When `num_processes < spawned_worker_count` (e.g. when the
16
+ spec-count partition produces more non-empty groups than
17
+ `num_processes`, or shared-runner CPU detection drifts mid-run),
18
+ peer caches with `TEST_ENV_NUMBER` above the env bound were silently
19
+ dropped from the merge (warm-run skip decisions get made against
20
+ an under-sampled merged manifest) and left behind by the purge
21
+ (visible as straggler `parallel_tests_<N>/` directories under
22
+ `rspec_tracer_cache/`). The same gem behaviour was documented on
23
+ v1.1.1's `last_process?` fix
24
+ ([PR #101](https://github.com/avmnu-sng/rspec-tracer/pull/101)) but
25
+ the iteration call-sites kept the buggy bound. Each method now globs
26
+ the actual `parallel_tests_*` subdirectories under its base path,
27
+ making the merge + purge robust to whatever count parallel_tests
28
+ spawned. (from v1.2.1) No cache format change.
29
+
30
+ ## [1.0.1] - 2026-04-24
31
+
32
+ Long-tail maintenance release. Backports high-impact crash and correctness
33
+ fixes from 1.1.x / 1.2.x onto the v1.0.0 foundation so users on Ruby 2.5 -
34
+ 3.0 who can't upgrade can still pick them up via `gem 'rspec-tracer', '~> 1.0'`.
35
+
36
+ ### Fixed
37
+
38
+ - `Cache#cached_examples_coverage` returns `{}` (not `nil`) when `last_run.json`
39
+ is present but `examples_coverage.json` is missing, preventing a
40
+ NoMethodError on the next consumer. (B1, from v1.1.0)
41
+ - `Runner#generate_missed_coverage` tolerates a nil cached coverage map and
42
+ nil per-line strength entries. (B2, from v1.1.0)
43
+ - `Runner#register_file_dependency` / `#register_example_file_dependency`
44
+ skip gracefully when `SourceFile.from_path` / `.from_name` resolves to
45
+ nil (e.g. gem-generated examples whose path is absent at runtime),
46
+ instead of crashing. (B3, from v1.1.0)
47
+ - `CoverageReporter#merge_coverage` treats a nil existing line-coverage
48
+ entry as 0 when summing skipped-test contributions. (B4, from v1.1.0)
49
+ - `parallel_tests`-mode merge-worker election no longer deadlocks under
50
+ slow CI: the elected worker is now picked via
51
+ `::ParallelTests.first_process?` (immutable at spawn) instead of the
52
+ lock-file max TEST_ENV_NUMBER (racy on slow runners). (from v1.1.1)
53
+ - Pin `encoding: 'UTF-8'` on every legacy `File.read` / `File.write` JSON
54
+ I/O site so shells with `LANG=` unset no longer crash the tracer on
55
+ multibyte spec descriptions. Cache (11), report_writer (12), and nine
56
+ other lib/ call sites covered. (from v1.1.2)
57
+ - `SourceFile.from_path` computes the file digest via `File.binread`, hashing
58
+ raw bytes regardless of Encoding.default_external. (from v1.1.2)
59
+ - `SourceFile.file_path` returns an absolute-external path unchanged when
60
+ the referenced file exists on disk (e.g. shared examples from vendored
61
+ gems at `/opt/bundle/gems/...`), preventing silent drop of dependency
62
+ registration. The guard is narrow — `start_with?('/') &&
63
+ !start_with?(root) && File.file?(path)` — so stripped-root cache forms
64
+ like `/spec/foo.rb` continue through the existing expand_path branch and
65
+ cache `file_name` keys stay byte-identical to v1.0.0. (C1, from v1.2.0)
66
+ - `RemoteCache::Aws#upload_dir` error message corrected from
67
+ "Failed to download files from" to "Failed to upload files from". (C3,
68
+ from v1.2.0)
69
+ - `RemoteCache::Validator::ValidationError` declared as a proper
70
+ `StandardError` subclass inside `Validator`; previously the
71
+ `TEST_SUITE_ID ^ TEST_SUITES` XOR-guard raised a `NameError:
72
+ uninitialized constant` instead of the intended validation error.
73
+ (from v1.2.0)
74
+ - `enviornment` → `environment` typo in the same XOR-guard raise message.
75
+ (from v1.2.0)
76
+ - `RemoteCache::Validator`'s single-suite `@cached_files_regex` anchored
77
+ with a trailing `$` so files with extensions beyond `.json` (e.g.
78
+ `.json.backup`) no longer match as cache files. (from v1.2.0)
79
+ - `RemoteCache::Repo#initialize` guards `ENV['GIT_BRANCH']` for nil
80
+ before calling `.chomp`; previously a `NoMethodError: undefined
81
+ method 'chomp' for nil:NilClass` crashed the init path and masked
82
+ the intended `RepoError` message when `GIT_BRANCH` was not set in
83
+ the environment. (from v1.1.0 PR #51)
84
+ - `RemoteCache::Repo#download_branch_refs` uses `FileUtils.rm_f`
85
+ (not `File.rm_f`) to clean up a partial `branch_refs.json` on a
86
+ failed AWS download. `File.rm_f` is undefined — `rm_f` is a
87
+ FileUtils method — so the failing-download branch would crash with
88
+ `NoMethodError` instead of cleaning up and logging. (from v1.1.0
89
+ PR #65)
90
+
91
+ ### Note on exclusions
92
+
93
+ The following items from 1.1.x / 1.2.0 are intentionally NOT in this
94
+ release to preserve the 1.0.0 cache-format contract and Ruby 2.5+ floor:
95
+
96
+ - Default-filter expansion (1.1.0 — adds `/lib/rspec_tracer/`,
97
+ `/usr/local/lib/ruby/`, etc. to the default filters). Changing the
98
+ default filter set shifts the files present in `all_files.json` for
99
+ most users, which would invalidate existing caches on upgrade.
100
+ - `USE_TEST_SUITE_ID_CACHE` opt-in ENV flag (1.2.0). This is a new
101
+ feature, not a bug fix; users who want it can upgrade to 1.2.x.
102
+ - The 1.1+ configuration DSL refactor (anonymous block forwarding,
103
+ alias_method wrapping, ENV.fetch normalizations). These are
104
+ Ruby-3.1-exclusive in places and orthogonal to the crash-fix scope.
105
+
106
+ ### Ruby support
107
+
108
+ Gemspec `required_ruby_version` unchanged at `>= 2.5.0`. CI gates
109
+ Ruby 2.5 - 4.0 inclusive on `ubuntu-latest`.
110
+
1
111
  ## [1.0.0] - 2021-10-21
2
112
 
3
113
  ### Added
@@ -17,6 +17,7 @@ module RSpecTracer
17
17
  @pending_examples = Set.new
18
18
  @all_files = {}
19
19
  @dependency = Hash.new { |hash, key| hash[key] = Set.new }
20
+ @examples_coverage = {}
20
21
  end
21
22
 
22
23
  def load_cache_for_run
@@ -50,23 +51,25 @@ module RSpecTracer
50
51
  end
51
52
 
52
53
  def cached_examples_coverage
53
- return @examples_coverage if defined?(@examples_coverage)
54
+ return @examples_coverage if @examples_coverage_loaded
55
+
56
+ @examples_coverage_loaded = true
54
57
 
55
58
  cache_path = RSpecTracer.cache_path
56
59
  cache_path = File.dirname(cache_path) if RSpecTracer.parallel_tests?
57
60
  run_id = last_run_id(cache_path)
58
61
 
59
- return @examples_coverage = {} if run_id.nil?
62
+ return @examples_coverage if run_id.nil?
60
63
 
61
64
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
62
65
  cache_dir = File.join(cache_path, run_id)
63
- coverage = load_examples_coverage_cache(cache_dir)
66
+ load_examples_coverage_cache(cache_dir)
64
67
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
68
  elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
66
69
 
67
70
  puts "RSpec tracer loaded cached examples coverage (took #{elapsed})" if RSpecTracer.verbose?
68
71
 
69
- coverage
72
+ @examples_coverage
70
73
  end
71
74
 
72
75
  private
@@ -76,7 +79,7 @@ module RSpecTracer
76
79
 
77
80
  return unless File.file?(file_name)
78
81
 
79
- JSON.parse(File.read(file_name))['run_id']
82
+ JSON.parse(File.read(file_name, encoding: 'UTF-8'))['run_id']
80
83
  end
81
84
 
82
85
  def load_all_examples_cache(cache_dir, discard_run_reason: true)
@@ -84,7 +87,7 @@ module RSpecTracer
84
87
 
85
88
  return unless File.file?(file_name)
86
89
 
87
- @all_examples = JSON.parse(File.read(file_name)).transform_values do |examples|
90
+ @all_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |examples|
88
91
  examples.transform_keys(&:to_sym)
89
92
  end
90
93
 
@@ -99,7 +102,7 @@ module RSpecTracer
99
102
 
100
103
  return unless File.file?(file_name)
101
104
 
102
- @duplicate_examples = JSON.parse(File.read(file_name)).transform_values do |examples|
105
+ @duplicate_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |examples|
103
106
  examples.map { |example| example.transform_keys(&:to_sym) }
104
107
  end
105
108
  end
@@ -109,7 +112,7 @@ module RSpecTracer
109
112
 
110
113
  return unless File.file?(file_name)
111
114
 
112
- @interrupted_examples = JSON.parse(File.read(file_name)).to_set
115
+ @interrupted_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
113
116
  end
114
117
 
115
118
  def load_flaky_examples_cache(cache_dir)
@@ -117,7 +120,7 @@ module RSpecTracer
117
120
 
118
121
  return unless File.file?(file_name)
119
122
 
120
- @flaky_examples = JSON.parse(File.read(file_name)).to_set
123
+ @flaky_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
121
124
  end
122
125
 
123
126
  def load_failed_examples_cache(cache_dir)
@@ -125,7 +128,7 @@ module RSpecTracer
125
128
 
126
129
  return unless File.file?(file_name)
127
130
 
128
- @failed_examples = JSON.parse(File.read(file_name)).to_set
131
+ @failed_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
129
132
  end
130
133
 
131
134
  def load_pending_examples_cache(cache_dir)
@@ -133,7 +136,7 @@ module RSpecTracer
133
136
 
134
137
  return unless File.file?(file_name)
135
138
 
136
- @pending_examples = JSON.parse(File.read(file_name)).to_set
139
+ @pending_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
137
140
  end
138
141
 
139
142
  def load_skipped_examples_cache(cache_dir)
@@ -141,7 +144,7 @@ module RSpecTracer
141
144
 
142
145
  return unless File.file?(file_name)
143
146
 
144
- @skipped_examples = JSON.parse(File.read(file_name)).to_set
147
+ @skipped_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
145
148
  end
146
149
 
147
150
  def load_all_files_cache(cache_dir)
@@ -149,7 +152,7 @@ module RSpecTracer
149
152
 
150
153
  return unless File.file?(file_name)
151
154
 
152
- @all_files = JSON.parse(File.read(file_name)).transform_values do |files|
155
+ @all_files = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |files|
153
156
  files.transform_keys(&:to_sym)
154
157
  end
155
158
  end
@@ -159,7 +162,7 @@ module RSpecTracer
159
162
 
160
163
  return unless File.file?(file_name)
161
164
 
162
- @dependency = JSON.parse(File.read(file_name)).transform_values(&:to_set)
165
+ @dependency = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values(&:to_set)
163
166
  end
164
167
 
165
168
  def load_examples_coverage_cache(cache_dir)
@@ -167,7 +170,7 @@ module RSpecTracer
167
170
 
168
171
  return unless File.file?(file_name)
169
172
 
170
- @examples_coverage = JSON.parse(File.read(file_name))
173
+ @examples_coverage = JSON.parse(File.read(file_name, encoding: 'UTF-8'))
171
174
  end
172
175
  end
173
176
  end
@@ -25,7 +25,7 @@ module RSpecTracer
25
25
  end
26
26
 
27
27
  def cache_dir
28
- @cache_dir ||= (ENV['RSPEC_TRACER_CACHE_DIR'] || DEFAULT_CACHE_DIR)
28
+ @cache_dir ||= ENV['RSPEC_TRACER_CACHE_DIR'] || DEFAULT_CACHE_DIR
29
29
  end
30
30
 
31
31
  def cache_path
@@ -41,7 +41,7 @@ module RSpecTracer
41
41
  end
42
42
 
43
43
  def report_dir
44
- @report_dir ||= (ENV['RSPEC_TRACER_REPORT_DIR'] || DEFAULT_REPORT_DIR)
44
+ @report_dir ||= ENV['RSPEC_TRACER_REPORT_DIR'] || DEFAULT_REPORT_DIR
45
45
  end
46
46
 
47
47
  def report_path
@@ -57,7 +57,7 @@ module RSpecTracer
57
57
  end
58
58
 
59
59
  def coverage_dir
60
- @coverage_dir ||= (ENV['RSPEC_TRACER_COVERAGE_DIR'] || DEFAULT_COVERAGE_DIR)
60
+ @coverage_dir ||= ENV['RSPEC_TRACER_COVERAGE_DIR'] || DEFAULT_COVERAGE_DIR
61
61
  end
62
62
 
63
63
  def coverage_path
@@ -114,7 +114,7 @@ module RSpecTracer
114
114
  if ParallelTests.first_process?
115
115
  'parallel_tests_1'
116
116
  else
117
- "parallel_tests_#{ENV['TEST_ENV_NUMBER']}"
117
+ "parallel_tests_#{ENV.fetch('TEST_ENV_NUMBER', nil)}"
118
118
  end
119
119
  end
120
120
 
@@ -14,7 +14,8 @@ module RSpecTracer
14
14
  reports_dir.each do |report_dir|
15
15
  next unless File.directory?(report_dir)
16
16
 
17
- cache_coverage = JSON.parse(File.read("#{report_dir}/coverage.json"))['RSpecTracer']['coverage']
17
+ cache_coverage = JSON.parse(File.read("#{report_dir}/coverage.json",
18
+ encoding: 'UTF-8'))['RSpecTracer']['coverage']
18
19
 
19
20
  cache_coverage.each_pair do |file_name, line_coverage|
20
21
  unless @coverage.key?(file_name)
@@ -72,7 +72,8 @@ module RSpecTracer
72
72
  end
73
73
 
74
74
  line_coverage.each_pair do |line_number, strength|
75
- line_coverage_dup[line_number.to_i] += strength
75
+ index = line_number.to_i
76
+ line_coverage_dup[index] = (line_coverage_dup[index] || 0) + strength
76
77
  end
77
78
 
78
79
  @coverage[file_path] = line_coverage_dup.freeze
@@ -164,7 +165,7 @@ module RSpecTracer
164
165
 
165
166
  def jruby_line_stub(file_path)
166
167
  lines = File.foreach(file_path).map { nil }
167
- root_node = ::JRuby.parse(File.read(file_path))
168
+ root_node = ::JRuby.parse(File.read(file_path, encoding: 'UTF-8'))
168
169
 
169
170
  visitor = org.jruby.ast.visitor.NodeVisitor.impl do |_name, node|
170
171
  if node.newline?
@@ -15,7 +15,7 @@ module RSpecTracer
15
15
  }
16
16
  }
17
17
 
18
- File.write(@file_name, JSON.pretty_generate(report))
18
+ File.write(@file_name, JSON.pretty_generate(report), encoding: 'UTF-8')
19
19
  end
20
20
 
21
21
  def print_stats(elapsed_time)
@@ -4,14 +4,6 @@ module RSpecTracer
4
4
  class Filter
5
5
  attr_reader :filter
6
6
 
7
- def initialize(filter)
8
- @filter = filter
9
- end
10
-
11
- def match?(_source_file)
12
- raise "#{self.class.name}#match? is not intended for direct use"
13
- end
14
-
15
7
  def self.register(filter)
16
8
  return filter if filter.is_a?(Filter)
17
9
 
@@ -32,6 +24,14 @@ module RSpecTracer
32
24
  raise ArgumentError, 'Unknow filter'
33
25
  end
34
26
  end
27
+
28
+ def initialize(filter)
29
+ @filter = filter
30
+ end
31
+
32
+ def match?(_source_file)
33
+ raise "#{self.class.name}#match? is not intended for direct use"
34
+ end
35
35
  end
36
36
 
37
37
  class ArrayFilter < RSpecTracer::Filter
@@ -211,7 +211,7 @@ module RSpecTracer
211
211
  end
212
212
 
213
213
  def template(name)
214
- ERB.new(File.read(File.join(File.dirname(__FILE__), 'views/', "#{name}.erb")))
214
+ ERB.new(File.read(File.join(File.dirname(__FILE__), 'views/', "#{name}.erb"), encoding: 'UTF-8'))
215
215
  end
216
216
 
217
217
  def example_status_css_class(example_status)
@@ -125,7 +125,7 @@ module RSpecTracer
125
125
  remote_dir = s3_dir(ref, run_id)
126
126
  local_dir = File.join(RSpecTracer.cache_path, run_id)
127
127
 
128
- raise AwsError, "Failed to download files from #{local_dir}" unless system(
128
+ raise AwsError, "Failed to upload files from #{local_dir}" unless system(
129
129
  @aws_cli,
130
130
  's3',
131
131
  'cp',
@@ -142,7 +142,7 @@ module RSpecTracer
142
142
  private
143
143
 
144
144
  def setup_s3
145
- s3_uri = ENV['RSPEC_TRACER_S3_URI']
145
+ s3_uri = ENV.fetch('RSPEC_TRACER_S3_URI', nil)
146
146
 
147
147
  raise AwsError, 'RSPEC_TRACER_S3_URI environment variable is not set' if s3_uri.nil?
148
148
 
@@ -165,7 +165,7 @@ module RSpecTracer
165
165
  end
166
166
 
167
167
  def s3_dir(ref, run_id = nil)
168
- test_suite_id = ENV['TEST_SUITE_ID']
168
+ test_suite_id = ENV.fetch('TEST_SUITE_ID', nil)
169
169
 
170
170
  if test_suite_id.nil?
171
171
  "s3://#{@s3_bucket}/#{@s3_path}/#{ref}/#{run_id}/".sub(%r{/+$}, '/')
@@ -64,7 +64,7 @@ module RSpecTracer
64
64
 
65
65
  ref_list = @repo.branch_refs.merge(@repo.branch_ref => branch_ref_time.to_i)
66
66
 
67
- File.write(file_name, JSON.pretty_generate(ref_list))
67
+ File.write(file_name, JSON.pretty_generate(ref_list), encoding: 'UTF-8')
68
68
  end
69
69
 
70
70
  def last_run_id
@@ -72,7 +72,7 @@ module RSpecTracer
72
72
 
73
73
  raise CacheError, 'Could not find any local cache to upload' unless File.file?(file_name)
74
74
 
75
- JSON.parse(File.read(file_name))['run_id']
75
+ JSON.parse(File.read(file_name, encoding: 'UTF-8'))['run_id']
76
76
  end
77
77
  end
78
78
  end
@@ -8,11 +8,11 @@ module RSpecTracer
8
8
  attr_reader :branch_name, :branch_ref, :branch_refs, :ancestry_refs, :cache_refs
9
9
 
10
10
  def initialize(aws)
11
+ raise RepoError, 'GIT_BRANCH environment variable is not set' if ENV['GIT_BRANCH'].nil?
12
+
11
13
  @aws = aws
12
14
  @branch_name = ENV['GIT_BRANCH'].chomp
13
15
 
14
- raise RepoError, 'GIT_BRANCH environment variable is not set' if @branch_name.nil?
15
-
16
16
  fetch_head_ref
17
17
  fetch_branch_ref
18
18
  fetch_ancestry_refs
@@ -135,7 +135,7 @@ module RSpecTracer
135
135
  file_name = File.join(RSpecTracer.cache_path, 'branch_refs.json')
136
136
 
137
137
  if @aws.download_branch_refs(branch_name, file_name)
138
- @branch_refs = JSON.parse(File.read(file_name)).transform_values(&:to_i)
138
+ @branch_refs = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values(&:to_i)
139
139
 
140
140
  return if @branch_refs.empty?
141
141
 
@@ -144,7 +144,7 @@ module RSpecTracer
144
144
  else
145
145
  @branch_refs = {}
146
146
 
147
- File.rm_f(file_name)
147
+ FileUtils.rm_f(file_name)
148
148
 
149
149
  puts "Failed to fetch branch refs for #{@branch_name} branch"
150
150
  end
@@ -3,16 +3,18 @@
3
3
  module RSpecTracer
4
4
  module RemoteCache
5
5
  class Validator
6
+ class ValidationError < StandardError; end
7
+
6
8
  CACHE_FILES_PER_TEST_SUITE = 11
7
9
 
8
10
  def initialize
9
- @test_suite_id = ENV['TEST_SUITE_ID']
10
- @test_suites = ENV['TEST_SUITES']
11
+ @test_suite_id = ENV.fetch('TEST_SUITE_ID', nil)
12
+ @test_suites = ENV.fetch('TEST_SUITES', nil)
11
13
 
12
14
  if @test_suite_id.nil? ^ @test_suites.nil?
13
15
  raise(
14
16
  ValidationError,
15
- 'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
17
+ 'Both the environment variables TEST_SUITE_ID and TEST_SUITES are not set'
16
18
  )
17
19
  end
18
20
 
@@ -36,7 +38,7 @@ module RSpecTracer
36
38
  @last_run_files_count = 1
37
39
  @last_run_files_regex = '/%<ref>s/last_run.json$'
38
40
  @cached_files_count = CACHE_FILES_PER_TEST_SUITE
39
- @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
41
+ @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json$'
40
42
  else
41
43
  @test_suites = @test_suites.to_i
42
44
  @test_suites_regex = (1..@test_suites).to_a.join('|')
@@ -70,7 +70,7 @@ module RSpecTracer
70
70
 
71
71
  def merge_last_run_report(cache_dir)
72
72
  file_name = File.join(cache_dir, 'last_run.json')
73
- cached_last_run = JSON.parse(File.read(file_name), symbolize_names: true)
73
+ cached_last_run = JSON.parse(File.read(file_name, encoding: 'UTF-8'), symbolize_names: true)
74
74
  cached_last_run[:pid] = [cached_last_run[:pid]]
75
75
 
76
76
  cached_last_run.delete_if { |key, _| %i[run_id timestamp].include?(key) }
@@ -68,74 +68,74 @@ module RSpecTracer
68
68
  def write_all_examples_report
69
69
  file_name = File.join(@cache_dir, 'all_examples.json')
70
70
 
71
- File.write(file_name, JSON.pretty_generate(@reporter.all_examples))
71
+ File.write(file_name, JSON.pretty_generate(@reporter.all_examples), encoding: 'UTF-8')
72
72
  end
73
73
 
74
74
  def write_duplicate_examples_report
75
75
  file_name = File.join(@cache_dir, 'duplicate_examples.json')
76
76
 
77
- File.write(file_name, JSON.pretty_generate(@reporter.duplicate_examples))
77
+ File.write(file_name, JSON.pretty_generate(@reporter.duplicate_examples), encoding: 'UTF-8')
78
78
  end
79
79
 
80
80
  def write_interrupted_examples_report
81
81
  file_name = File.join(@cache_dir, 'interrupted_examples.json')
82
82
 
83
- File.write(file_name, JSON.pretty_generate(@reporter.interrupted_examples.sort.to_a))
83
+ File.write(file_name, JSON.pretty_generate(@reporter.interrupted_examples.sort.to_a), encoding: 'UTF-8')
84
84
  end
85
85
 
86
86
  def write_flaky_examples_report
87
87
  file_name = File.join(@cache_dir, 'flaky_examples.json')
88
88
 
89
- File.write(file_name, JSON.pretty_generate(@reporter.flaky_examples.sort.to_a))
89
+ File.write(file_name, JSON.pretty_generate(@reporter.flaky_examples.sort.to_a), encoding: 'UTF-8')
90
90
  end
91
91
 
92
92
  def write_failed_examples_report
93
93
  file_name = File.join(@cache_dir, 'failed_examples.json')
94
94
 
95
- File.write(file_name, JSON.pretty_generate(@reporter.failed_examples.sort.to_a))
95
+ File.write(file_name, JSON.pretty_generate(@reporter.failed_examples.sort.to_a), encoding: 'UTF-8')
96
96
  end
97
97
 
98
98
  def write_pending_examples_report
99
99
  file_name = File.join(@cache_dir, 'pending_examples.json')
100
100
 
101
- File.write(file_name, JSON.pretty_generate(@reporter.pending_examples.sort.to_a))
101
+ File.write(file_name, JSON.pretty_generate(@reporter.pending_examples.sort.to_a), encoding: 'UTF-8')
102
102
  end
103
103
 
104
104
  def write_skipped_examples_report
105
105
  file_name = File.join(@cache_dir, 'skipped_examples.json')
106
106
 
107
- File.write(file_name, JSON.pretty_generate(@reporter.skipped_examples.sort.to_a))
107
+ File.write(file_name, JSON.pretty_generate(@reporter.skipped_examples.sort.to_a), encoding: 'UTF-8')
108
108
  end
109
109
 
110
110
  def write_all_files_report
111
111
  file_name = File.join(@cache_dir, 'all_files.json')
112
112
 
113
- File.write(file_name, JSON.pretty_generate(@reporter.all_files))
113
+ File.write(file_name, JSON.pretty_generate(@reporter.all_files), encoding: 'UTF-8')
114
114
  end
115
115
 
116
116
  def write_dependency_report
117
117
  file_name = File.join(@cache_dir, 'dependency.json')
118
118
 
119
- File.write(file_name, JSON.pretty_generate(@reporter.dependency))
119
+ File.write(file_name, JSON.pretty_generate(@reporter.dependency), encoding: 'UTF-8')
120
120
  end
121
121
 
122
122
  def write_reverse_dependency_report
123
123
  file_name = File.join(@cache_dir, 'reverse_dependency.json')
124
124
 
125
- File.write(file_name, JSON.pretty_generate(@reporter.reverse_dependency))
125
+ File.write(file_name, JSON.pretty_generate(@reporter.reverse_dependency), encoding: 'UTF-8')
126
126
  end
127
127
 
128
128
  def write_examples_coverage_report
129
129
  file_name = File.join(@cache_dir, 'examples_coverage.json')
130
130
 
131
- File.write(file_name, JSON.pretty_generate(@reporter.examples_coverage))
131
+ File.write(file_name, JSON.pretty_generate(@reporter.examples_coverage), encoding: 'UTF-8')
132
132
  end
133
133
 
134
134
  def write_last_run_report
135
135
  file_name = File.join(@report_dir, 'last_run.json')
136
136
  last_run_data = @reporter.last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
137
137
 
138
- File.write(file_name, JSON.pretty_generate(last_run_data))
138
+ File.write(file_name, JSON.pretty_generate(last_run_data), encoding: 'UTF-8')
139
139
  end
140
140
  end
141
141
  end
@@ -6,7 +6,7 @@ module RSpecTracer
6
6
  RSpecTracer.coverage_reporter.record_coverage
7
7
  RSpecTracer.start_example_trace if RSpecTracer.trace_example?
8
8
 
9
- super(example)
9
+ super
10
10
  end
11
11
 
12
12
  def example_finished(example)
@@ -16,28 +16,28 @@ module RSpecTracer
16
16
  example_id = example.metadata[:rspec_tracer_example_id]
17
17
  RSpecTracer.coverage_reporter.compute_diff(example_id)
18
18
 
19
- super(example)
19
+ super
20
20
  end
21
21
 
22
22
  def example_passed(example)
23
23
  example_id = example.metadata[:rspec_tracer_example_id]
24
24
  RSpecTracer.runner.on_example_passed(example_id, example.execution_result)
25
25
 
26
- super(example)
26
+ super
27
27
  end
28
28
 
29
29
  def example_failed(example)
30
30
  example_id = example.metadata[:rspec_tracer_example_id]
31
31
  RSpecTracer.runner.on_example_failed(example_id, example.execution_result)
32
32
 
33
- super(example)
33
+ super
34
34
  end
35
35
 
36
36
  def example_pending(example)
37
37
  example_id = example.metadata[:rspec_tracer_example_id]
38
38
  RSpecTracer.runner.on_example_pending(example_id, example.execution_result)
39
39
 
40
- super(example)
40
+ super
41
41
  end
42
42
  end
43
43
  end
@@ -9,7 +9,7 @@ module RSpecTracer
9
9
  if RSpecTracer.no_examples
10
10
  RSpecTracer.running = true
11
11
 
12
- super(example_groups)
12
+ super
13
13
 
14
14
  return
15
15
  end
@@ -77,7 +77,7 @@ module RSpecTracer
77
77
  end
78
78
  end
79
79
 
80
- @cache.cached_examples_coverage.each_pair do |example_id, example_coverage|
80
+ (@cache.cached_examples_coverage || {}).each_pair do |example_id, example_coverage|
81
81
  example_coverage.each_pair do |file_path, line_coverage|
82
82
  next if @reporter.example_interrupted?(example_id) ||
83
83
  @reporter.duplicate_example?(example_id)
@@ -89,7 +89,7 @@ module RSpecTracer
89
89
  next if @reporter.file_deleted?(file_name)
90
90
 
91
91
  line_coverage.each_pair do |line_number, strength|
92
- missed_coverage[file_path][line_number] += strength
92
+ missed_coverage[file_path][line_number] += strength || 0
93
93
  end
94
94
  end
95
95
  end
@@ -271,6 +271,11 @@ module RSpecTracer
271
271
 
272
272
  source_file = RSpecTracer::SourceFile.from_name(file_name)
273
273
 
274
+ if source_file.nil?
275
+ puts "Skipping missing source file #{file_name} for example #{example_id}" if RSpecTracer.verbose?
276
+ return
277
+ end
278
+
274
279
  @reporter.register_source_file(source_file)
275
280
  @reporter.register_dependency(example_id, file_name)
276
281
  end
@@ -281,6 +286,11 @@ module RSpecTracer
281
286
 
282
287
  source_file = RSpecTracer::SourceFile.from_path(file_path)
283
288
 
289
+ if source_file.nil?
290
+ puts "Skipping missing source file #{file_path} for example #{example_id}" if RSpecTracer.verbose?
291
+ return false
292
+ end
293
+
284
294
  return false if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
285
295
 
286
296
  @reporter.register_source_file(source_file)
@@ -12,7 +12,7 @@ module RSpecTracer
12
12
  {
13
13
  file_path: file_path,
14
14
  file_name: file_name(file_path),
15
- digest: Digest::MD5.hexdigest(File.read(file_path))
15
+ digest: Digest::MD5.hexdigest(File.binread(file_path))
16
16
  }
17
17
  end
18
18
 
@@ -25,7 +25,15 @@ module RSpecTracer
25
25
  end
26
26
 
27
27
  def file_path(file_name)
28
+ return file_name if absolute_external_file?(file_name)
29
+
28
30
  File.expand_path(file_name.sub(%r{^/}, ''), RSpecTracer.root)
29
31
  end
32
+
33
+ def absolute_external_file?(file_name)
34
+ file_name.start_with?('/') &&
35
+ !file_name.start_with?(RSpecTracer.root) &&
36
+ File.file?(file_name)
37
+ end
30
38
  end
31
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end
data/lib/rspec_tracer.rb CHANGED
@@ -111,27 +111,27 @@ module RSpecTracer
111
111
  end
112
112
 
113
113
  def runner
114
- return @runner if defined?(@runner)
114
+ @runner if defined?(@runner)
115
115
  end
116
116
 
117
117
  def coverage_reporter
118
- return @coverage_reporter if defined?(@coverage_reporter)
118
+ @coverage_reporter if defined?(@coverage_reporter)
119
119
  end
120
120
 
121
121
  def coverage_merger
122
- return @coverage_merger if defined?(@coverage_merger)
122
+ @coverage_merger if defined?(@coverage_merger)
123
123
  end
124
124
 
125
125
  def report_merger
126
- return @report_merger if defined?(@report_merger)
126
+ @report_merger if defined?(@report_merger)
127
127
  end
128
128
 
129
129
  def trace_point
130
- return @trace_point if defined?(@trace_point)
130
+ @trace_point if defined?(@trace_point)
131
131
  end
132
132
 
133
133
  def traced_files
134
- return @traced_files if defined?(@traced_files)
134
+ @traced_files if defined?(@traced_files)
135
135
  end
136
136
 
137
137
  def trace_example?
@@ -139,11 +139,11 @@ module RSpecTracer
139
139
  end
140
140
 
141
141
  def simplecov?
142
- return @simplecov if defined?(@simplecov)
142
+ defined?(@simplecov) && @simplecov == true
143
143
  end
144
144
 
145
145
  def parallel_tests?
146
- return @parallel_tests if defined?(@parallel_tests)
146
+ defined?(@parallel_tests) && @parallel_tests == true
147
147
  end
148
148
 
149
149
  private
@@ -165,7 +165,7 @@ module RSpecTracer
165
165
  end
166
166
 
167
167
  def initial_setup
168
- unless setup_rspec
168
+ unless setup_rspec?
169
169
  puts 'Could not find a running RSpec process'
170
170
 
171
171
  return
@@ -179,7 +179,7 @@ module RSpecTracer
179
179
  end
180
180
 
181
181
  def parallel_tests_setup
182
- @parallel_tests = !(ENV['TEST_ENV_NUMBER'] && ENV['PARALLEL_TEST_GROUPS']).nil?
182
+ @parallel_tests = !(ENV.fetch('TEST_ENV_NUMBER', nil) && ENV.fetch('PARALLEL_TEST_GROUPS', nil)).nil?
183
183
 
184
184
  return unless parallel_tests?
185
185
 
@@ -208,7 +208,7 @@ module RSpecTracer
208
208
  end
209
209
  end
210
210
 
211
- def setup_rspec
211
+ def setup_rspec?
212
212
  runners = ObjectSpace.each_object(::RSpec::Core::Runner) do |runner|
213
213
  runner_clazz = runner.singleton_class
214
214
  clazz = RSpecTracer::RSpecRunner
@@ -339,13 +339,8 @@ module RSpecTracer
339
339
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
340
340
  reports_dir = []
341
341
 
342
- 1.upto(ENV['PARALLEL_TEST_GROUPS'].to_i) do |test_num|
343
- cache_path = File.dirname(RSpecTracer.cache_path)
344
- cache_dir = File.join(cache_path, "parallel_tests_#{test_num}")
345
-
346
- next unless File.directory?(cache_dir)
347
-
348
- run_id = JSON.parse(File.read(File.join(cache_dir, 'last_run.json')))['run_id']
342
+ parallel_tests_peer_dirs(File.dirname(RSpecTracer.cache_path)).each do |cache_dir|
343
+ run_id = JSON.parse(File.read(File.join(cache_dir, 'last_run.json'), encoding: 'UTF-8'))['run_id']
349
344
 
350
345
  reports_dir << File.join(cache_dir, run_id)
351
346
  end
@@ -374,14 +369,8 @@ module RSpecTracer
374
369
  return unless parallel_tests_executed? && !simplecov?
375
370
 
376
371
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
377
- reports_dir = []
378
372
 
379
- 1.upto(ENV['PARALLEL_TEST_GROUPS'].to_i) do |test_num|
380
- coverage_path = File.dirname(RSpecTracer.coverage_path)
381
- coverage_dir = File.join(coverage_path, "parallel_tests_#{test_num}")
382
-
383
- reports_dir << coverage_dir if File.directory?(coverage_dir)
384
- end
373
+ reports_dir = parallel_tests_peer_dirs(File.dirname(RSpecTracer.coverage_path))
385
374
 
386
375
  coverage_merger.merge(reports_dir)
387
376
 
@@ -410,13 +399,34 @@ module RSpecTracer
410
399
  def purge_parallel_tests_reports
411
400
  return unless parallel_tests_executed?
412
401
 
413
- 1.upto(ENV['PARALLEL_TEST_GROUPS'].to_i) do |test_num|
414
- [RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path].each do |path|
415
- FileUtils.rm_rf(File.join(File.dirname(path), "parallel_tests_#{test_num}"))
402
+ [RSpecTracer.cache_path, RSpecTracer.coverage_path, RSpecTracer.report_path].each do |path|
403
+ parallel_tests_peer_dirs(File.dirname(path)).each do |worker_dir|
404
+ FileUtils.rm_rf(worker_dir)
416
405
  end
417
406
  end
418
407
  end
419
408
 
409
+ # Returns every `parallel_tests_*` subdirectory directly under
410
+ # `base_path`. Used by the parallel_tests merge + purge paths.
411
+ #
412
+ # Earlier patches iterated `1..ENV['PARALLEL_TEST_GROUPS'].to_i`
413
+ # to construct dir names, but parallel_tests's own runner sets
414
+ # PARALLEL_TEST_GROUPS to the user-requested process count
415
+ # (`Parallel.processor_count` by default), NOT the actual worker
416
+ # count. When num_processes < spawned_worker_count, the upper
417
+ # bound was too small: peer caches with TEST_ENV_NUMBER above the
418
+ # bound were silently dropped from the merge AND left behind by
419
+ # the purge. PR #101 (v1.1.1) documented this gem behaviour for
420
+ # `last_process?` detection but did not extend the fix to the
421
+ # iteration call-sites; this method closes that gap. Globbing the
422
+ # actual filesystem state is robust to the env discrepancy
423
+ # regardless of how the gem partitions specs.
424
+ def parallel_tests_peer_dirs(base_path)
425
+ Dir.glob(File.join(base_path, 'parallel_tests_*')).select do |path|
426
+ File.directory?(path)
427
+ end
428
+ end
429
+
420
430
  def parallel_tests_executed?
421
431
  return false unless parallel_tests? && parallel_tests_last_process?
422
432
 
@@ -425,18 +435,25 @@ module RSpecTracer
425
435
  true
426
436
  end
427
437
 
438
+ # Elects the worker that performs the per-run merge. Delegates to
439
+ # `::ParallelTests.first_process?`, which returns true iff
440
+ # `TEST_ENV_NUMBER.to_i <= 1` — i.e. for exactly one worker per run,
441
+ # regardless of how many workers were spawned vs. CPU count.
442
+ #
443
+ # The prior lock-file-max election deadlocked under slow CI: a fast
444
+ # worker could finish before a slow worker loaded spec_helper and
445
+ # registered its TEST_ENV_NUMBER; both then self-elected and spun on
446
+ # each other's pid inside wait_for_other_processes_to_finish.
447
+ #
448
+ # `track_parallel_tests_test_env_number` and the lock-file cleanup
449
+ # in at_exit_behavior are retained so that users observing
450
+ # `parallel_tests.lock` see no behavior change — the file is still
451
+ # written and removed, just no longer consulted for election.
428
452
  def parallel_tests_last_process?
429
453
  return false unless parallel_tests?
454
+ return false unless defined?(::ParallelTests)
430
455
 
431
- max_test_num = 0
432
-
433
- File.open(RSpecTracer.parallel_tests_lock_file, 'r') do |f|
434
- f.flock(File::LOCK_SH)
435
-
436
- max_test_num = f.read.to_i
437
- end
438
-
439
- ENV['TEST_ENV_NUMBER'].to_i == max_test_num
456
+ ::ParallelTests.first_process?
440
457
  end
441
458
  end
442
459
  end
metadata CHANGED
@@ -1,55 +1,54 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abhimanyu Singh
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-10-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: docile
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.1.0
20
16
  - - "~>"
21
17
  - !ruby/object:Gem::Version
22
18
  version: '1.1'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.0
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
26
25
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 1.1.0
30
26
  - - "~>"
31
27
  - !ruby/object:Gem::Version
32
28
  version: '1.1'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.1.0
33
32
  - !ruby/object:Gem::Dependency
34
33
  name: rspec-core
35
34
  requirement: !ruby/object:Gem::Requirement
36
35
  requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: 3.6.0
40
36
  - - "~>"
41
37
  - !ruby/object:Gem::Version
42
38
  version: '3.6'
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.6.0
43
42
  type: :runtime
44
43
  prerelease: false
45
44
  version_requirements: !ruby/object:Gem::Requirement
46
45
  requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 3.6.0
50
46
  - - "~>"
51
47
  - !ruby/object:Gem::Version
52
48
  version: '3.6'
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.6.0
53
52
  description: RSpec Tracer is a specs dependency analyzer, flaky tests detector, tests
54
53
  accelerator, and coverage reporter tool for RSpec. It maintains a list of files
55
54
  for each test, enabling itself to skip tests in the subsequent runs if none of the
@@ -119,10 +118,9 @@ licenses:
119
118
  - MIT
120
119
  metadata:
121
120
  homepage_uri: https://github.com/avmnu-sng/rspec-tracer
122
- source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.0.0
121
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.0.2
123
122
  changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
124
123
  bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
125
- post_install_message:
126
124
  rdoc_options: []
127
125
  require_paths:
128
126
  - lib
@@ -137,8 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
135
  - !ruby/object:Gem::Version
138
136
  version: '0'
139
137
  requirements: []
140
- rubygems_version: 3.0.9
141
- signing_key:
138
+ rubygems_version: 3.6.9
142
139
  specification_version: 4
143
140
  summary: RSpec Tracer is a specs dependency analyzer, flaky tests detector, tests
144
141
  accelerator, and coverage reporter tool.