rspec-tracer 0.8.0 → 0.9.3

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: 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: []