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 +4 -4
- data/CHANGELOG.md +97 -0
- data/README.md +27 -2
- data/lib/rspec_tracer/cache.rb +88 -50
- data/lib/rspec_tracer/configuration.rb +19 -12
- data/lib/rspec_tracer/coverage_merger.rb +42 -0
- data/lib/rspec_tracer/coverage_reporter.rb +33 -29
- data/lib/rspec_tracer/coverage_writer.rb +58 -0
- data/lib/rspec_tracer/filter.rb +8 -8
- data/lib/rspec_tracer/html_reporter/reporter.rb +8 -7
- data/lib/rspec_tracer/remote_cache/aws.rb +3 -3
- data/lib/rspec_tracer/remote_cache/cache.rb +2 -2
- data/lib/rspec_tracer/remote_cache/repo.rb +4 -4
- data/lib/rspec_tracer/remote_cache/validator.rb +7 -5
- data/lib/rspec_tracer/report_generator.rb +158 -0
- data/lib/rspec_tracer/report_merger.rb +81 -0
- data/lib/rspec_tracer/report_writer.rb +141 -0
- data/lib/rspec_tracer/reporter.rb +4 -158
- data/lib/rspec_tracer/rspec_reporter.rb +5 -5
- data/lib/rspec_tracer/rspec_runner.rb +3 -5
- data/lib/rspec_tracer/runner.rb +14 -114
- data/lib/rspec_tracer/source_file.rb +9 -1
- data/lib/rspec_tracer/version.rb +1 -1
- data/lib/rspec_tracer.rb +211 -34
- metadata +9 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85f9e51c157fec46651a9276dd4b098d4ff749ef07efe66284b8c317b2b4d288
|
|
4
|
+
data.tar.gz: 676738fc078e588ac51233939274ed7245c9cdf41145dd86a2cdf7ce7068060d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|

|
|
2
2
|
|
|
3
|
+
[](https://discord.gg/H2G9yWeuRZ)
|
|
3
4
|
[](https://codeclimate.com/github/avmnu-sng/rspec-tracer/maintainability)
|
|
4
5
|
[](https://codeclimate.com/github/avmnu-sng/rspec-tracer/test_coverage)
|
|
5
6
|
[](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
|
|
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
|
|
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
|
data/lib/rspec_tracer/cache.rb
CHANGED
|
@@ -2,137 +2,175 @@
|
|
|
2
2
|
|
|
3
3
|
module RSpecTracer
|
|
4
4
|
class Cache
|
|
5
|
-
attr_reader :all_examples, :
|
|
6
|
-
:
|
|
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 @
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
48
|
+
elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
|
40
49
|
|
|
41
|
-
puts "RSpec tracer loaded cache from #{
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
|
52
69
|
|
|
53
|
-
puts "RSpec tracer loaded cached examples coverage (took #{
|
|
70
|
+
puts "RSpec tracer loaded cached examples coverage (took #{elapsed})" if RSpecTracer.verbose?
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
@examples_coverage
|
|
56
73
|
end
|
|
57
74
|
|
|
58
75
|
private
|
|
59
76
|
|
|
60
|
-
def last_run_id
|
|
61
|
-
file_name = File.join(
|
|
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(
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
file_name = File.join(
|
|
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
|
-
@
|
|
131
|
+
@failed_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
|
|
94
132
|
end
|
|
95
133
|
|
|
96
|
-
def
|
|
97
|
-
file_name = File.join(
|
|
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
|
-
@
|
|
139
|
+
@pending_examples = JSON.parse(File.read(file_name, encoding: 'UTF-8')).to_set
|
|
102
140
|
end
|
|
103
141
|
|
|
104
|
-
def
|
|
105
|
-
file_name = File.join(
|
|
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
|
-
@
|
|
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(
|
|
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(
|
|
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(
|
|
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 ||=
|
|
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 ||=
|
|
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 ||=
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|