rspec-tracer 0.8.0 → 0.9.3
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 +20 -0
- data/README.md +161 -48
- data/lib/rspec_tracer/cache.rb +8 -3
- data/lib/rspec_tracer/html_reporter/reporter.rb +49 -10
- data/lib/rspec_tracer/html_reporter/views/duplicate_examples.erb +34 -0
- data/lib/rspec_tracer/html_reporter/views/examples.erb +5 -0
- data/lib/rspec_tracer/html_reporter/views/layout.erb +8 -5
- data/lib/rspec_tracer/reporter.rb +79 -5
- data/lib/rspec_tracer/rspec_runner.rb +14 -4
- data/lib/rspec_tracer/runner.rb +42 -0
- data/lib/rspec_tracer/version.rb +1 -1
- data/lib/rspec_tracer.rb +19 -13
- metadata +11 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91a840ac79a148f2203602abe8f3c0d3b2019565532edd3ac04990b7ca7f1035
|
4
|
+
data.tar.gz: 0cd0bd13178616e79730c9f062098f8425964507fddd704f8c8725d2cad33050
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a98b3e279e71fcab7ceef12ec25998ece1bef3710c00dfed75a760cca7541ec2dad8b599dfdfa154f22b0f9eb9d32be74984b0696535956c831ea2cba6289a05
|
7
|
+
data.tar.gz: 907c29eb6e4ca0b013cf73ad3221a1137ec6ed7a44f493ec005b867a7d6808eaf99ec23c2ec1c35e7cb7c6e045be75eb49ca578162debc2fde8c3c8f786594a6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
## [0.9.2] - 2021-09-30
|
2
|
+
|
3
|
+
### Fixed
|
4
|
+
|
5
|
+
Caches getting corrupted on interrupts (#39)
|
6
|
+
|
7
|
+
## [0.9.1] - 2021-09-23
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
Flaky and failed examples dependency check (#38)
|
12
|
+
|
13
|
+
## [0.9.0] - 2021-09-15
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- Handling all examples filtered by RSpec (#34)
|
18
|
+
- Warn on incorrect analysis to stop using RSpec Tracer (#35)
|
19
|
+
- Run `SimpleCov.at_exit` hook (#36)
|
20
|
+
|
1
21
|
## [0.8.0] - 2021-09-13
|
2
22
|
|
3
23
|
### Fixed
|
data/README.md
CHANGED
@@ -28,9 +28,7 @@ recommended to use **simplecov >= 0.12.0**. To use RSpec Tracer **cache on CI**,
|
|
28
28
|
need to have an **S3 bucket** and **[AWS CLI](https://aws.amazon.com/cli/)**
|
29
29
|
installed.
|
30
30
|
|
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**.
|
31
|
+
> 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
32
|
|
35
33
|
## Table of Contents
|
36
34
|
|
@@ -40,6 +38,7 @@ describing the **intention** and implementation details of **managing dependency
|
|
40
38
|
* [Advanced Configuration](#advanced-configuration)
|
41
39
|
* [Filters](#filters)
|
42
40
|
* [Environment Variables](#environment-variables)
|
41
|
+
* [Duplicate Examples](#duplicate-examples)
|
43
42
|
|
44
43
|
## Demo
|
45
44
|
|
@@ -63,6 +62,12 @@ These reports provide basic test information:
|
|
63
62
|
|
64
63
|

|
65
64
|
|
65
|
+
### Duplicate Examples Report
|
66
|
+
|
67
|
+
These reports provide duplicate tests information.
|
68
|
+
|
69
|
+

|
70
|
+
|
66
71
|
### Flaky Examples Report
|
67
72
|
|
68
73
|
These reports provide flaky tests information. Assuming **the following two tests
|
@@ -93,7 +98,7 @@ These reports provide information on the total number of tests that will run aft
|
|
93
98
|
|
94
99
|
1. Add this line to your `Gemfile` and `bundle install`:
|
95
100
|
```ruby
|
96
|
-
gem 'rspec-tracer', '~> 0.
|
101
|
+
gem 'rspec-tracer', '~> 0.9', group: :test, require: false
|
97
102
|
```
|
98
103
|
|
99
104
|
And, add the followings to your `.gitignore`:
|
@@ -124,10 +129,8 @@ any of the application code.**
|
|
124
129
|
RSpecTracer.start
|
125
130
|
```
|
126
131
|
|
127
|
-
|
128
|
-
|
129
|
-
- SimpleCov **won't be able to provide branch coverage report** even when enabled.
|
130
|
-
- RSpec Tracer **nullifies the `SimpleCov.at_exit`** callback.
|
132
|
+
If you use RSpec Tracer with SimpleCov, then **SimpleCov would not report branch
|
133
|
+
coverage results even when enabled**.
|
131
134
|
|
132
135
|
3. After running your tests, open `rspec_tracer_report/index.html` in the browser
|
133
136
|
of your choice.
|
@@ -257,52 +260,48 @@ end
|
|
257
260
|
### Defining Custom Filteres
|
258
261
|
|
259
262
|
You can currently define a filter using either a String or Regexp (that will then
|
260
|
-
be Regexp-matched against each source file's
|
261
|
-
own Filter class.
|
262
|
-
|
263
|
-
#### String Filter
|
264
|
-
|
265
|
-
```ruby
|
266
|
-
RSpecTracer.start do
|
267
|
-
add_filter '/helpers/'
|
268
|
-
end
|
269
|
-
```
|
270
|
-
|
271
|
-
This simple string filter will remove all files that match "/helpers/" in their path.
|
272
|
-
|
273
|
-
#### Regex Filter
|
274
|
-
|
275
|
-
```ruby
|
276
|
-
RSpecTracer.start do
|
277
|
-
add_filter %r{^/helpers/}
|
278
|
-
end
|
279
|
-
```
|
263
|
+
be Regexp-matched against each source file's name relative to the project root),
|
264
|
+
a block or by passing in your own Filter class.
|
280
265
|
|
281
|
-
|
282
|
-
|
283
|
-
|
266
|
+
- **String Filter**: The string filter matches files that have the given string
|
267
|
+
in their name. For example, the following string filter will remove all files that
|
268
|
+
have `"/helpers/"` in their name.
|
269
|
+
```ruby
|
270
|
+
RSpecTracer.start do
|
271
|
+
add_filter '/helpers/'
|
272
|
+
end
|
273
|
+
```
|
284
274
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
275
|
+
- **Regex Filter**: The regex filter removes all files that have a successful name
|
276
|
+
match against the given regex expression. This simple regex filter will remove
|
277
|
+
all files that start with `%r{^/helper/}` in their name:
|
278
|
+
```ruby
|
279
|
+
RSpecTracer.start do
|
280
|
+
add_filter %r{^/helpers/}
|
289
281
|
end
|
290
|
-
|
291
|
-
```
|
282
|
+
```
|
292
283
|
|
293
|
-
Block filters receive a `Hash` object and expect your block
|
294
|
-
(if the file is to be removed from the result) or false
|
295
|
-
In the
|
284
|
+
- **Block Filter**: Block filters receive a `Hash` object and expect your block
|
285
|
+
to return either **true** (if the file is to be removed from the result) or **false**
|
286
|
+
(if the result should be kept). In the below example, the filter will remove all
|
287
|
+
files that match `"/helpers/"` in their path.
|
288
|
+
```ruby
|
289
|
+
RSpecTracer.start do
|
290
|
+
add_filter do |source_file|
|
291
|
+
source_file[:file_path].include?('/helpers/')
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
296
295
|
|
297
|
-
|
296
|
+
You can also use `source_file[:name]` to define the return value of the block
|
297
|
+
filter for the given source file.
|
298
298
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
You can pass in an array containing any of the other filter types.
|
299
|
+
- **Array Filter**: You can pass in an array containing any of the other filter types:
|
300
|
+
```ruby
|
301
|
+
RSpecTracer.start do
|
302
|
+
add_filter ['/helpers/', %r{^/utils/}]
|
303
|
+
end
|
304
|
+
```
|
306
305
|
|
307
306
|
## Environment Variables
|
308
307
|
|
@@ -314,6 +313,9 @@ development environment. You can install [localstack](https://github.com/localst
|
|
314
313
|
and [awscli-local](https://github.com/localstack/awscli-local) and then invoke the
|
315
314
|
rake tasks with `LOCAL_AWS=true`.
|
316
315
|
|
316
|
+
- **`RSPEC_TRACER_FAIL_ON_DUPLICATES (default: true)`:** By default, RSpec Tracer
|
317
|
+
exits with one if there are [duplicate examples](#duplicate-examples).
|
318
|
+
|
317
319
|
- **`RSPEC_TRACER_NO_SKIP (default: false)`:** Use this environment variables to
|
318
320
|
not skip any tests. Note that it will continue to maintain cache files and generate
|
319
321
|
reports.
|
@@ -340,6 +342,117 @@ specific test suites and not merge them.
|
|
340
342
|
TEST_SUITE_ID=2 bundle exec rspec spec/helpers
|
341
343
|
```
|
342
344
|
|
345
|
+
## Duplicate Examples
|
346
|
+
|
347
|
+
To uniquely identify the examples is one of the requirements for the correctness
|
348
|
+
of the RSpec Tracer. Sometimes, it would not be possible to do so depending upon
|
349
|
+
how we have written the specs. The following attributes determine the uniqueness:
|
350
|
+
|
351
|
+
- The example group
|
352
|
+
- The example full description
|
353
|
+
- The spec file location, i.e., file name and line number
|
354
|
+
- All the shared examples and contexts
|
355
|
+
|
356
|
+
Consider the following `Calculator` module:
|
357
|
+
```ruby
|
358
|
+
module Calculator
|
359
|
+
module_function
|
360
|
+
|
361
|
+
def add(a, b) a + b; end
|
362
|
+
def sub(a, b) a - b; end
|
363
|
+
def mul(a, b) a * b; end
|
364
|
+
end
|
365
|
+
```
|
366
|
+
|
367
|
+
And the corresponding spec file `spec/calculator_spec.rb`:
|
368
|
+
```ruby
|
369
|
+
RSpec.describe Calculator do
|
370
|
+
describe '#add' do
|
371
|
+
[
|
372
|
+
[1, 2, 3],
|
373
|
+
[0, 0, 0],
|
374
|
+
[5, 32, 37],
|
375
|
+
[-1, -8, -9],
|
376
|
+
[10, -10, 0]
|
377
|
+
].each { |a, b, r| it { expect(described_class.add(a, b)).to eq(r) } }
|
378
|
+
end
|
379
|
+
|
380
|
+
describe '#sub' do
|
381
|
+
[
|
382
|
+
[1, 2, -1],
|
383
|
+
[10, 0, 10],
|
384
|
+
[37, 5, 32],
|
385
|
+
[-1, -8, 7],
|
386
|
+
[10, 10, 0]
|
387
|
+
].each do |a, b, r|
|
388
|
+
it 'performs subtraction' do
|
389
|
+
expect(described_class.sub(a, b)).to eq(r)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
describe '#mul' do
|
395
|
+
[
|
396
|
+
[1, 2, -2],
|
397
|
+
[10, 0, 0],
|
398
|
+
[5, 7, 35],
|
399
|
+
[-1, -8, 8],
|
400
|
+
[10, 10, 100]
|
401
|
+
].each do |a, b, r|
|
402
|
+
it "multiplies #{a} and #{b} to #{r}" do
|
403
|
+
expect(described_class.mul(a, b)).to eq(r)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
```
|
409
|
+
|
410
|
+
Running the spec with `bundle exec rspec spec/calculator_spec.rb` generates the
|
411
|
+
following output:
|
412
|
+
```
|
413
|
+
Calculator
|
414
|
+
#mul
|
415
|
+
multiplies 5 and 7 to 35
|
416
|
+
multiplies 10 and 10 to 100
|
417
|
+
multiplies 10 and 0 to 0
|
418
|
+
multiplies 1 and 2 to -2 (FAILED - 1)
|
419
|
+
multiplies -1 and -8 to 8
|
420
|
+
#add
|
421
|
+
example at ./spec/calculator_spec.rb:13
|
422
|
+
example at ./spec/calculator_spec.rb:13
|
423
|
+
example at ./spec/calculator_spec.rb:13
|
424
|
+
example at ./spec/calculator_spec.rb:13
|
425
|
+
example at ./spec/calculator_spec.rb:13
|
426
|
+
#sub
|
427
|
+
performs subtraction
|
428
|
+
performs subtraction
|
429
|
+
performs subtraction
|
430
|
+
performs subtraction
|
431
|
+
performs subtraction
|
432
|
+
```
|
433
|
+
|
434
|
+
In this scenario, RSpec Tracer cannot determine the `Calculator#add` and
|
435
|
+
`Calculator#sub` group examples.
|
436
|
+
|
437
|
+
```
|
438
|
+
================================================================================
|
439
|
+
IMPORTANT NOTICE -- RSPEC TRACER COULD NOT IDENTIFY SOME EXAMPLES UNIQUELY
|
440
|
+
================================================================================
|
441
|
+
RSpec tracer could not uniquely identify the following 10 examples:
|
442
|
+
- Example ID: eabd51a899db4f64d5839afe96004f03 (5 examples)
|
443
|
+
* Calculator#add (spec/calculator_spec.rb:13)
|
444
|
+
* Calculator#add (spec/calculator_spec.rb:13)
|
445
|
+
* Calculator#add (spec/calculator_spec.rb:13)
|
446
|
+
* Calculator#add (spec/calculator_spec.rb:13)
|
447
|
+
* Calculator#add (spec/calculator_spec.rb:13)
|
448
|
+
- Example ID: 72171b502c5a42b9aa133f165cf09ec2 (5 examples)
|
449
|
+
* Calculator#sub performs subtraction (spec/calculator_spec.rb:24)
|
450
|
+
* Calculator#sub performs subtraction (spec/calculator_spec.rb:24)
|
451
|
+
* Calculator#sub performs subtraction (spec/calculator_spec.rb:24)
|
452
|
+
* Calculator#sub performs subtraction (spec/calculator_spec.rb:24)
|
453
|
+
* Calculator#sub performs subtraction (spec/calculator_spec.rb:24)
|
454
|
+
```
|
455
|
+
|
343
456
|
## Contributing
|
344
457
|
|
345
458
|
Read the [contribution guide](https://github.com/avmnu-sng/rspec-tracer/blob/main/.github/CONTRIBUTING.md).
|
data/lib/rspec_tracer/cache.rb
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
module RSpecTracer
|
4
4
|
class Cache
|
5
|
-
attr_reader :all_examples, :
|
6
|
-
:all_files, :dependency, :run_id
|
5
|
+
attr_reader :all_examples, :interrupted_examples, :flaky_examples, :failed_examples,
|
6
|
+
:pending_examples, :all_files, :dependency, :run_id
|
7
7
|
|
8
8
|
def initialize
|
9
9
|
@run_id = last_run_id
|
@@ -12,6 +12,7 @@ module RSpecTracer
|
|
12
12
|
@cached = false
|
13
13
|
|
14
14
|
@all_examples = {}
|
15
|
+
@interrupted_examples = Set.new
|
15
16
|
@flaky_examples = Set.new
|
16
17
|
@failed_examples = Set.new
|
17
18
|
@pending_examples = Set.new
|
@@ -74,7 +75,11 @@ module RSpecTracer
|
|
74
75
|
end
|
75
76
|
|
76
77
|
@all_examples.each_value do |example|
|
77
|
-
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
|
78
83
|
|
79
84
|
example[:run_reason] = nil
|
80
85
|
end
|
@@ -10,18 +10,12 @@ module RSpecTracer
|
|
10
10
|
|
11
11
|
def initialize
|
12
12
|
@reporter = RSpecTracer.runner.reporter
|
13
|
-
|
14
|
-
format_last_run
|
15
|
-
format_examples
|
16
|
-
format_flaky_examples
|
17
|
-
format_examples_dependency
|
18
|
-
format_files_dependency
|
19
13
|
end
|
20
14
|
|
21
15
|
def generate_report
|
22
16
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
23
17
|
|
24
|
-
|
18
|
+
prepare
|
25
19
|
|
26
20
|
file_name = File.join(RSpecTracer.report_path, 'index.html')
|
27
21
|
|
@@ -37,6 +31,16 @@ module RSpecTracer
|
|
37
31
|
|
38
32
|
private
|
39
33
|
|
34
|
+
def prepare
|
35
|
+
format_last_run
|
36
|
+
format_examples
|
37
|
+
format_duplicate_examples
|
38
|
+
format_flaky_examples
|
39
|
+
format_examples_dependency
|
40
|
+
format_files_dependency
|
41
|
+
copy_assets
|
42
|
+
end
|
43
|
+
|
40
44
|
def copy_assets
|
41
45
|
Dir[File.join(File.dirname(__FILE__), 'public/*')].each do |path|
|
42
46
|
FileUtils.cp_r(path, asset_output_path)
|
@@ -46,6 +50,7 @@ module RSpecTracer
|
|
46
50
|
def format_last_run
|
47
51
|
@last_run = @reporter.last_run.slice(
|
48
52
|
:actual_count,
|
53
|
+
:duplicate_examples,
|
49
54
|
:failed_examples,
|
50
55
|
:pending_examples,
|
51
56
|
:skipped_examples
|
@@ -60,13 +65,39 @@ module RSpecTracer
|
|
60
65
|
id: example_id,
|
61
66
|
description: example[:full_description],
|
62
67
|
location: example_location(example[:rerun_file_name], example[:rerun_line_number]),
|
63
|
-
status: example[:run_reason] || 'Skipped'
|
68
|
+
status: example[:run_reason] || 'Skipped'
|
69
|
+
}.merge(example_result(example_id, example))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def example_result(example_id, example)
|
74
|
+
if example[:execution_result].nil?
|
75
|
+
{
|
76
|
+
result: @reporter.example_interrupted?(example_id) ? 'Interrupted' : '_',
|
77
|
+
last_run: '_'
|
78
|
+
}
|
79
|
+
else
|
80
|
+
{
|
64
81
|
result: example[:execution_result][:status].capitalize,
|
65
82
|
last_run: example_run_local_time(example[:execution_result][:finished_at])
|
66
83
|
}
|
67
84
|
end
|
68
85
|
end
|
69
86
|
|
87
|
+
def format_duplicate_examples
|
88
|
+
@duplicate_examples = []
|
89
|
+
|
90
|
+
@reporter.duplicate_examples.each_pair do |example_id, examples|
|
91
|
+
examples.each do |example|
|
92
|
+
@duplicate_examples << {
|
93
|
+
id: example_id,
|
94
|
+
description: example[:full_description],
|
95
|
+
location: example_location(example[:rerun_file_name], example[:rerun_line_number])
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
70
101
|
def format_flaky_examples
|
71
102
|
@flaky_examples = @examples.slice(*@reporter.flaky_examples).values
|
72
103
|
end
|
@@ -142,6 +173,14 @@ module RSpecTracer
|
|
142
173
|
template(title_id).result(current_binding)
|
143
174
|
end
|
144
175
|
|
176
|
+
def formatted_duplicate_examples(title, duplicate_examples)
|
177
|
+
title_id = report_container_id(title)
|
178
|
+
current_binding = binding
|
179
|
+
|
180
|
+
current_binding.local_variable_set(:title_id, title_id)
|
181
|
+
template(title_id).result(current_binding)
|
182
|
+
end
|
183
|
+
|
145
184
|
def formatted_flaky_examples(title, flaky_examples)
|
146
185
|
title_id = report_container_id(title)
|
147
186
|
current_binding = binding
|
@@ -176,7 +215,7 @@ module RSpecTracer
|
|
176
215
|
|
177
216
|
def example_status_css_class(example_status)
|
178
217
|
case example_status.split.first
|
179
|
-
when 'Failed', 'Flaky'
|
218
|
+
when 'Failed', 'Flaky', 'Interrupted'
|
180
219
|
'red'
|
181
220
|
when 'Pending'
|
182
221
|
'yellow'
|
@@ -189,7 +228,7 @@ module RSpecTracer
|
|
189
228
|
case example_result
|
190
229
|
when 'Passed'
|
191
230
|
'green'
|
192
|
-
when 'Failed'
|
231
|
+
when 'Failed', 'Interrupted'
|
193
232
|
'red'
|
194
233
|
when 'Pending'
|
195
234
|
'yellow'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<div class="report_container" id="<%= title_id %>">
|
2
|
+
<h2>
|
3
|
+
<span class="group_name"><%= title %></span>
|
4
|
+
(
|
5
|
+
<span class="blue">
|
6
|
+
<strong><%= duplicate_examples.count %></strong>
|
7
|
+
</span> examples
|
8
|
+
)
|
9
|
+
</h2>
|
10
|
+
|
11
|
+
<a name="<%= title_id %>"></a>
|
12
|
+
|
13
|
+
<div class="report-table--responsive">
|
14
|
+
<table class="report-table">
|
15
|
+
<thead>
|
16
|
+
<tr>
|
17
|
+
<th>ID</th>
|
18
|
+
<th>Description</th>
|
19
|
+
<th>Location</th>
|
20
|
+
</tr>
|
21
|
+
</thead>
|
22
|
+
|
23
|
+
<tbody>
|
24
|
+
<% duplicate_examples.each do |example| %>
|
25
|
+
<tr>
|
26
|
+
<td><%= example[:id] %></td>
|
27
|
+
<td><%= example[:description] %></td>
|
28
|
+
<td><%= example[:location] %></td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
</tbody>
|
32
|
+
</table>
|
33
|
+
</div>
|
34
|
+
</div>
|
@@ -5,6 +5,11 @@
|
|
5
5
|
<span class="blue">
|
6
6
|
<strong><%= last_run[:actual_count] %></strong>
|
7
7
|
</span> examples,
|
8
|
+
<% if last_run[:duplicate_examples].positive? %>
|
9
|
+
<span class="blue">
|
10
|
+
<strong><%= last_run[:duplicate_examples] %></strong>
|
11
|
+
</span> duplicates,
|
12
|
+
<% end %>
|
8
13
|
<% if last_run[:failed_examples].positive? %>
|
9
14
|
<span class="red">
|
10
15
|
<strong><%= last_run[:failed_examples] %></strong>
|
@@ -18,12 +18,15 @@
|
|
18
18
|
<ul class="group_tabs"></ul>
|
19
19
|
|
20
20
|
<div id="content">
|
21
|
-
<%= formatted_examples('Examples', examples.values) %>
|
22
|
-
<% unless
|
23
|
-
<%=
|
21
|
+
<%= formatted_examples('Examples', @examples.values) %>
|
22
|
+
<% unless @duplicate_examples.empty? %>
|
23
|
+
<%= formatted_duplicate_examples('Duplicate Examples', @duplicate_examples) %>
|
24
24
|
<% end %>
|
25
|
-
|
26
|
-
|
25
|
+
<% unless @flaky_examples.empty? %>
|
26
|
+
<%= formatted_flaky_examples('Flaky Examples', @flaky_examples) %>
|
27
|
+
<% end %>
|
28
|
+
<%= formatted_examples_dependency('Examples Dependency', @examples_dependency) %>
|
29
|
+
<%= formatted_files_dependency("Files Dependency", @files_dependency) %>
|
27
30
|
</div>
|
28
31
|
|
29
32
|
<div id="footer">
|
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
module RSpecTracer
|
4
4
|
class Reporter
|
5
|
-
attr_reader :all_examples, :
|
6
|
-
:
|
7
|
-
:
|
5
|
+
attr_reader :all_examples, :interrupted_examples, :duplicate_examples,
|
6
|
+
:possibly_flaky_examples, :flaky_examples, :pending_examples,
|
7
|
+
:all_files, :modified_files, :deleted_files, :dependency,
|
8
|
+
:reverse_dependency, :examples_coverage, :last_run
|
8
9
|
|
9
10
|
def initialize
|
10
11
|
initialize_examples
|
@@ -15,6 +16,15 @@ module RSpecTracer
|
|
15
16
|
|
16
17
|
def register_example(example)
|
17
18
|
@all_examples[example[:example_id]] = example
|
19
|
+
@duplicate_examples[example[:example_id]] << example
|
20
|
+
end
|
21
|
+
|
22
|
+
def deregister_duplicate_examples
|
23
|
+
@duplicate_examples.select! { |_, examples| examples.count > 1 }
|
24
|
+
|
25
|
+
return if @duplicate_examples.empty?
|
26
|
+
|
27
|
+
@all_examples.reject! { |example_id, _| @duplicate_examples.key?(example_id) }
|
18
28
|
end
|
19
29
|
|
20
30
|
def on_example_skipped(example_id)
|
@@ -22,22 +32,41 @@ module RSpecTracer
|
|
22
32
|
end
|
23
33
|
|
24
34
|
def on_example_passed(example_id, result)
|
35
|
+
return if @duplicate_examples.key?(example_id)
|
36
|
+
|
25
37
|
@passed_examples << example_id
|
26
38
|
@all_examples[example_id][:execution_result] = formatted_execution_result(result)
|
27
39
|
end
|
28
40
|
|
29
41
|
def on_example_failed(example_id, result)
|
42
|
+
return if @duplicate_examples.key?(example_id)
|
43
|
+
|
30
44
|
@failed_examples << example_id
|
31
45
|
@all_examples[example_id][:execution_result] = formatted_execution_result(result)
|
32
46
|
end
|
33
47
|
|
34
48
|
def on_example_pending(example_id, result)
|
49
|
+
return if @duplicate_examples.key?(example_id)
|
50
|
+
|
35
51
|
@pending_examples << example_id
|
36
52
|
@all_examples[example_id][:execution_result] = formatted_execution_result(result)
|
37
53
|
end
|
38
54
|
|
55
|
+
def register_interrupted_examples
|
56
|
+
@all_examples.each_pair do |example_id, example|
|
57
|
+
next if example.key?(:execution_result)
|
58
|
+
|
59
|
+
@interrupted_examples << example_id
|
60
|
+
end
|
61
|
+
|
62
|
+
return if @interrupted_examples.empty?
|
63
|
+
|
64
|
+
puts "RSpec tracer is not processing #{@interrupted_examples.count} interrupted examples"
|
65
|
+
end
|
66
|
+
|
39
67
|
def register_deleted_examples(seen_examples)
|
40
68
|
@deleted_examples = seen_examples.keys.to_set - (@skipped_examples | @all_examples.keys)
|
69
|
+
@deleted_examples -= @interrupted_examples
|
41
70
|
|
42
71
|
@deleted_examples.select! do |example_id|
|
43
72
|
example = seen_examples[example_id]
|
@@ -62,6 +91,14 @@ module RSpecTracer
|
|
62
91
|
@pending_examples << example_id
|
63
92
|
end
|
64
93
|
|
94
|
+
def duplicate_example?(example_id)
|
95
|
+
@duplicate_examples.key?(example_id)
|
96
|
+
end
|
97
|
+
|
98
|
+
def example_interrupted?(example_id)
|
99
|
+
@interrupted_examples.include?(example_id)
|
100
|
+
end
|
101
|
+
|
65
102
|
def example_passed?(example_id)
|
66
103
|
@passed_examples.include?(example_id)
|
67
104
|
end
|
@@ -116,6 +153,8 @@ module RSpecTracer
|
|
116
153
|
|
117
154
|
def generate_reverse_dependency_report
|
118
155
|
@dependency.each_pair do |example_id, files|
|
156
|
+
next if @interrupted_examples.include?(example_id)
|
157
|
+
|
119
158
|
example_file = @all_examples[example_id][:rerun_file_name]
|
120
159
|
|
121
160
|
files.each do |file_name|
|
@@ -129,13 +168,15 @@ module RSpecTracer
|
|
129
168
|
|
130
169
|
def generate_last_run_report
|
131
170
|
@last_run = {
|
132
|
-
run_id: @run_id,
|
133
171
|
pid: RSpecTracer.pid,
|
134
172
|
actual_count: RSpec.world.example_count + @skipped_examples.count,
|
135
173
|
example_count: RSpec.world.example_count,
|
174
|
+
duplicate_examples: @duplicate_examples.sum { |_, examples| examples.count },
|
175
|
+
interrupted_examples: @interrupted_examples.count,
|
136
176
|
failed_examples: @failed_examples.count,
|
137
177
|
skipped_examples: @skipped_examples.count,
|
138
|
-
pending_examples: @pending_examples.count
|
178
|
+
pending_examples: @pending_examples.count,
|
179
|
+
flaky_examples: @flaky_examples.count
|
139
180
|
}
|
140
181
|
end
|
141
182
|
|
@@ -165,10 +206,43 @@ module RSpecTracer
|
|
165
206
|
puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
|
166
207
|
end
|
167
208
|
|
209
|
+
# rubocop:disable Metrics/AbcSize
|
210
|
+
def print_duplicate_examples
|
211
|
+
return if @duplicate_examples.empty?
|
212
|
+
|
213
|
+
total = @duplicate_examples.sum { |_, examples| examples.count }
|
214
|
+
|
215
|
+
puts '=' * 80
|
216
|
+
puts ' IMPORTANT NOTICE -- RSPEC TRACER COULD NOT IDENTIFY SOME EXAMPLES UNIQUELY'
|
217
|
+
puts '=' * 80
|
218
|
+
puts "RSpec tracer could not uniquely identify the following #{total} examples:"
|
219
|
+
|
220
|
+
justify = ' ' * 2
|
221
|
+
nested_justify = justify * 3
|
222
|
+
|
223
|
+
@duplicate_examples.each_pair do |example_id, examples|
|
224
|
+
puts "#{justify}- Example ID: #{example_id} (#{examples.count} examples)"
|
225
|
+
|
226
|
+
examples.each do |example|
|
227
|
+
description = example[:full_description].strip
|
228
|
+
file_name = example[:rerun_file_name].sub(%r{^/}, '')
|
229
|
+
line_number = example[:rerun_line_number]
|
230
|
+
location = "#{file_name}:#{line_number}"
|
231
|
+
|
232
|
+
puts "#{nested_justify}* #{description} (#{location})"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
puts
|
237
|
+
end
|
238
|
+
# rubocop:enable Metrics/AbcSize
|
239
|
+
|
168
240
|
private
|
169
241
|
|
170
242
|
def initialize_examples
|
171
243
|
@all_examples = {}
|
244
|
+
@duplicate_examples = Hash.new { |examples, example_id| examples[example_id] = [] }
|
245
|
+
@interrupted_examples = Set.new
|
172
246
|
@passed_examples = Set.new
|
173
247
|
@possibly_flaky_examples = Set.new
|
174
248
|
@flaky_examples = Set.new
|
@@ -3,13 +3,23 @@
|
|
3
3
|
module RSpecTracer
|
4
4
|
module RSpecRunner
|
5
5
|
# rubocop:disable Metrics/AbcSize
|
6
|
-
def run_specs(
|
6
|
+
def run_specs(example_groups)
|
7
7
|
actual_count = RSpec.world.example_count
|
8
|
+
RSpecTracer.no_examples = actual_count.zero?
|
9
|
+
|
10
|
+
if RSpecTracer.no_examples
|
11
|
+
RSpecTracer.running = true
|
12
|
+
|
13
|
+
super(example_groups)
|
14
|
+
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
8
18
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
9
|
-
filtered_examples,
|
19
|
+
filtered_examples, filtered_example_groups = RSpecTracer.filter_examples
|
10
20
|
|
11
21
|
RSpec.world.instance_variable_set(:@filtered_examples, filtered_examples)
|
12
|
-
RSpec.world.instance_variable_set(:@example_groups,
|
22
|
+
RSpec.world.instance_variable_set(:@example_groups, filtered_example_groups)
|
13
23
|
|
14
24
|
current_count = RSpec.world.example_count
|
15
25
|
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -23,7 +33,7 @@ module RSpecTracer
|
|
23
33
|
|
24
34
|
RSpecTracer.running = true
|
25
35
|
|
26
|
-
super(
|
36
|
+
super(filtered_example_groups)
|
27
37
|
end
|
28
38
|
# rubocop:enable Metrics/AbcSize
|
29
39
|
end
|
data/lib/rspec_tracer/runner.rb
CHANGED
@@ -8,6 +8,7 @@ module RSpecTracer
|
|
8
8
|
EXAMPLE_RUN_REASON = {
|
9
9
|
explicit_run: 'Explicit run',
|
10
10
|
no_cache: 'No cache',
|
11
|
+
interrupted: 'Interrupted previously',
|
11
12
|
flaky_example: 'Flaky example',
|
12
13
|
failed_example: 'Failed previously',
|
13
14
|
pending_example: 'Pending previously',
|
@@ -43,6 +44,10 @@ module RSpecTracer
|
|
43
44
|
@reporter.register_example(example)
|
44
45
|
end
|
45
46
|
|
47
|
+
def deregister_duplicate_examples
|
48
|
+
@reporter.deregister_duplicate_examples
|
49
|
+
end
|
50
|
+
|
46
51
|
def on_example_skipped(example_id)
|
47
52
|
@reporter.on_example_skipped(example_id)
|
48
53
|
end
|
@@ -59,6 +64,10 @@ module RSpecTracer
|
|
59
64
|
@reporter.on_example_pending(example_id, execution_result)
|
60
65
|
end
|
61
66
|
|
67
|
+
def register_interrupted_examples
|
68
|
+
@reporter.register_interrupted_examples
|
69
|
+
end
|
70
|
+
|
62
71
|
def register_deleted_examples
|
63
72
|
@reporter.register_deleted_examples(@cache.all_examples)
|
64
73
|
end
|
@@ -73,6 +82,9 @@ module RSpecTracer
|
|
73
82
|
|
74
83
|
@cache.cached_examples_coverage.each_pair do |example_id, example_coverage|
|
75
84
|
example_coverage.each_pair do |file_path, line_coverage|
|
85
|
+
next if @reporter.example_interrupted?(example_id) ||
|
86
|
+
@reporter.duplicate_example?(example_id)
|
87
|
+
|
76
88
|
next unless @reporter.example_skipped?(example_id)
|
77
89
|
|
78
90
|
file_name = RSpecTracer::SourceFile.file_name(file_path)
|
@@ -93,6 +105,9 @@ module RSpecTracer
|
|
93
105
|
filtered_files = Set.new
|
94
106
|
|
95
107
|
examples_coverage.each_pair do |example_id, example_coverage|
|
108
|
+
next if @reporter.example_interrupted?(example_id) ||
|
109
|
+
@reporter.duplicate_example?(example_id)
|
110
|
+
|
96
111
|
register_example_files_dependency(example_id)
|
97
112
|
|
98
113
|
example_coverage.each_key do |file_path|
|
@@ -118,6 +133,9 @@ module RSpecTracer
|
|
118
133
|
@reporter.register_source_file(source_file)
|
119
134
|
|
120
135
|
@reporter.all_examples.each_key do |example_id|
|
136
|
+
next if @reporter.example_interrupted?(example_id) ||
|
137
|
+
@reporter.duplicate_example?(example_id)
|
138
|
+
|
121
139
|
@reporter.register_dependency(example_id, source_file[:file_name])
|
122
140
|
end
|
123
141
|
end
|
@@ -143,6 +161,12 @@ module RSpecTracer
|
|
143
161
|
end
|
144
162
|
|
145
163
|
@reporter.write_reports
|
164
|
+
@reporter.print_duplicate_examples
|
165
|
+
end
|
166
|
+
|
167
|
+
def non_zero_exit_code?
|
168
|
+
!@reporter.duplicate_examples.empty? &&
|
169
|
+
ENV.fetch('RSPEC_TRACER_FAIL_ON_DUPLICATES', 'true') == 'true'
|
146
170
|
end
|
147
171
|
|
148
172
|
private
|
@@ -165,6 +189,7 @@ module RSpecTracer
|
|
165
189
|
end
|
166
190
|
|
167
191
|
def filter_by_example_status
|
192
|
+
add_previously_interrupted_examples
|
168
193
|
add_previously_flaky_examples
|
169
194
|
add_previously_failed_examples
|
170
195
|
add_previously_pending_examples
|
@@ -179,10 +204,17 @@ module RSpecTracer
|
|
179
204
|
end
|
180
205
|
end
|
181
206
|
|
207
|
+
def add_previously_interrupted_examples
|
208
|
+
@cache.interrupted_examples.each do |example_id|
|
209
|
+
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:interrupted]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
182
213
|
def add_previously_flaky_examples
|
183
214
|
@cache.flaky_examples.each do |example_id|
|
184
215
|
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:flaky_example]
|
185
216
|
|
217
|
+
next unless @cache.dependency.key?(example_id)
|
186
218
|
next unless (@changed_files & @cache.dependency[example_id]).empty?
|
187
219
|
|
188
220
|
@reporter.register_possibly_flaky_example(example_id)
|
@@ -195,6 +227,7 @@ module RSpecTracer
|
|
195
227
|
|
196
228
|
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:failed_example]
|
197
229
|
|
230
|
+
next unless @cache.dependency.key?(example_id)
|
198
231
|
next unless (@changed_files & @cache.dependency[example_id]).empty?
|
199
232
|
|
200
233
|
@reporter.register_possibly_flaky_example(example_id)
|
@@ -243,6 +276,9 @@ module RSpecTracer
|
|
243
276
|
end
|
244
277
|
|
245
278
|
def register_example_files_dependency(example_id)
|
279
|
+
return if @reporter.example_interrupted?(example_id) ||
|
280
|
+
@reporter.duplicate_example?(example_id)
|
281
|
+
|
246
282
|
example = @reporter.all_examples[example_id]
|
247
283
|
|
248
284
|
register_example_file_dependency(example_id, example[:file_name])
|
@@ -253,6 +289,9 @@ module RSpecTracer
|
|
253
289
|
end
|
254
290
|
|
255
291
|
def register_example_file_dependency(example_id, file_name)
|
292
|
+
return if @reporter.example_interrupted?(example_id) ||
|
293
|
+
@reporter.duplicate_example?(example_id)
|
294
|
+
|
256
295
|
source_file = RSpecTracer::SourceFile.from_name(file_name)
|
257
296
|
|
258
297
|
@reporter.register_source_file(source_file)
|
@@ -260,6 +299,9 @@ module RSpecTracer
|
|
260
299
|
end
|
261
300
|
|
262
301
|
def register_file_dependency(example_id, file_path)
|
302
|
+
return if @reporter.example_interrupted?(example_id) ||
|
303
|
+
@reporter.duplicate_example?(example_id)
|
304
|
+
|
263
305
|
source_file = RSpecTracer::SourceFile.from_path(file_path)
|
264
306
|
|
265
307
|
return false if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
|
data/lib/rspec_tracer/version.rb
CHANGED
data/lib/rspec_tracer.rb
CHANGED
@@ -28,7 +28,7 @@ require_relative 'rspec_tracer/version'
|
|
28
28
|
|
29
29
|
module RSpecTracer
|
30
30
|
class << self
|
31
|
-
attr_accessor :running, :pid
|
31
|
+
attr_accessor :running, :pid, :no_examples
|
32
32
|
|
33
33
|
def start(&block)
|
34
34
|
RSpecTracer.running = false
|
@@ -67,6 +67,8 @@ module RSpecTracer
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
+
runner.deregister_duplicate_examples
|
71
|
+
|
70
72
|
[to_run, groups.to_a]
|
71
73
|
end
|
72
74
|
# rubocop:enable Metrics/AbcSize
|
@@ -75,6 +77,10 @@ module RSpecTracer
|
|
75
77
|
return unless RSpecTracer.pid == Process.pid && RSpecTracer.running
|
76
78
|
|
77
79
|
run_exit_tasks
|
80
|
+
|
81
|
+
::Kernel.exit(1) if runner.non_zero_exit_code?
|
82
|
+
ensure
|
83
|
+
RSpecTracer.running = false
|
78
84
|
end
|
79
85
|
|
80
86
|
def start_example_trace
|
@@ -154,15 +160,11 @@ module RSpecTracer
|
|
154
160
|
def setup_coverage
|
155
161
|
@simplecov = defined?(SimpleCov) && SimpleCov.running
|
156
162
|
|
157
|
-
if simplecov?
|
158
|
-
# rubocop:disable Lint/EmptyBlock
|
159
|
-
SimpleCov.at_exit {}
|
160
|
-
# rubocop:enable Lint/EmptyBlock
|
161
|
-
else
|
162
|
-
require 'coverage'
|
163
|
+
return if simplecov?
|
163
164
|
|
164
|
-
|
165
|
-
|
165
|
+
require 'coverage'
|
166
|
+
|
167
|
+
::Coverage.start
|
166
168
|
end
|
167
169
|
|
168
170
|
def setup_trace_point
|
@@ -175,11 +177,13 @@ module RSpecTracer
|
|
175
177
|
end
|
176
178
|
|
177
179
|
def run_exit_tasks
|
178
|
-
|
180
|
+
if RSpecTracer.no_examples
|
181
|
+
puts 'Skipped reports generation since all examples were filtered out'
|
182
|
+
else
|
183
|
+
generate_reports
|
184
|
+
end
|
179
185
|
|
180
186
|
simplecov? ? run_simplecov_exit_task : run_coverage_exit_task
|
181
|
-
ensure
|
182
|
-
RSpecTracer.running = false
|
183
187
|
end
|
184
188
|
|
185
189
|
def generate_reports
|
@@ -194,6 +198,7 @@ module RSpecTracer
|
|
194
198
|
def process_dependency
|
195
199
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
196
200
|
|
201
|
+
runner.register_interrupted_examples
|
197
202
|
runner.register_deleted_examples
|
198
203
|
runner.register_dependency(coverage_reporter.examples_coverage)
|
199
204
|
runner.register_untraced_dependency(@traced_files)
|
@@ -224,12 +229,13 @@ module RSpecTracer
|
|
224
229
|
|
225
230
|
puts 'SimpleCov will now generate coverage report (<3 RSpec tracer)'
|
226
231
|
|
227
|
-
|
232
|
+
coverage_reporter.record_coverage if RSpecTracer.no_examples
|
228
233
|
end
|
229
234
|
|
230
235
|
def run_coverage_exit_task
|
231
236
|
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
232
237
|
|
238
|
+
coverage_reporter.record_coverage if RSpecTracer.no_examples
|
233
239
|
coverage_reporter.generate_final_coverage
|
234
240
|
|
235
241
|
file_name = File.join(RSpecTracer.coverage_path, 'coverage.json')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-tracer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abhimanyu Singh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: docile
|
@@ -50,9 +50,11 @@ dependencies:
|
|
50
50
|
- - ">="
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: 3.6.0
|
53
|
-
description: RSpec Tracer is a specs dependency
|
54
|
-
RSpec. It maintains a list of files
|
55
|
-
in the subsequent runs if none of the
|
53
|
+
description: RSpec Tracer is a specs dependency analyzer, flaky tests detector, tests
|
54
|
+
accelerator, and coverage reporter tool for RSpec. It maintains a list of files
|
55
|
+
for each test, enabling itself to skip tests in the subsequent runs if none of the
|
56
|
+
dependent files are changed. It uses Ruby's built-in coverage library to keep track
|
57
|
+
of the coverage for each test.
|
56
58
|
email:
|
57
59
|
- abhisinghabhimanyu@gmail.com
|
58
60
|
executables: []
|
@@ -88,6 +90,7 @@ files:
|
|
88
90
|
- lib/rspec_tracer/html_reporter/public/favicon.png
|
89
91
|
- lib/rspec_tracer/html_reporter/public/loading.gif
|
90
92
|
- lib/rspec_tracer/html_reporter/reporter.rb
|
93
|
+
- lib/rspec_tracer/html_reporter/views/duplicate_examples.erb
|
91
94
|
- lib/rspec_tracer/html_reporter/views/examples.erb
|
92
95
|
- lib/rspec_tracer/html_reporter/views/examples_dependency.erb
|
93
96
|
- lib/rspec_tracer/html_reporter/views/files_dependency.erb
|
@@ -111,7 +114,7 @@ licenses:
|
|
111
114
|
- MIT
|
112
115
|
metadata:
|
113
116
|
homepage_uri: https://github.com/avmnu-sng/rspec-tracer
|
114
|
-
source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.
|
117
|
+
source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.9.3
|
115
118
|
changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
|
116
119
|
bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
|
117
120
|
post_install_message:
|
@@ -132,5 +135,6 @@ requirements: []
|
|
132
135
|
rubygems_version: 3.2.26
|
133
136
|
signing_key:
|
134
137
|
specification_version: 4
|
135
|
-
summary: RSpec Tracer is a specs dependency
|
138
|
+
summary: RSpec Tracer is a specs dependency analyzer, flaky tests detector, tests
|
139
|
+
accelerator, and coverage reporter tool.
|
136
140
|
test_files: []
|