rspec-tracer 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1df1d0c90323ae123f24629f74f654bce0fff93bba8e8b052388d582dfb64d8f
4
- data.tar.gz: 5caabede1fb71ffcbbd70f0e9bfe8ca355303e843dbb99cacf14f146149afe5a
3
+ metadata.gz: 9aaeb8a00f4cebc311367e6a61d59526014849df9f7fe890455d64cef16d435f
4
+ data.tar.gz: b803d5104a0a35f783bdd15b7a43252a1cfddcdd56d9f0a26c5a07bb8c36ef5e
5
5
  SHA512:
6
- metadata.gz: 5ad9ec360d867ed745039fc2cf2a4aef314912a5b6a596c8c79e9d6615b00f2b578f5d3a164c2725ed710d9b0aa055855c607eebe52f7b032d1b959d483cb079
7
- data.tar.gz: 347236ad3e59372ff1cf33aa3c812ae5a22911ff6b46bfdfced44b96ef80161c4da0798a9afecf96846bc44963713bac4aa625c714266431b975c3152ebd92b1
6
+ metadata.gz: 56acd78d6d4bf7b6270e8517a82d0bb1237dd78518a22d1a84a96de29e00c59379092cd725cc276d3f59ca2916306835c38426917747dcaf278e45d611c99389
7
+ data.tar.gz: a4e026b2c6fdaad653bd48063c9d0b771e7e7bc8876f653cc2e32279930d6a043b546ab1c7a713e12aac934f1f069497decdb5856246d4ee8033e905577be1db
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [1.0.0] - 2021-10-21
2
+
3
+ ### Added
4
+
5
+ - [JRuby](https://github.com/jruby/jruby) support
6
+ - [Parallel Tests](https://github.com/grosser/parallel_tests) support
7
+
8
+ ### Breaking Changes
9
+
10
+ The first run on this version will not use any cache on the CI because the number
11
+ of files changed from eight to eleven, so there will be no appropriate cache to use.
12
+
13
+ ## [0.9.3] - 2021-10-03
14
+
15
+ Generate reports ignoring duplicate examples (#42)
16
+
17
+ ## [0.9.2] - 2021-09-30
18
+
19
+ ### Fixed
20
+
21
+ Caches getting corrupted on interrupts (#39)
22
+
23
+ ## [0.9.1] - 2021-09-23
24
+
25
+ ### Fixed
26
+
27
+ Flaky and failed examples dependency check (#38)
28
+
1
29
  ## [0.9.0] - 2021-09-15
2
30
 
3
31
  ### Added
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)
@@ -28,19 +29,19 @@ recommended to use **simplecov >= 0.12.0**. To use RSpec Tracer **cache on CI**,
28
29
  need to have an **S3 bucket** and **[AWS CLI](https://aws.amazon.com/cli/)**
29
30
  installed.
30
31
 
31
- You should take some time and go through the **[document](./RSPEC_TRACER.md)**
32
- describing the **intention** and implementation details of **managing dependency**,
33
- **managing flaky tests**, **skipping tests**, and **caching on CI**.
32
+ > You should take some time and go through the **[document](./RSPEC_TRACER.md)** describing the **intention** and implementation details of **managing dependency**, **managing flaky tests**, **skipping tests**, and **caching on CI**.
34
33
 
35
34
  ## Table of Contents
36
35
 
37
36
  * [Demo](#demo)
38
37
  * [Getting Started](#getting-started)
38
+ * [Working with JRuby](#working-with-jruby)
39
+ * [Working with Parallel Tests](#working-with-parallel-tests)
39
40
  * [Configuring CI Caching](#configuring-ci-caching)
40
41
  * [Advanced Configuration](#advanced-configuration)
41
42
  * [Filters](#filters)
42
43
  * [Environment Variables](#environment-variables)
43
- * [When Should You Not Use RSpec Tracer](#when-should-you-not-use-rspec-tracer)
44
+ * [Duplicate Examples](#duplicate-examples)
44
45
 
45
46
  ## Demo
46
47
 
@@ -64,6 +65,12 @@ These reports provide basic test information:
64
65
 
65
66
  ![](./readme_files/examples_report_next_run.png)
66
67
 
68
+ ### Duplicate Examples Report
69
+
70
+ These reports provide duplicate tests information.
71
+
72
+ ![](./readme_files/duplicate_examples_report.png)
73
+
67
74
  ### Flaky Examples Report
68
75
 
69
76
  These reports provide flaky tests information. Assuming **the following two tests
@@ -131,6 +138,28 @@ any of the application code.**
131
138
  3. After running your tests, open `rspec_tracer_report/index.html` in the browser
132
139
  of your choice.
133
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
+
134
163
  ## Configuring CI Caching
135
164
 
136
165
  To enable RSpec Tracer to share cache between different builds on CI, update the
@@ -224,11 +253,11 @@ variables:
224
253
  ```
225
254
  - **`RSPEC_TRACER_COVERAGE_DIR`** to update the default coverage directory (`rspec_tracer_coverage`).
226
255
  ```sh
227
- export RSPEC_TRACER_CACHE_DIR=/tmp/rspec_tracer_coverage
256
+ export RSPEC_TRACER_COVERAGE_DIR=/tmp/rspec_tracer_coverage
228
257
  ```
229
258
  - **`RSPEC_TRACER_REPORT_DIR`** to update the default html reports directory (`rspec_tracer_report`).
230
259
  ```sh
231
- export RSPEC_TRACER_CACHE_DIR=/tmp/rspec_tracer_report
260
+ export RSPEC_TRACER_REPORT_DIR=/tmp/rspec_tracer_report
232
261
  ```
233
262
 
234
263
  These settings are available through environment variables because the rake tasks
@@ -309,6 +338,9 @@ development environment. You can install [localstack](https://github.com/localst
309
338
  and [awscli-local](https://github.com/localstack/awscli-local) and then invoke the
310
339
  rake tasks with `LOCAL_AWS=true`.
311
340
 
341
+ - **`RSPEC_TRACER_FAIL_ON_DUPLICATES (default: true)`:** By default, RSpec Tracer
342
+ exits with one if there are [duplicate examples](#duplicate-examples).
343
+
312
344
  - **`RSPEC_TRACER_NO_SKIP (default: false)`:** Use this environment variables to
313
345
  not skip any tests. Note that it will continue to maintain cache files and generate
314
346
  reports.
@@ -335,7 +367,7 @@ specific test suites and not merge them.
335
367
  TEST_SUITE_ID=2 bundle exec rspec spec/helpers
336
368
  ```
337
369
 
338
- ## When Should You Not Use RSpec Tracer
370
+ ## Duplicate Examples
339
371
 
340
372
  To uniquely identify the examples is one of the requirements for the correctness
341
373
  of the RSpec Tracer. Sometimes, it would not be possible to do so depending upon
@@ -425,17 +457,12 @@ Calculator
425
457
  ```
426
458
 
427
459
  In this scenario, RSpec Tracer cannot determine the `Calculator#add` and
428
- `Calculator#sub` group examples. Also, it will ask you not to use the gem unless
429
- you have made some changes to your spec files.
460
+ `Calculator#sub` group examples.
430
461
 
431
462
  ```
432
463
  ================================================================================
433
- IMPORTANT NOTICE -- DO NOT USE RSPEC TRACER
464
+ IMPORTANT NOTICE -- RSPEC TRACER COULD NOT IDENTIFY SOME EXAMPLES UNIQUELY
434
465
  ================================================================================
435
- It would be best to make changes so that the RSpec tracer can uniquely
436
- identify all the examples, and then you can enable the RSpec tracer back.
437
- ================================================================================
438
-
439
466
  RSpec tracer could not uniquely identify the following 10 examples:
440
467
  - Example ID: eabd51a899db4f64d5839afe96004f03 (5 examples)
441
468
  * Calculator#add (spec/calculator_spec.rb:13)
@@ -2,16 +2,16 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Cache
5
- attr_reader :all_examples, :flaky_examples, :failed_examples, :pending_examples,
6
- :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 = {}
14
+ @interrupted_examples = Set.new
15
15
  @flaky_examples = Set.new
16
16
  @failed_examples = Set.new
17
17
  @pending_examples = Set.new
@@ -20,52 +20,67 @@ module RSpecTracer
20
20
  end
21
21
 
22
22
  def load_cache_for_run
23
- return if @run_id.nil? || @cached
23
+ return if @cached
24
+
25
+ cache_path = RSpecTracer.cache_path
26
+ cache_path = File.dirname(cache_path) if RSpecTracer.parallel_tests?
27
+ run_id = last_run_id(cache_path)
28
+
29
+ return if run_id.nil?
24
30
 
25
31
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ cache_dir = File.join(cache_path, run_id)
26
33
 
27
- load_all_examples_cache
28
- load_flaky_examples_cache
29
- load_failed_examples_cache
30
- load_pending_examples_cache
31
- load_all_files_cache
32
- load_dependency_cache
34
+ load_all_examples_cache(cache_dir)
35
+ load_duplicate_examples_cache(cache_dir)
36
+ load_interrupted_examples_cache(cache_dir)
37
+ load_flaky_examples_cache(cache_dir)
38
+ load_failed_examples_cache(cache_dir)
39
+ load_pending_examples_cache(cache_dir)
40
+ load_all_files_cache(cache_dir)
41
+ load_dependency_cache(cache_dir)
33
42
 
34
43
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
35
44
 
36
45
  @cached = true
37
46
 
38
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
47
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
39
48
 
40
- puts "RSpec tracer loaded cache from #{@cache_dir} (took #{elpased})"
49
+ puts "RSpec tracer loaded cache from #{cache_dir} (took #{elapsed})"
41
50
  end
42
51
 
43
52
  def cached_examples_coverage
44
53
  return @examples_coverage if defined?(@examples_coverage)
45
- return @examples_coverage = {} if @run_id.nil?
54
+
55
+ cache_path = RSpecTracer.cache_path
56
+ cache_path = File.dirname(cache_path) if RSpecTracer.parallel_tests?
57
+ run_id = last_run_id(cache_path)
58
+
59
+ return @examples_coverage = {} if run_id.nil?
46
60
 
47
61
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
48
- coverage = load_examples_coverage_cache
62
+ cache_dir = File.join(cache_path, run_id)
63
+ coverage = load_examples_coverage_cache(cache_dir)
49
64
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
65
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
51
66
 
52
- puts "RSpec tracer loaded cached examples coverage (took #{elpased})" if RSpecTracer.verbose?
67
+ puts "RSpec tracer loaded cached examples coverage (took #{elapsed})" if RSpecTracer.verbose?
53
68
 
54
69
  coverage
55
70
  end
56
71
 
57
72
  private
58
73
 
59
- def last_run_id
60
- file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
74
+ def last_run_id(cache_dir)
75
+ file_name = File.join(cache_dir, 'last_run.json')
61
76
 
62
77
  return unless File.file?(file_name)
63
78
 
64
79
  JSON.parse(File.read(file_name))['run_id']
65
80
  end
66
81
 
67
- def load_all_examples_cache
68
- file_name = File.join(@cache_dir, 'all_examples.json')
82
+ def load_all_examples_cache(cache_dir, discard_run_reason: true)
83
+ file_name = File.join(cache_dir, 'all_examples.json')
69
84
 
70
85
  return unless File.file?(file_name)
71
86
 
@@ -74,38 +89,63 @@ module RSpecTracer
74
89
  end
75
90
 
76
91
  @all_examples.each_value do |example|
77
- example[:execution_result].transform_keys!(&:to_sym)
92
+ example[:execution_result].transform_keys!(&:to_sym) if example.key?(:execution_result)
93
+ example[:run_reason] = nil if discard_run_reason
94
+ end
95
+ end
96
+
97
+ def load_duplicate_examples_cache(cache_dir)
98
+ file_name = File.join(cache_dir, 'duplicate_examples.json')
99
+
100
+ return unless File.file?(file_name)
78
101
 
79
- example[:run_reason] = nil
102
+ @duplicate_examples = JSON.parse(File.read(file_name)).transform_values do |examples|
103
+ examples.map { |example| example.transform_keys(&:to_sym) }
80
104
  end
81
105
  end
82
106
 
83
- def load_flaky_examples_cache
84
- file_name = File.join(@cache_dir, 'flaky_examples.json')
107
+ def load_interrupted_examples_cache(cache_dir)
108
+ file_name = File.join(cache_dir, 'interrupted_examples.json')
109
+
110
+ return unless File.file?(file_name)
111
+
112
+ @interrupted_examples = JSON.parse(File.read(file_name)).to_set
113
+ end
114
+
115
+ def load_flaky_examples_cache(cache_dir)
116
+ file_name = File.join(cache_dir, 'flaky_examples.json')
85
117
 
86
118
  return unless File.file?(file_name)
87
119
 
88
120
  @flaky_examples = JSON.parse(File.read(file_name)).to_set
89
121
  end
90
122
 
91
- def load_failed_examples_cache
92
- file_name = File.join(@cache_dir, 'failed_examples.json')
123
+ def load_failed_examples_cache(cache_dir)
124
+ file_name = File.join(cache_dir, 'failed_examples.json')
93
125
 
94
126
  return unless File.file?(file_name)
95
127
 
96
128
  @failed_examples = JSON.parse(File.read(file_name)).to_set
97
129
  end
98
130
 
99
- def load_pending_examples_cache
100
- file_name = File.join(@cache_dir, 'pending_examples.json')
131
+ def load_pending_examples_cache(cache_dir)
132
+ file_name = File.join(cache_dir, 'pending_examples.json')
101
133
 
102
134
  return unless File.file?(file_name)
103
135
 
104
136
  @pending_examples = JSON.parse(File.read(file_name)).to_set
105
137
  end
106
138
 
107
- def load_all_files_cache
108
- file_name = File.join(@cache_dir, 'all_files.json')
139
+ def load_skipped_examples_cache(cache_dir)
140
+ file_name = File.join(cache_dir, 'skipped_examples.json')
141
+
142
+ return unless File.file?(file_name)
143
+
144
+ @skipped_examples = JSON.parse(File.read(file_name)).to_set
145
+ end
146
+
147
+ def load_all_files_cache(cache_dir)
148
+ file_name = File.join(cache_dir, 'all_files.json')
109
149
 
110
150
  return unless File.file?(file_name)
111
151
 
@@ -114,16 +154,16 @@ module RSpecTracer
114
154
  end
115
155
  end
116
156
 
117
- def load_dependency_cache
118
- file_name = File.join(@cache_dir, 'dependency.json')
157
+ def load_dependency_cache(cache_dir)
158
+ file_name = File.join(cache_dir, 'dependency.json')
119
159
 
120
160
  return unless File.file?(file_name)
121
161
 
122
162
  @dependency = JSON.parse(File.read(file_name)).transform_values(&:to_set)
123
163
  end
124
164
 
125
- def load_examples_coverage_cache
126
- file_name = File.join(@cache_dir, 'examples_coverage.json')
165
+ def load_examples_coverage_cache(cache_dir)
166
+ file_name = File.join(cache_dir, 'examples_coverage.json')
127
167
 
128
168
  return unless File.file?(file_name)
129
169
 
@@ -31,7 +31,8 @@ module RSpecTracer
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
 
@@ -46,7 +47,8 @@ module RSpecTracer
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
 
@@ -61,7 +63,8 @@ module RSpecTracer
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['TEST_ENV_NUMBER']}"
118
+ end
112
119
  end
113
120
 
114
121
  def at_exit(&block)
@@ -0,0 +1,41 @@
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"))['RSpecTracer']['coverage']
18
+
19
+ cache_coverage.each_pair do |file_name, line_coverage|
20
+ unless @coverage.key?(file_name)
21
+ @coverage[file_name] = line_coverage
22
+
23
+ next
24
+ end
25
+
26
+ merge_line_coverage(file_name, line_coverage)
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def merge_line_coverage(file_name, line_coverage)
34
+ line_coverage.each_with_index do |strength, line_number|
35
+ next unless strength && @coverage[file_name][line_number]
36
+
37
+ @coverage[file_name][line_number] += strength
38
+ end
39
+ end
40
+ end
41
+ end
@@ -88,8 +88,6 @@ module RSpecTracer
88
88
  all_files.each do |file_path|
89
89
  @coverage[file_path] ||= line_stub(file_path).freeze
90
90
  end
91
-
92
- generate_final_coverage_stat
93
91
  end
94
92
 
95
93
  private
@@ -131,31 +129,6 @@ module RSpecTracer
131
129
  all_files.sort
132
130
  end
133
131
 
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
132
  def peek_coverage
160
133
  data = ::Coverage.peek_result.select do |file_path, _|
161
134
  file_path.start_with?(RSpecTracer.root)
@@ -167,8 +140,17 @@ module RSpecTracer
167
140
  end
168
141
 
169
142
  def line_stub(file_path)
143
+ case RUBY_ENGINE
144
+ when 'ruby'
145
+ ruby_line_stub(file_path)
146
+ when 'jruby'
147
+ jruby_line_stub(file_path)
148
+ end
149
+ end
150
+
151
+ def ruby_line_stub(file_path)
170
152
  lines = File.foreach(file_path).map { nil }
171
- iseqs = [RubyVM::InstructionSequence.compile_file(file_path)]
153
+ iseqs = [::RubyVM::InstructionSequence.compile_file(file_path)]
172
154
 
173
155
  until iseqs.empty?
174
156
  iseq = iseqs.pop
@@ -179,5 +161,26 @@ module RSpecTracer
179
161
 
180
162
  lines
181
163
  end
164
+
165
+ def jruby_line_stub(file_path)
166
+ lines = File.foreach(file_path).map { nil }
167
+ root_node = ::JRuby.parse(File.read(file_path))
168
+
169
+ visitor = org.jruby.ast.visitor.NodeVisitor.impl do |_name, node|
170
+ if node.newline?
171
+ if node.respond_to?(:position)
172
+ lines[node.position.line] = 0
173
+ else
174
+ lines[node.line] = 0
175
+ end
176
+ end
177
+
178
+ node.child_nodes.each { |child| child&.accept(visitor) }
179
+ end
180
+
181
+ root_node.accept(visitor)
182
+
183
+ lines
184
+ end
182
185
  end
183
186
  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))
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