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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 675be2bd6603501deb282508ea234808f0bbdf60c2b626611919f84c32bb4ba9
4
- data.tar.gz: 591be21b4e97c50f33385111ac758665a97e83ca82e154cd3b86d32133f7ef52
3
+ metadata.gz: 91a840ac79a148f2203602abe8f3c0d3b2019565532edd3ac04990b7ca7f1035
4
+ data.tar.gz: 0cd0bd13178616e79730c9f062098f8425964507fddd704f8c8725d2cad33050
5
5
  SHA512:
6
- metadata.gz: 1ab8ca453da65dd7185fa4869633d9b9ab85fb5c73cfb45929928a492d58d0e16d24e3bcef2466ceae4408c7e1ad2e5ab3f10137061ab684b1decf885809607d
7
- data.tar.gz: 8dc1d3078f2c16c8ee5142c03d05222dd0bd458c979bca0a35ee1b1e44fead38ed2c1a6d84795a93f2a4d7a15c58da40562f4dae360f22e65f1518fc0ddecf75
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
  ![](./readme_files/examples_report_next_run.png)
65
64
 
65
+ ### Duplicate Examples Report
66
+
67
+ These reports provide duplicate tests information.
68
+
69
+ ![](./readme_files/duplicate_examples_report.png)
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.7', group: :test, require: false
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
- Currently using RSpec Tracer with SimpleCov has the following two limitations:
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 path), a block or by passing in your
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
- This simple regex filter will remove all files that start with /helper/ in their path.
282
-
283
- #### Block Filter
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
- ```ruby
286
- RSpecTracer.start do
287
- add_filter do |source_file|
288
- source_file[:file_path].include?('/helpers/')
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
- end
291
- ```
282
+ ```
292
283
 
293
- Block filters receive a `Hash` object and expect your block to return either true
294
- (if the file is to be removed from the result) or false (if the result should be kept).
295
- In the above example, the filter will remove all files that match "/helpers/" in their path.
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
- #### Array Filter
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
- ```ruby
300
- RSpecTracer.start do
301
- add_filter ['/helpers/', %r{^/utils/}]
302
- end
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).
@@ -2,8 +2,8 @@
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, :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[:execution_result].transform_keys!(&:to_sym)
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
- copy_assets
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 flaky_examples.empty? %>
23
- <%= formatted_flaky_examples('Flaky Examples', flaky_examples) %>
21
+ <%= formatted_examples('Examples', @examples.values) %>
22
+ <% unless @duplicate_examples.empty? %>
23
+ <%= formatted_duplicate_examples('Duplicate Examples', @duplicate_examples) %>
24
24
  <% end %>
25
- <%= formatted_examples_dependency('Examples Dependency', examples_dependency) %>
26
- <%= formatted_files_dependency("Files Dependency", files_dependency) %>
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, :possibly_flaky_examples, :flaky_examples, :pending_examples,
6
- :all_files, :modified_files, :deleted_files, :dependency, :reverse_dependency,
7
- :examples_coverage, :last_run
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(_example_groups)
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, example_groups = RSpecTracer.filter_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, 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(example_groups)
36
+ super(filtered_example_groups)
27
37
  end
28
38
  # rubocop:enable Metrics/AbcSize
29
39
  end
@@ -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) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '0.8.0'
4
+ VERSION = '0.9.3'
5
5
  end
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
- ::Coverage.start
165
- end
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
- generate_reports
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
- SimpleCov.result.format!
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.8.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-09-13 00:00:00.000000000 Z
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 analysis tool and a test skipper for
54
- RSpec. It maintains a list of files for each test, enabling itself to skip tests
55
- in the subsequent runs if none of the dependent files are changed.
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.8.0
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 analysis tool and a test skipper for RSpec
138
+ summary: RSpec Tracer is a specs dependency analyzer, flaky tests detector, tests
139
+ accelerator, and coverage reporter tool.
136
140
  test_files: []