rspec-tracer 0.9.3 → 1.0.1

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: 91a840ac79a148f2203602abe8f3c0d3b2019565532edd3ac04990b7ca7f1035
4
- data.tar.gz: 0cd0bd13178616e79730c9f062098f8425964507fddd704f8c8725d2cad33050
3
+ metadata.gz: 85f9e51c157fec46651a9276dd4b098d4ff749ef07efe66284b8c317b2b4d288
4
+ data.tar.gz: 676738fc078e588ac51233939274ed7245c9cdf41145dd86a2cdf7ce7068060d
5
5
  SHA512:
6
- metadata.gz: a98b3e279e71fcab7ceef12ec25998ece1bef3710c00dfed75a760cca7541ec2dad8b599dfdfa154f22b0f9eb9d32be74984b0696535956c831ea2cba6289a05
7
- data.tar.gz: 907c29eb6e4ca0b013cf73ad3221a1137ec6ed7a44f493ec005b867a7d6808eaf99ec23c2ec1c35e7cb7c6e045be75eb49ca578162debc2fde8c3c8f786594a6
6
+ metadata.gz: c30eb3e54a8c19d7ad560cd03e860a12a9c5ead82a55af2b570e6a29737fb9c213a9ece0d0335b7882e8c65e851093e9f78710efe8df10e40fa54f01d2c84e51
7
+ data.tar.gz: fa0db0dd8257fc3841758fafbae04df2bd0abd64950b524ba3df10aace8a7965c11533d28dd78a51a4bfaf45dbdc34b634d48ccdce5b0c565a59ed3ee02cc7a1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,100 @@
1
+ ## [1.0.1] - 2026-04-24
2
+
3
+ Long-tail maintenance release. Backports high-impact crash and correctness
4
+ fixes from 1.1.x / 1.2.x onto the v1.0.0 foundation so users on Ruby 2.5 -
5
+ 3.0 who can't upgrade can still pick them up via `gem 'rspec-tracer', '~> 1.0'`.
6
+
7
+ ### Fixed
8
+
9
+ - `Cache#cached_examples_coverage` returns `{}` (not `nil`) when `last_run.json`
10
+ is present but `examples_coverage.json` is missing, preventing a
11
+ NoMethodError on the next consumer. (B1, from v1.1.0)
12
+ - `Runner#generate_missed_coverage` tolerates a nil cached coverage map and
13
+ nil per-line strength entries. (B2, from v1.1.0)
14
+ - `Runner#register_file_dependency` / `#register_example_file_dependency`
15
+ skip gracefully when `SourceFile.from_path` / `.from_name` resolves to
16
+ nil (e.g. gem-generated examples whose path is absent at runtime),
17
+ instead of crashing. (B3, from v1.1.0)
18
+ - `CoverageReporter#merge_coverage` treats a nil existing line-coverage
19
+ entry as 0 when summing skipped-test contributions. (B4, from v1.1.0)
20
+ - `parallel_tests`-mode merge-worker election no longer deadlocks under
21
+ slow CI: the elected worker is now picked via
22
+ `::ParallelTests.first_process?` (immutable at spawn) instead of the
23
+ lock-file max TEST_ENV_NUMBER (racy on slow runners). (from v1.1.1)
24
+ - Pin `encoding: 'UTF-8'` on every legacy `File.read` / `File.write` JSON
25
+ I/O site so shells with `LANG=` unset no longer crash the tracer on
26
+ multibyte spec descriptions. Cache (11), report_writer (12), and nine
27
+ other lib/ call sites covered. (from v1.1.2)
28
+ - `SourceFile.from_path` computes the file digest via `File.binread`, hashing
29
+ raw bytes regardless of Encoding.default_external. (from v1.1.2)
30
+ - `SourceFile.file_path` returns an absolute-external path unchanged when
31
+ the referenced file exists on disk (e.g. shared examples from vendored
32
+ gems at `/opt/bundle/gems/...`), preventing silent drop of dependency
33
+ registration. The guard is narrow — `start_with?('/') &&
34
+ !start_with?(root) && File.file?(path)` — so stripped-root cache forms
35
+ like `/spec/foo.rb` continue through the existing expand_path branch and
36
+ cache `file_name` keys stay byte-identical to v1.0.0. (C1, from v1.2.0)
37
+ - `RemoteCache::Aws#upload_dir` error message corrected from
38
+ "Failed to download files from" to "Failed to upload files from". (C3,
39
+ from v1.2.0)
40
+ - `RemoteCache::Validator::ValidationError` declared as a proper
41
+ `StandardError` subclass inside `Validator`; previously the
42
+ `TEST_SUITE_ID ^ TEST_SUITES` XOR-guard raised a `NameError:
43
+ uninitialized constant` instead of the intended validation error.
44
+ (from v1.2.0)
45
+ - `enviornment` → `environment` typo in the same XOR-guard raise message.
46
+ (from v1.2.0)
47
+ - `RemoteCache::Validator`'s single-suite `@cached_files_regex` anchored
48
+ with a trailing `$` so files with extensions beyond `.json` (e.g.
49
+ `.json.backup`) no longer match as cache files. (from v1.2.0)
50
+ - `RemoteCache::Repo#initialize` guards `ENV['GIT_BRANCH']` for nil
51
+ before calling `.chomp`; previously a `NoMethodError: undefined
52
+ method 'chomp' for nil:NilClass` crashed the init path and masked
53
+ the intended `RepoError` message when `GIT_BRANCH` was not set in
54
+ the environment. (from v1.1.0 PR #51)
55
+ - `RemoteCache::Repo#download_branch_refs` uses `FileUtils.rm_f`
56
+ (not `File.rm_f`) to clean up a partial `branch_refs.json` on a
57
+ failed AWS download. `File.rm_f` is undefined — `rm_f` is a
58
+ FileUtils method — so the failing-download branch would crash with
59
+ `NoMethodError` instead of cleaning up and logging. (from v1.1.0
60
+ PR #65)
61
+
62
+ ### Note on exclusions
63
+
64
+ The following items from 1.1.x / 1.2.0 are intentionally NOT in this
65
+ release to preserve the 1.0.0 cache-format contract and Ruby 2.5+ floor:
66
+
67
+ - Default-filter expansion (1.1.0 — adds `/lib/rspec_tracer/`,
68
+ `/usr/local/lib/ruby/`, etc. to the default filters). Changing the
69
+ default filter set shifts the files present in `all_files.json` for
70
+ most users, which would invalidate existing caches on upgrade.
71
+ - `USE_TEST_SUITE_ID_CACHE` opt-in ENV flag (1.2.0). This is a new
72
+ feature, not a bug fix; users who want it can upgrade to 1.2.x.
73
+ - The 1.1+ configuration DSL refactor (anonymous block forwarding,
74
+ alias_method wrapping, ENV.fetch normalizations). These are
75
+ Ruby-3.1-exclusive in places and orthogonal to the crash-fix scope.
76
+
77
+ ### Ruby support
78
+
79
+ Gemspec `required_ruby_version` unchanged at `>= 2.5.0`. CI gates
80
+ Ruby 2.5 - 4.0 inclusive on `ubuntu-latest`.
81
+
82
+ ## [1.0.0] - 2021-10-21
83
+
84
+ ### Added
85
+
86
+ - [JRuby](https://github.com/jruby/jruby) support
87
+ - [Parallel Tests](https://github.com/grosser/parallel_tests) support
88
+
89
+ ### Breaking Changes
90
+
91
+ The first run on this version will not use any cache on the CI because the number
92
+ of files changed from eight to eleven, so there will be no appropriate cache to use.
93
+
94
+ ## [0.9.3] - 2021-10-03
95
+
96
+ Generate reports ignoring duplicate examples (#42)
97
+
1
98
  ## [0.9.2] - 2021-09-30
2
99
 
3
100
  ### Fixed
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  ![](./readme_files/rspec_tracer.png)
2
2
 
3
+ [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/H2G9yWeuRZ)
3
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/eabce2757839c08d8f8d/maintainability)](https://codeclimate.com/github/avmnu-sng/rspec-tracer/maintainability)
4
5
  [![Test Coverage](https://api.codeclimate.com/v1/badges/eabce2757839c08d8f8d/test_coverage)](https://codeclimate.com/github/avmnu-sng/rspec-tracer/test_coverage)
5
6
  [![Gem Version](https://badge.fury.io/rb/rspec-tracer.svg)](https://badge.fury.io/rb/rspec-tracer)
@@ -34,6 +35,8 @@ installed.
34
35
 
35
36
  * [Demo](#demo)
36
37
  * [Getting Started](#getting-started)
38
+ * [Working with JRuby](#working-with-jruby)
39
+ * [Working with Parallel Tests](#working-with-parallel-tests)
37
40
  * [Configuring CI Caching](#configuring-ci-caching)
38
41
  * [Advanced Configuration](#advanced-configuration)
39
42
  * [Filters](#filters)
@@ -135,6 +138,28 @@ any of the application code.**
135
138
  3. After running your tests, open `rspec_tracer_report/index.html` in the browser
136
139
  of your choice.
137
140
 
141
+ ### Working with JRuby
142
+
143
+ It is recommend to use **JRuby 9.2.10.0+**. Also, configure it with **`JRUBY_OPTS="--debug -X+O"`**
144
+ or have the `.jrubyrc` file:
145
+
146
+ ```ruby
147
+ debug.fullTrace=true
148
+ objectspace.enabled=true
149
+ ```
150
+
151
+ ### Working with Parallel Tests
152
+
153
+ The Rspec tracer, by default, supports working with [parallel_tests](https://github.com/grosser/parallel_tests/)
154
+ gem. It maintains a lock file `/tmp/parallel_tests.lock` to identify the last
155
+ running process. Usually, you are not required to do anything special unless you
156
+ interrupt the execution in between and the process did not complete correctly.
157
+ In such a case, you must delete the lock file before the next run.
158
+
159
+ ```sh
160
+ rm -f /tmp/parallel_tests.lock && bundle exec parallel_rspec
161
+ ```
162
+
138
163
  ## Configuring CI Caching
139
164
 
140
165
  To enable RSpec Tracer to share cache between different builds on CI, update the
@@ -228,11 +253,11 @@ variables:
228
253
  ```
229
254
  - **`RSPEC_TRACER_COVERAGE_DIR`** to update the default coverage directory (`rspec_tracer_coverage`).
230
255
  ```sh
231
- export RSPEC_TRACER_CACHE_DIR=/tmp/rspec_tracer_coverage
256
+ export RSPEC_TRACER_COVERAGE_DIR=/tmp/rspec_tracer_coverage
232
257
  ```
233
258
  - **`RSPEC_TRACER_REPORT_DIR`** to update the default html reports directory (`rspec_tracer_report`).
234
259
  ```sh
235
- export RSPEC_TRACER_CACHE_DIR=/tmp/rspec_tracer_report
260
+ export RSPEC_TRACER_REPORT_DIR=/tmp/rspec_tracer_report
236
261
  ```
237
262
 
238
263
  These settings are available through environment variables because the rake tasks
@@ -2,137 +2,175 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Cache
5
- attr_reader :all_examples, :interrupted_examples, :flaky_examples, :failed_examples,
6
- :pending_examples, :all_files, :dependency, :run_id
5
+ attr_reader :all_examples, :duplicate_examples, :interrupted_examples,
6
+ :flaky_examples, :failed_examples, :pending_examples, :skipped_examples,
7
+ :all_files, :dependency, :examples_coverage, :run_id
7
8
 
8
9
  def initialize
9
- @run_id = last_run_id
10
- @cache_dir = File.join(RSpecTracer.cache_path, @run_id) if @run_id
11
-
12
10
  @cached = false
13
11
 
14
12
  @all_examples = {}
13
+ @duplicate_examples = {}
15
14
  @interrupted_examples = Set.new
16
15
  @flaky_examples = Set.new
17
16
  @failed_examples = Set.new
18
17
  @pending_examples = Set.new
19
18
  @all_files = {}
20
19
  @dependency = Hash.new { |hash, key| hash[key] = Set.new }
20
+ @examples_coverage = {}
21
21
  end
22
22
 
23
23
  def load_cache_for_run
24
- return if @run_id.nil? || @cached
24
+ return if @cached
25
+
26
+ cache_path = RSpecTracer.cache_path
27
+ cache_path = File.dirname(cache_path) if RSpecTracer.parallel_tests?
28
+ run_id = last_run_id(cache_path)
29
+
30
+ return if run_id.nil?
25
31
 
26
32
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
+ cache_dir = File.join(cache_path, run_id)
27
34
 
28
- load_all_examples_cache
29
- load_flaky_examples_cache
30
- load_failed_examples_cache
31
- load_pending_examples_cache
32
- load_all_files_cache
33
- load_dependency_cache
35
+ load_all_examples_cache(cache_dir)
36
+ load_duplicate_examples_cache(cache_dir)
37
+ load_interrupted_examples_cache(cache_dir)
38
+ load_flaky_examples_cache(cache_dir)
39
+ load_failed_examples_cache(cache_dir)
40
+ load_pending_examples_cache(cache_dir)
41
+ load_all_files_cache(cache_dir)
42
+ load_dependency_cache(cache_dir)
34
43
 
35
44
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
45
 
37
46
  @cached = true
38
47
 
39
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
48
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
40
49
 
41
- puts "RSpec tracer loaded cache from #{@cache_dir} (took #{elpased})"
50
+ puts "RSpec tracer loaded cache from #{cache_dir} (took #{elapsed})"
42
51
  end
43
52
 
44
53
  def cached_examples_coverage
45
- return @examples_coverage if defined?(@examples_coverage)
46
- return @examples_coverage = {} if @run_id.nil?
54
+ return @examples_coverage if @examples_coverage_loaded
55
+
56
+ @examples_coverage_loaded = true
57
+
58
+ cache_path = RSpecTracer.cache_path
59
+ cache_path = File.dirname(cache_path) if RSpecTracer.parallel_tests?
60
+ run_id = last_run_id(cache_path)
61
+
62
+ return @examples_coverage if run_id.nil?
47
63
 
48
64
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
- coverage = load_examples_coverage_cache
65
+ cache_dir = File.join(cache_path, run_id)
66
+ load_examples_coverage_cache(cache_dir)
50
67
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
68
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
52
69
 
53
- puts "RSpec tracer loaded cached examples coverage (took #{elpased})" if RSpecTracer.verbose?
70
+ puts "RSpec tracer loaded cached examples coverage (took #{elapsed})" if RSpecTracer.verbose?
54
71
 
55
- coverage
72
+ @examples_coverage
56
73
  end
57
74
 
58
75
  private
59
76
 
60
- def last_run_id
61
- file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
77
+ def last_run_id(cache_dir)
78
+ file_name = File.join(cache_dir, 'last_run.json')
62
79
 
63
80
  return unless File.file?(file_name)
64
81
 
65
- JSON.parse(File.read(file_name))['run_id']
82
+ JSON.parse(File.read(file_name, encoding: 'UTF-8'))['run_id']
66
83
  end
67
84
 
68
- def load_all_examples_cache
69
- file_name = File.join(@cache_dir, 'all_examples.json')
85
+ def load_all_examples_cache(cache_dir, discard_run_reason: true)
86
+ file_name = File.join(cache_dir, 'all_examples.json')
70
87
 
71
88
  return unless File.file?(file_name)
72
89
 
73
- @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|
74
91
  examples.transform_keys(&:to_sym)
75
92
  end
76
93
 
77
94
  @all_examples.each_value do |example|
78
- if example.key?(:execution_result)
79
- example[:execution_result].transform_keys!(&:to_sym)
80
- else
81
- @interrupted_examples << example[:example_id]
82
- end
95
+ example[:execution_result].transform_keys!(&:to_sym) if example.key?(:execution_result)
96
+ example[:run_reason] = nil if discard_run_reason
97
+ end
98
+ end
99
+
100
+ def load_duplicate_examples_cache(cache_dir)
101
+ file_name = File.join(cache_dir, 'duplicate_examples.json')
102
+
103
+ return unless File.file?(file_name)
83
104
 
84
- example[:run_reason] = nil
105
+ @duplicate_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).transform_values do |examples|
106
+ examples.map { |example| example.transform_keys(&:to_sym) }
85
107
  end
86
108
  end
87
109
 
88
- def load_flaky_examples_cache
89
- file_name = File.join(@cache_dir, 'flaky_examples.json')
110
+ def load_interrupted_examples_cache(cache_dir)
111
+ file_name = File.join(cache_dir, 'interrupted_examples.json')
112
+
113
+ return unless File.file?(file_name)
114
+
115
+ @interrupted_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
116
+ end
117
+
118
+ def load_flaky_examples_cache(cache_dir)
119
+ file_name = File.join(cache_dir, 'flaky_examples.json')
120
+
121
+ return unless File.file?(file_name)
122
+
123
+ @flaky_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
124
+ end
125
+
126
+ def load_failed_examples_cache(cache_dir)
127
+ file_name = File.join(cache_dir, 'failed_examples.json')
90
128
 
91
129
  return unless File.file?(file_name)
92
130
 
93
- @flaky_examples = JSON.parse(File.read(file_name)).to_set
131
+ @failed_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
94
132
  end
95
133
 
96
- def load_failed_examples_cache
97
- file_name = File.join(@cache_dir, 'failed_examples.json')
134
+ def load_pending_examples_cache(cache_dir)
135
+ file_name = File.join(cache_dir, 'pending_examples.json')
98
136
 
99
137
  return unless File.file?(file_name)
100
138
 
101
- @failed_examples = JSON.parse(File.read(file_name)).to_set
139
+ @pending_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
102
140
  end
103
141
 
104
- def load_pending_examples_cache
105
- file_name = File.join(@cache_dir, 'pending_examples.json')
142
+ def load_skipped_examples_cache(cache_dir)
143
+ file_name = File.join(cache_dir, 'skipped_examples.json')
106
144
 
107
145
  return unless File.file?(file_name)
108
146
 
109
- @pending_examples = JSON.parse(File.read(file_name)).to_set
147
+ @skipped_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
110
148
  end
111
149
 
112
- def load_all_files_cache
113
- file_name = File.join(@cache_dir, 'all_files.json')
150
+ def load_all_files_cache(cache_dir)
151
+ file_name = File.join(cache_dir, 'all_files.json')
114
152
 
115
153
  return unless File.file?(file_name)
116
154
 
117
- @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|
118
156
  files.transform_keys(&:to_sym)
119
157
  end
120
158
  end
121
159
 
122
- def load_dependency_cache
123
- file_name = File.join(@cache_dir, 'dependency.json')
160
+ def load_dependency_cache(cache_dir)
161
+ file_name = File.join(cache_dir, 'dependency.json')
124
162
 
125
163
  return unless File.file?(file_name)
126
164
 
127
- @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)
128
166
  end
129
167
 
130
- def load_examples_coverage_cache
131
- file_name = File.join(@cache_dir, 'examples_coverage.json')
168
+ def load_examples_coverage_cache(cache_dir)
169
+ file_name = File.join(cache_dir, 'examples_coverage.json')
132
170
 
133
171
  return unless File.file?(file_name)
134
172
 
135
- @examples_coverage = JSON.parse(File.read(file_name))
173
+ @examples_coverage = JSON.parse(File.read(file_name, encoding: 'UTF-8'))
136
174
  end
137
175
  end
138
176
  end
@@ -25,13 +25,14 @@ 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
32
32
  @cache_path ||= begin
33
33
  cache_path = File.expand_path(cache_dir, root)
34
- cache_path = File.join(cache_path, ENV['TEST_SUITE_ID'].to_s)
34
+ cache_path = File.join(cache_path, ENV['TEST_SUITE_ID'].to_s) if ENV['TEST_SUITE_ID']
35
+ cache_path = File.join(cache_path, parallel_tests_id) if RSpecTracer.parallel_tests?
35
36
 
36
37
  FileUtils.mkdir_p(cache_path)
37
38
 
@@ -40,13 +41,14 @@ module RSpecTracer
40
41
  end
41
42
 
42
43
  def report_dir
43
- @report_dir ||= (ENV['RSPEC_TRACER_REPORT_DIR'] || DEFAULT_REPORT_DIR)
44
+ @report_dir ||= ENV['RSPEC_TRACER_REPORT_DIR'] || DEFAULT_REPORT_DIR
44
45
  end
45
46
 
46
47
  def report_path
47
48
  @report_path ||= begin
48
49
  report_path = File.expand_path(report_dir, root)
49
- report_path = File.join(report_path, ENV['TEST_SUITE_ID'].to_s)
50
+ report_path = File.join(report_path, ENV['TEST_SUITE_ID'].to_s) if ENV['TEST_SUITE_ID']
51
+ report_path = File.join(report_path, parallel_tests_id) if RSpecTracer.parallel_tests?
50
52
 
51
53
  FileUtils.mkdir_p(report_path)
52
54
 
@@ -55,13 +57,14 @@ module RSpecTracer
55
57
  end
56
58
 
57
59
  def coverage_dir
58
- @coverage_dir ||= (ENV['RSPEC_TRACER_COVERAGE_DIR'] || DEFAULT_COVERAGE_DIR)
60
+ @coverage_dir ||= ENV['RSPEC_TRACER_COVERAGE_DIR'] || DEFAULT_COVERAGE_DIR
59
61
  end
60
62
 
61
63
  def coverage_path
62
64
  @coverage_path ||= begin
63
65
  coverage_path = File.expand_path(coverage_dir, root)
64
- coverage_path = File.join(coverage_path, ENV['TEST_SUITE_ID'].to_s)
66
+ coverage_path = File.join(coverage_path, ENV['TEST_SUITE_ID'].to_s) if ENV['TEST_SUITE_ID']
67
+ coverage_path = File.join(coverage_path, parallel_tests_id) if RSpecTracer.parallel_tests?
65
68
 
66
69
  FileUtils.mkdir_p(coverage_path)
67
70
 
@@ -93,6 +96,10 @@ module RSpecTracer
93
96
  @coverage_filters ||= []
94
97
  end
95
98
 
99
+ def parallel_tests_lock_file
100
+ '/tmp/parallel_tests.lock'
101
+ end
102
+
96
103
  def verbose?
97
104
  @verbose ||= (ENV.fetch('RSPEC_TRACER_VERBOSE', 'false') == 'true')
98
105
  end
@@ -103,12 +110,12 @@ module RSpecTracer
103
110
 
104
111
  private
105
112
 
106
- def test_suite_id
107
- suite_id = ENV.fetch('TEST_SUITE_ID', '')
108
-
109
- return if suite_id.empty?
110
-
111
- suite_id
113
+ def parallel_tests_id
114
+ if ParallelTests.first_process?
115
+ 'parallel_tests_1'
116
+ else
117
+ "parallel_tests_#{ENV.fetch('TEST_ENV_NUMBER', nil)}"
118
+ end
112
119
  end
113
120
 
114
121
  def at_exit(&block)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class CoverageMerger
5
+ attr_reader :coverage
6
+
7
+ def initialize
8
+ @coverage = {}
9
+ end
10
+
11
+ def merge(reports_dir)
12
+ return if RSpecTracer.simplecov?
13
+
14
+ reports_dir.each do |report_dir|
15
+ next unless File.directory?(report_dir)
16
+
17
+ cache_coverage = JSON.parse(File.read("#{report_dir}/coverage.json",
18
+ encoding: 'UTF-8'))['RSpecTracer']['coverage']
19
+
20
+ cache_coverage.each_pair do |file_name, line_coverage|
21
+ unless @coverage.key?(file_name)
22
+ @coverage[file_name] = line_coverage
23
+
24
+ next
25
+ end
26
+
27
+ merge_line_coverage(file_name, line_coverage)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def merge_line_coverage(file_name, line_coverage)
35
+ line_coverage.each_with_index do |strength, line_number|
36
+ next unless strength && @coverage[file_name][line_number]
37
+
38
+ @coverage[file_name][line_number] += strength
39
+ end
40
+ end
41
+ end
42
+ end
@@ -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
@@ -88,8 +89,6 @@ module RSpecTracer
88
89
  all_files.each do |file_path|
89
90
  @coverage[file_path] ||= line_stub(file_path).freeze
90
91
  end
91
-
92
- generate_final_coverage_stat
93
92
  end
94
93
 
95
94
  private
@@ -131,31 +130,6 @@ module RSpecTracer
131
130
  all_files.sort
132
131
  end
133
132
 
134
- def generate_final_coverage_stat
135
- total_loc = 0
136
- covered_loc = 0
137
-
138
- @coverage.each_pair do |_file_path, line_coverage|
139
- line_coverage.each do |strength|
140
- next if strength.nil?
141
-
142
- total_loc += 1
143
- covered_loc += 1 if strength.positive?
144
- end
145
- end
146
-
147
- @coverage_stat = {
148
- total_lines: total_loc,
149
- covered_lines: covered_loc,
150
- missed_lines: total_loc - covered_loc,
151
- covered_percent: 0.0
152
- }
153
-
154
- return if total_loc.zero?
155
-
156
- @coverage_stat[:covered_percent] = (100.0 * covered_loc / total_loc).round(2)
157
- end
158
-
159
133
  def peek_coverage
160
134
  data = ::Coverage.peek_result.select do |file_path, _|
161
135
  file_path.start_with?(RSpecTracer.root)
@@ -167,8 +141,17 @@ module RSpecTracer
167
141
  end
168
142
 
169
143
  def line_stub(file_path)
144
+ case RUBY_ENGINE
145
+ when 'ruby'
146
+ ruby_line_stub(file_path)
147
+ when 'jruby'
148
+ jruby_line_stub(file_path)
149
+ end
150
+ end
151
+
152
+ def ruby_line_stub(file_path)
170
153
  lines = File.foreach(file_path).map { nil }
171
- iseqs = [RubyVM::InstructionSequence.compile_file(file_path)]
154
+ iseqs = [::RubyVM::InstructionSequence.compile_file(file_path)]
172
155
 
173
156
  until iseqs.empty?
174
157
  iseq = iseqs.pop
@@ -179,5 +162,26 @@ module RSpecTracer
179
162
 
180
163
  lines
181
164
  end
165
+
166
+ def jruby_line_stub(file_path)
167
+ lines = File.foreach(file_path).map { nil }
168
+ root_node = ::JRuby.parse(File.read(file_path, encoding: 'UTF-8'))
169
+
170
+ visitor = org.jruby.ast.visitor.NodeVisitor.impl do |_name, node|
171
+ if node.newline?
172
+ if node.respond_to?(:position)
173
+ lines[node.position.line] = 0
174
+ else
175
+ lines[node.line] = 0
176
+ end
177
+ end
178
+
179
+ node.child_nodes.each { |child| child&.accept(visitor) }
180
+ end
181
+
182
+ root_node.accept(visitor)
183
+
184
+ lines
185
+ end
182
186
  end
183
187
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class CoverageWriter
5
+ def initialize(file_name, reporter)
6
+ @file_name = file_name
7
+ @reporter = reporter
8
+ end
9
+
10
+ def write_report
11
+ report = {
12
+ RSpecTracer: {
13
+ coverage: @reporter.coverage,
14
+ timestamp: Time.now.utc.to_i
15
+ }
16
+ }
17
+
18
+ File.write(@file_name, JSON.pretty_generate(report), encoding: 'UTF-8')
19
+ end
20
+
21
+ def print_stats(elapsed_time)
22
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
+
24
+ total, covered, percent = coverage_stats
25
+
26
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
+ elapsed = RSpecTracer::TimeFormatter.format_time((ending - starting) + elapsed_time)
28
+
29
+ puts <<-STATS.strip.gsub(/\s+/, ' ')
30
+ Coverage report generated for RSpecTracer to #{@file_name}.
31
+ #{covered} / #{total} LOC (#{percent}%) covered (took #{elapsed})
32
+ STATS
33
+ end
34
+
35
+ private
36
+
37
+ def coverage_stats
38
+ total_loc = 0
39
+ covered_loc = 0
40
+ covered_percent = 0.0
41
+
42
+ @reporter.coverage.each_pair do |_file_path, line_coverage|
43
+ line_coverage.each do |strength|
44
+ next if strength.nil?
45
+
46
+ total_loc += 1
47
+ covered_loc += 1 if strength.positive?
48
+ end
49
+ end
50
+
51
+ return [total_loc, covered_loc, covered_percent] if total_loc.zero?
52
+
53
+ covered_percent = (100.0 * covered_loc / total_loc).round(2)
54
+
55
+ [total_loc, covered_loc, covered_percent]
56
+ end
57
+ end
58
+ end