rspec-tracer 0.5.0 → 0.6.0
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 +14 -0
- data/README.md +34 -6
- data/lib/rspec_tracer/cache.rb +27 -3
- data/lib/rspec_tracer/defaults.rb +7 -1
- data/lib/rspec_tracer/html_reporter/reporter.rb +28 -6
- data/lib/rspec_tracer/html_reporter/views/flaky_examples.erb +38 -0
- data/lib/rspec_tracer/html_reporter/views/layout.erb +3 -0
- data/lib/rspec_tracer/remote_cache/cache.rb +1 -1
- data/lib/rspec_tracer/reporter.rb +40 -11
- data/lib/rspec_tracer/rspec_runner.rb +6 -1
- data/lib/rspec_tracer/runner.rb +84 -34
- data/lib/rspec_tracer/time_formatter.rb +52 -0
- data/lib/rspec_tracer/version.rb +1 -1
- data/lib/rspec_tracer.rb +29 -7
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 885e868cf847ca1bab4ba2243cd4cd6a4bc3f936a3c8c10f677146433acc2068
|
4
|
+
data.tar.gz: d60fcdeef0067a37faec3ea876ffa519bdd7039a1765ad59e551427bcf2c8e29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cffa43507448703f4471ed2a463e7ef486f105b06560663d307b21c3303368b0ae0260bb36bbc392f900bfb0e54482fdc8055d231ab75fb33e5a93360be46ac9
|
7
|
+
data.tar.gz: d59157d318beda7c81a65cdd1a38ff3d849521a502e31c6c805916ace8f6f3568a5114f201bc03cdb33e63cae3c0a86ae5e81dd148c1c32f735d7d5d3cea5750
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## [0.6.0] - 2021-09-05
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- Improved dependency change detection (#18)
|
6
|
+
- Flaky tests detection (#19)
|
7
|
+
- Exclude vendor files from analysis (#21)
|
8
|
+
- Report elapsed time at various stages (#23)
|
9
|
+
|
10
|
+
### Note
|
11
|
+
|
12
|
+
The first run on this version will not use any cache on the CI because the number
|
13
|
+
of files changed from eight to nine, so there will be no appropriate cache to use.
|
14
|
+
|
1
15
|
## [0.5.0] - 2021-09-03
|
2
16
|
|
3
17
|
### Fixed
|
data/README.md
CHANGED
@@ -8,7 +8,11 @@ It uses [Ruby's built-in coverage library](https://ruby-doc.org/stdlib/libdoc/co
|
|
8
8
|
to keep track of the coverage for each test. For each test executed, the coverage
|
9
9
|
diff provides the desired file list. RSpec Tracer takes care of reporting the
|
10
10
|
**correct code coverage when skipping tests** by using the cached reports. Also,
|
11
|
-
note that it will **never skip
|
11
|
+
note that it will **never skip**:
|
12
|
+
|
13
|
+
- **Flaky examples**
|
14
|
+
- **Failed examples**
|
15
|
+
- **Pending examples**
|
12
16
|
|
13
17
|
Knowing the examples and files dependency gives us a better insight into the codebase,
|
14
18
|
and we have **a clear idea of what to test for when making any changes**. With this data,
|
@@ -16,9 +20,10 @@ we can also analyze the coupling between different components and much more.
|
|
16
20
|
|
17
21
|
## Note
|
18
22
|
|
19
|
-
You should take some time and go through the [document](./RSPEC_TRACER.md) describing
|
20
|
-
the **intention** and implementation details of **
|
21
|
-
and **caching on CI
|
23
|
+
You should take some time and go through the **[document](./RSPEC_TRACER.md)** describing
|
24
|
+
the **intention** and implementation details of **managing dependency**, **managing flaky tests**,
|
25
|
+
**skipping tests**, and **caching on CI**. You must go through the README file before
|
26
|
+
integrating the gem into your project to better understand what is happening.
|
22
27
|
|
23
28
|
## Table of Contents
|
24
29
|
|
@@ -28,6 +33,7 @@ and **caching on CI**, etc.
|
|
28
33
|
* [Additional Tools](#additional-tools)
|
29
34
|
* [Getting Started](#getting-started)
|
30
35
|
* [Environment Variables](#environment-variables)
|
36
|
+
* [BUNDLE_PATH](#bundle_path)
|
31
37
|
* [CI](#ci)
|
32
38
|
* [LOCAL_AWS](#local_aws)
|
33
39
|
* [RSPEC_TRACER_NO_SKIP](#rspec_tracer_no_skip)
|
@@ -37,6 +43,7 @@ and **caching on CI**, etc.
|
|
37
43
|
* [TEST_SUITE_ID](#test_suite_id)
|
38
44
|
* [Sample Reports](#sample-reports)
|
39
45
|
* [Examples](#examples)
|
46
|
+
* [Flaky Examples](#flaky-examples)
|
40
47
|
* [Examples Dependency](#examples-dependency)
|
41
48
|
* [Files Dependency](#files-dependency)
|
42
49
|
* [Configuring RSpec Tracer](#configuring-rspec-tracer)
|
@@ -142,7 +149,15 @@ browser of your choice.
|
|
142
149
|
|
143
150
|
## Environment Variables
|
144
151
|
|
145
|
-
To get better control on execution, you can use the following
|
152
|
+
To get better control on execution, you can use the following environment variables
|
153
|
+
whenever required.
|
154
|
+
|
155
|
+
### BUNDLE_PATH
|
156
|
+
|
157
|
+
Since the bundler uses a vendor directory inside the project, it might cause slowness
|
158
|
+
depending on the vendor size. You can configure the bundle path outside of the project
|
159
|
+
using `BUNDLE_PATH` environment variable, for example, `BUNDLE_PATH=$HOME/vendor/bundle`.
|
160
|
+
Make sure to cache this directory in the CI configuration.
|
146
161
|
|
147
162
|
### CI
|
148
163
|
|
@@ -190,7 +205,7 @@ export TEST_SUITES=8
|
|
190
205
|
|
191
206
|
If you have a large set of tests to run, it is recommended to run them in
|
192
207
|
separate groups. This way, RSpec Tracer is not overwhelmed with loading massive
|
193
|
-
cached data in the memory. Also, it
|
208
|
+
cached data in the memory. Also, it generates and uses cache for specific test suites
|
194
209
|
and not merge them.
|
195
210
|
|
196
211
|
```ruby
|
@@ -229,6 +244,19 @@ These reports provide basic test information:
|
|
229
244
|
|
230
245
|

|
231
246
|
|
247
|
+
### Flaky Examples
|
248
|
+
|
249
|
+
These reports provide flaky tests information. Assuming **the following two tests
|
250
|
+
failed in the first run.**
|
251
|
+
|
252
|
+
**Next Run**
|
253
|
+
|
254
|
+

|
255
|
+
|
256
|
+
**Another Run**
|
257
|
+
|
258
|
+

|
259
|
+
|
232
260
|
### Examples Dependency
|
233
261
|
|
234
262
|
These reports show a list of dependent files for each test.
|
data/lib/rspec_tracer/cache.rb
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
module RSpecTracer
|
4
4
|
class Cache
|
5
|
-
attr_reader :all_examples, :
|
5
|
+
attr_reader :all_examples, :flaky_examples, :failed_examples, :pending_examples,
|
6
|
+
:all_files, :dependency, :run_id
|
6
7
|
|
7
8
|
def initialize
|
8
9
|
@run_id = last_run_id
|
@@ -11,6 +12,7 @@ module RSpecTracer
|
|
11
12
|
@cached = false
|
12
13
|
|
13
14
|
@all_examples = {}
|
15
|
+
@flaky_examples = Set.new
|
14
16
|
@failed_examples = Set.new
|
15
17
|
@pending_examples = Set.new
|
16
18
|
@all_files = {}
|
@@ -20,22 +22,36 @@ module RSpecTracer
|
|
20
22
|
def load_cache_for_run
|
21
23
|
return if @run_id.nil? || @cached
|
22
24
|
|
25
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
26
|
+
|
23
27
|
load_all_examples_cache
|
28
|
+
load_flaky_examples_cache
|
24
29
|
load_failed_examples_cache
|
25
30
|
load_pending_examples_cache
|
26
31
|
load_all_files_cache
|
27
32
|
load_dependency_cache
|
28
33
|
|
34
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
35
|
+
|
29
36
|
@cached = true
|
30
37
|
|
31
|
-
|
38
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
39
|
+
|
40
|
+
puts "RSpec tracer loaded cache from #{@cache_dir} (took #{elpased})"
|
32
41
|
end
|
33
42
|
|
34
43
|
def cached_examples_coverage
|
35
44
|
return @examples_coverage if defined?(@examples_coverage)
|
36
45
|
return @examples_coverage = {} if @run_id.nil?
|
37
46
|
|
38
|
-
|
47
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
48
|
+
coverage = load_examples_coverage_cache
|
49
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
50
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
51
|
+
|
52
|
+
puts "RSpec tracer loaded cached examples coverage (took #{elpased})"
|
53
|
+
|
54
|
+
coverage
|
39
55
|
end
|
40
56
|
|
41
57
|
private
|
@@ -64,6 +80,14 @@ module RSpecTracer
|
|
64
80
|
end
|
65
81
|
end
|
66
82
|
|
83
|
+
def load_flaky_examples_cache
|
84
|
+
file_name = File.join(@cache_dir, 'flaky_examples.json')
|
85
|
+
|
86
|
+
return unless File.file?(file_name)
|
87
|
+
|
88
|
+
@flaky_examples = JSON.parse(File.read(file_name)).to_set
|
89
|
+
end
|
90
|
+
|
67
91
|
def load_failed_examples_cache
|
68
92
|
file_name = File.join(@cache_dir, 'failed_examples.json')
|
69
93
|
|
@@ -6,21 +6,22 @@ require 'time'
|
|
6
6
|
module RSpecTracer
|
7
7
|
module HTMLReporter
|
8
8
|
class Reporter
|
9
|
-
attr_reader :last_run, :examples, :examples_dependency, :files_dependency
|
9
|
+
attr_reader :last_run, :examples, :flaky_examples, :examples_dependency, :files_dependency
|
10
10
|
|
11
11
|
def initialize
|
12
12
|
@reporter = RSpecTracer.runner.reporter
|
13
13
|
|
14
14
|
format_last_run
|
15
15
|
format_examples
|
16
|
+
format_flaky_examples
|
16
17
|
format_examples_dependency
|
17
18
|
format_files_dependency
|
18
19
|
end
|
19
20
|
|
20
21
|
def generate_report
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
23
|
+
|
24
|
+
copy_assets
|
24
25
|
|
25
26
|
file_name = File.join(RSpecTracer.report_path, 'index.html')
|
26
27
|
|
@@ -28,11 +29,20 @@ module RSpecTracer
|
|
28
29
|
file.puts(template('layout').result(binding))
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
33
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
34
|
+
|
35
|
+
puts "RSpecTracer generated HTML report to #{file_name} (took #{elpased})"
|
32
36
|
end
|
33
37
|
|
34
38
|
private
|
35
39
|
|
40
|
+
def copy_assets
|
41
|
+
Dir[File.join(File.dirname(__FILE__), 'public/*')].each do |path|
|
42
|
+
FileUtils.cp_r(path, asset_output_path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
36
46
|
def format_last_run
|
37
47
|
@last_run = @reporter.last_run.slice(
|
38
48
|
:actual_count,
|
@@ -57,6 +67,10 @@ module RSpecTracer
|
|
57
67
|
end
|
58
68
|
end
|
59
69
|
|
70
|
+
def format_flaky_examples
|
71
|
+
@flaky_examples = @examples.slice(*@reporter.flaky_examples).values
|
72
|
+
end
|
73
|
+
|
60
74
|
def example_run_local_time(utc_time)
|
61
75
|
case utc_time
|
62
76
|
when Time
|
@@ -128,6 +142,14 @@ module RSpecTracer
|
|
128
142
|
template(title_id).result(current_binding)
|
129
143
|
end
|
130
144
|
|
145
|
+
def formatted_flaky_examples(title, flaky_examples)
|
146
|
+
title_id = report_container_id(title)
|
147
|
+
current_binding = binding
|
148
|
+
|
149
|
+
current_binding.local_variable_set(:title_id, title_id)
|
150
|
+
template(title_id).result(current_binding)
|
151
|
+
end
|
152
|
+
|
131
153
|
def formatted_examples_dependency(title, examples_dependency)
|
132
154
|
title_id = report_container_id(title)
|
133
155
|
current_binding = binding
|
@@ -154,7 +176,7 @@ module RSpecTracer
|
|
154
176
|
|
155
177
|
def example_status_css_class(example_status)
|
156
178
|
case example_status.split.first
|
157
|
-
when 'Failed'
|
179
|
+
when 'Failed', 'Flaky'
|
158
180
|
'red'
|
159
181
|
when 'Pending'
|
160
182
|
'yellow'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
<div class="report_container" id="<%= title_id %>">
|
2
|
+
<h2>
|
3
|
+
<span class="group_name"><%= title %></span>
|
4
|
+
(
|
5
|
+
<span class="blue">
|
6
|
+
<strong><%= flaky_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
|
+
<th>Result</th>
|
21
|
+
<th>Run At</th>
|
22
|
+
</tr>
|
23
|
+
</thead>
|
24
|
+
|
25
|
+
<tbody>
|
26
|
+
<% flaky_examples.each do |example| %>
|
27
|
+
<tr>
|
28
|
+
<td><%= example[:id] %></td>
|
29
|
+
<td><%= example[:description] %></td>
|
30
|
+
<td><%= example[:location] %></td>
|
31
|
+
<td><strong class="<%= example_result_css_class(example[:result]) %>"><%= example[:result] %></strong></td>
|
32
|
+
<td width="8%"><%= example[:last_run] %></td>
|
33
|
+
</tr>
|
34
|
+
<% end %>
|
35
|
+
</tbody>
|
36
|
+
</table>
|
37
|
+
</div>
|
38
|
+
</div>
|
@@ -19,6 +19,9 @@
|
|
19
19
|
|
20
20
|
<div id="content">
|
21
21
|
<%= formatted_examples('Examples', examples.values) %>
|
22
|
+
<% unless flaky_examples.empty? %>
|
23
|
+
<%= formatted_flaky_examples('Flaky Examples', flaky_examples) %>
|
24
|
+
<% end %>
|
22
25
|
<%= formatted_examples_dependency('Examples Dependency', examples_dependency) %>
|
23
26
|
<%= formatted_files_dependency("Files Dependency", files_dependency) %>
|
24
27
|
</div>
|
@@ -2,8 +2,9 @@
|
|
2
2
|
|
3
3
|
module RSpecTracer
|
4
4
|
class Reporter
|
5
|
-
attr_reader :all_examples, :
|
6
|
-
:
|
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
|
7
8
|
|
8
9
|
def initialize
|
9
10
|
initialize_examples
|
@@ -21,6 +22,7 @@ module RSpecTracer
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def on_example_passed(example_id, result)
|
25
|
+
@passed_examples << example_id
|
24
26
|
@all_examples[example_id][:execution_result] = formatted_execution_result(result)
|
25
27
|
end
|
26
28
|
|
@@ -44,6 +46,14 @@ module RSpecTracer
|
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
49
|
+
def register_possibly_flaky_example(example_id)
|
50
|
+
@possibly_flaky_examples << example_id
|
51
|
+
end
|
52
|
+
|
53
|
+
def register_flaky_example(example_id)
|
54
|
+
@flaky_examples << example_id
|
55
|
+
end
|
56
|
+
|
47
57
|
def register_failed_example(example_id)
|
48
58
|
@failed_examples << example_id
|
49
59
|
end
|
@@ -52,6 +62,10 @@ module RSpecTracer
|
|
52
62
|
@pending_examples << example_id
|
53
63
|
end
|
54
64
|
|
65
|
+
def example_passed?(example_id)
|
66
|
+
@passed_examples.include?(example_id)
|
67
|
+
end
|
68
|
+
|
55
69
|
def example_skipped?(example_id)
|
56
70
|
@skipped_examples.include?(example_id)
|
57
71
|
end
|
@@ -126,6 +140,8 @@ module RSpecTracer
|
|
126
140
|
end
|
127
141
|
|
128
142
|
def write_reports
|
143
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
144
|
+
|
129
145
|
@run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
|
130
146
|
@cache_dir = File.join(RSpecTracer.cache_path, @run_id)
|
131
147
|
|
@@ -133,6 +149,7 @@ module RSpecTracer
|
|
133
149
|
|
134
150
|
%i[
|
135
151
|
all_examples
|
152
|
+
flaky_examples
|
136
153
|
failed_examples
|
137
154
|
pending_examples
|
138
155
|
all_files
|
@@ -142,13 +159,19 @@ module RSpecTracer
|
|
142
159
|
last_run
|
143
160
|
].each { |report_type| send("write_#{report_type}_report") }
|
144
161
|
|
145
|
-
|
162
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
163
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
164
|
+
|
165
|
+
puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
|
146
166
|
end
|
147
167
|
|
148
168
|
private
|
149
169
|
|
150
170
|
def initialize_examples
|
151
171
|
@all_examples = {}
|
172
|
+
@passed_examples = Set.new
|
173
|
+
@possibly_flaky_examples = Set.new
|
174
|
+
@flaky_examples = Set.new
|
152
175
|
@failed_examples = Set.new
|
153
176
|
@skipped_examples = Set.new
|
154
177
|
@pending_examples = Set.new
|
@@ -206,50 +229,56 @@ module RSpecTracer
|
|
206
229
|
def write_all_examples_report
|
207
230
|
file_name = File.join(@cache_dir, 'all_examples.json')
|
208
231
|
|
209
|
-
File.write(file_name, JSON.
|
232
|
+
File.write(file_name, JSON.generate(@all_examples))
|
233
|
+
end
|
234
|
+
|
235
|
+
def write_flaky_examples_report
|
236
|
+
file_name = File.join(@cache_dir, 'flaky_examples.json')
|
237
|
+
|
238
|
+
File.write(file_name, JSON.generate(@flaky_examples.to_a))
|
210
239
|
end
|
211
240
|
|
212
241
|
def write_failed_examples_report
|
213
242
|
file_name = File.join(@cache_dir, 'failed_examples.json')
|
214
243
|
|
215
|
-
File.write(file_name, JSON.
|
244
|
+
File.write(file_name, JSON.generate(@failed_examples.to_a))
|
216
245
|
end
|
217
246
|
|
218
247
|
def write_pending_examples_report
|
219
248
|
file_name = File.join(@cache_dir, 'pending_examples.json')
|
220
249
|
|
221
|
-
File.write(file_name, JSON.
|
250
|
+
File.write(file_name, JSON.generate(@pending_examples.to_a))
|
222
251
|
end
|
223
252
|
|
224
253
|
def write_all_files_report
|
225
254
|
file_name = File.join(@cache_dir, 'all_files.json')
|
226
255
|
|
227
|
-
File.write(file_name, JSON.
|
256
|
+
File.write(file_name, JSON.generate(@all_files))
|
228
257
|
end
|
229
258
|
|
230
259
|
def write_dependency_report
|
231
260
|
file_name = File.join(@cache_dir, 'dependency.json')
|
232
261
|
|
233
|
-
File.write(file_name, JSON.
|
262
|
+
File.write(file_name, JSON.generate(@dependency))
|
234
263
|
end
|
235
264
|
|
236
265
|
def write_reverse_dependency_report
|
237
266
|
file_name = File.join(@cache_dir, 'reverse_dependency.json')
|
238
267
|
|
239
|
-
File.write(file_name, JSON.
|
268
|
+
File.write(file_name, JSON.generate(@reverse_dependency))
|
240
269
|
end
|
241
270
|
|
242
271
|
def write_examples_coverage_report
|
243
272
|
file_name = File.join(@cache_dir, 'examples_coverage.json')
|
244
273
|
|
245
|
-
File.write(file_name, JSON.
|
274
|
+
File.write(file_name, JSON.generate(@examples_coverage))
|
246
275
|
end
|
247
276
|
|
248
277
|
def write_last_run_report
|
249
278
|
file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
|
250
279
|
last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
|
251
280
|
|
252
|
-
File.write(file_name, JSON.
|
281
|
+
File.write(file_name, JSON.generate(last_run_data))
|
253
282
|
end
|
254
283
|
end
|
255
284
|
end
|
@@ -2,24 +2,29 @@
|
|
2
2
|
|
3
3
|
module RSpecTracer
|
4
4
|
module RSpecRunner
|
5
|
+
# rubocop:disable Metrics/AbcSize
|
5
6
|
def run_specs(_example_groups)
|
6
7
|
actual_count = RSpec.world.example_count
|
8
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
7
9
|
filtered_examples, example_groups = RSpecTracer.filter_examples
|
8
10
|
|
9
11
|
RSpec.world.instance_variable_set(:@filtered_examples, filtered_examples)
|
10
12
|
RSpec.world.instance_variable_set(:@example_groups, example_groups)
|
11
13
|
|
12
14
|
current_count = RSpec.world.example_count
|
15
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
16
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
13
17
|
|
14
18
|
puts
|
15
19
|
puts <<-EXAMPLES.strip.gsub(/\s+/, ' ')
|
16
20
|
RSpec tracer is running #{current_count} examples (actual: #{actual_count},
|
17
|
-
skipped: #{actual_count - current_count})
|
21
|
+
skipped: #{actual_count - current_count}) (took #{elpased})
|
18
22
|
EXAMPLES
|
19
23
|
|
20
24
|
RSpecTracer.running = true
|
21
25
|
|
22
26
|
super(example_groups)
|
23
27
|
end
|
28
|
+
# rubocop:enable Metrics/AbcSize
|
24
29
|
end
|
25
30
|
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
|
+
flaky_example: 'Flaky example',
|
11
12
|
failed_example: 'Failed previously',
|
12
13
|
pending_example: 'Pending previously',
|
13
14
|
files_changed: 'Files changed'
|
@@ -20,6 +21,8 @@ module RSpecTracer
|
|
20
21
|
@reporter = RSpecTracer::Reporter.new
|
21
22
|
@filtered_examples = {}
|
22
23
|
|
24
|
+
return if @cache.run_id.nil?
|
25
|
+
|
23
26
|
@cache.load_cache_for_run
|
24
27
|
filter_examples_to_run
|
25
28
|
end
|
@@ -127,15 +130,19 @@ module RSpecTracer
|
|
127
130
|
|
128
131
|
def generate_report
|
129
132
|
@reporter.generate_last_run_report
|
133
|
+
generate_examples_status_report
|
130
134
|
|
131
|
-
|
132
|
-
|
135
|
+
%i[all_files all_examples dependency examples_coverage reverse_dependency].each do |report_type|
|
136
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
133
137
|
|
134
|
-
%i[all_files all_examples dependency examples_coverage].each do |report_type|
|
135
138
|
send("generate_#{report_type}_report")
|
139
|
+
|
140
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
141
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
142
|
+
|
143
|
+
puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elpased})"
|
136
144
|
end
|
137
145
|
|
138
|
-
@reporter.generate_reverse_dependency_report
|
139
146
|
@reporter.write_reports
|
140
147
|
end
|
141
148
|
|
@@ -146,54 +153,74 @@ module RSpecTracer
|
|
146
153
|
end
|
147
154
|
|
148
155
|
def filter_examples_to_run
|
149
|
-
|
150
|
-
|
156
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
157
|
+
@changed_files = fetch_changed_files
|
158
|
+
|
159
|
+
filter_by_example_status
|
151
160
|
filter_by_files_changed
|
152
|
-
end
|
153
161
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
162
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
163
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
164
|
+
|
165
|
+
puts "RSpec tracer processed cache (took #{elpased})"
|
158
166
|
end
|
159
167
|
|
160
|
-
def
|
161
|
-
|
162
|
-
|
163
|
-
|
168
|
+
def filter_by_example_status
|
169
|
+
add_previously_flaky_examples
|
170
|
+
add_previously_failed_examples
|
171
|
+
add_previously_pending_examples
|
164
172
|
end
|
165
173
|
|
166
174
|
def filter_by_files_changed
|
167
175
|
@cache.dependency.each_pair do |example_id, files|
|
168
176
|
next if @filtered_examples.key?(example_id)
|
177
|
+
next if (@changed_files & files).empty?
|
169
178
|
|
170
|
-
|
171
|
-
break if filtered_by_file_changed?(example_id, file_name)
|
172
|
-
end
|
179
|
+
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:files_changed]
|
173
180
|
end
|
174
181
|
end
|
175
182
|
|
176
|
-
def
|
177
|
-
|
178
|
-
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:
|
183
|
+
def add_previously_flaky_examples
|
184
|
+
@cache.flaky_examples.each do |example_id|
|
185
|
+
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:flaky_example]
|
179
186
|
|
180
|
-
|
187
|
+
next unless (@changed_files & @cache.dependency[example_id]).empty?
|
188
|
+
|
189
|
+
@reporter.register_possibly_flaky_example(example_id)
|
181
190
|
end
|
191
|
+
end
|
182
192
|
|
183
|
-
|
193
|
+
def add_previously_failed_examples
|
194
|
+
@cache.failed_examples.each do |example_id|
|
195
|
+
next if @filtered_examples.key?(example_id)
|
196
|
+
|
197
|
+
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:failed_example]
|
184
198
|
|
185
|
-
|
186
|
-
|
199
|
+
next unless (@changed_files & @cache.dependency[example_id]).empty?
|
200
|
+
|
201
|
+
@reporter.register_possibly_flaky_example(example_id)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def add_previously_pending_examples
|
206
|
+
@cache.pending_examples.each do |example_id|
|
207
|
+
@filtered_examples[example_id] = EXAMPLE_RUN_REASON[:pending_example]
|
208
|
+
end
|
209
|
+
end
|
187
210
|
|
188
|
-
|
211
|
+
def fetch_changed_files
|
212
|
+
@cache.all_files.each_value do |cached_file|
|
213
|
+
file_name = cached_file[:file_name]
|
214
|
+
source_file = RSpecTracer::SourceFile.from_name(file_name)
|
189
215
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
216
|
+
if source_file.nil?
|
217
|
+
@reporter.on_file_deleted(file_name)
|
218
|
+
elsif cached_file[:digest] != source_file[:digest]
|
219
|
+
@reporter.on_file_modified(file_name)
|
220
|
+
end
|
194
221
|
end
|
195
222
|
|
196
|
-
|
223
|
+
@reporter.modified_files | @reporter.deleted_files
|
197
224
|
end
|
198
225
|
|
199
226
|
def generate_untraced_files(trace_point_files)
|
@@ -227,14 +254,23 @@ module RSpecTracer
|
|
227
254
|
end
|
228
255
|
|
229
256
|
def register_example_file_dependency(example_id, file_name)
|
230
|
-
source_file =
|
257
|
+
source_file = RSpecTracer::SourceFile.from_name(file_name)
|
231
258
|
|
232
259
|
@reporter.register_source_file(source_file)
|
233
260
|
@reporter.register_dependency(example_id, file_name)
|
234
261
|
end
|
235
262
|
|
236
|
-
def
|
237
|
-
|
263
|
+
def generate_examples_status_report
|
264
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
265
|
+
|
266
|
+
generate_flaky_examples_report
|
267
|
+
generate_failed_examples_report
|
268
|
+
generate_pending_examples_report
|
269
|
+
|
270
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
271
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
272
|
+
|
273
|
+
puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elpased})"
|
238
274
|
end
|
239
275
|
|
240
276
|
def generate_all_files_report
|
@@ -255,6 +291,16 @@ module RSpecTracer
|
|
255
291
|
end
|
256
292
|
end
|
257
293
|
|
294
|
+
def generate_flaky_examples_report
|
295
|
+
@reporter.possibly_flaky_examples.each do |example_id|
|
296
|
+
next if @reporter.example_deleted?(example_id)
|
297
|
+
next unless @cache.flaky_examples.include?(example_id) ||
|
298
|
+
@reporter.example_passed?(example_id)
|
299
|
+
|
300
|
+
@reporter.register_flaky_example(example_id)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
258
304
|
def generate_failed_examples_report
|
259
305
|
@cache.failed_examples.each do |example_id|
|
260
306
|
next if @reporter.example_deleted?(example_id) ||
|
@@ -296,5 +342,9 @@ module RSpecTracer
|
|
296
342
|
end
|
297
343
|
end
|
298
344
|
end
|
345
|
+
|
346
|
+
def generate_reverse_dependency_report
|
347
|
+
@reporter.generate_reverse_dependency_report
|
348
|
+
end
|
299
349
|
end
|
300
350
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpecTracer
|
4
|
+
module TimeFormatter
|
5
|
+
DEFAULT_PRECISION = 2
|
6
|
+
SECONDS_PRECISION = 5
|
7
|
+
|
8
|
+
UNITS = {
|
9
|
+
second: 60,
|
10
|
+
minute: 60,
|
11
|
+
hour: 24,
|
12
|
+
day: Float::INFINITY
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
module_function
|
16
|
+
|
17
|
+
def format_time(seconds)
|
18
|
+
return pluralize(format_duration(seconds), 'second') if seconds < 60
|
19
|
+
|
20
|
+
formatted_duration = UNITS.each_pair.with_object([]) do |(unit, count), duration|
|
21
|
+
next unless seconds.positive?
|
22
|
+
|
23
|
+
seconds, remainder = seconds.divmod(count)
|
24
|
+
remainder = format_duration(remainder)
|
25
|
+
|
26
|
+
next if remainder.zero?
|
27
|
+
|
28
|
+
duration << pluralize(remainder, unit)
|
29
|
+
end
|
30
|
+
|
31
|
+
formatted_duration.reverse.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
def format_duration(duration)
|
35
|
+
return 0 if duration.negative?
|
36
|
+
|
37
|
+
precision = duration < 1 ? SECONDS_PRECISION : DEFAULT_PRECISION
|
38
|
+
|
39
|
+
format("%<duration>0.#{precision}f", duration: duration)
|
40
|
+
end
|
41
|
+
|
42
|
+
def pluralize(duration, unit)
|
43
|
+
if duration == 1
|
44
|
+
"#{duration} #{unit}"
|
45
|
+
else
|
46
|
+
"#{duration} #{unit}s"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private_class_method :format_duration, :pluralize
|
51
|
+
end
|
52
|
+
end
|
data/lib/rspec_tracer/version.rb
CHANGED
data/lib/rspec_tracer.rb
CHANGED
@@ -23,6 +23,7 @@ require_relative 'rspec_tracer/rspec_runner'
|
|
23
23
|
require_relative 'rspec_tracer/ruby_coverage'
|
24
24
|
require_relative 'rspec_tracer/runner'
|
25
25
|
require_relative 'rspec_tracer/source_file'
|
26
|
+
require_relative 'rspec_tracer/time_formatter'
|
26
27
|
require_relative 'rspec_tracer/version'
|
27
28
|
|
28
29
|
module RSpecTracer
|
@@ -184,22 +185,36 @@ module RSpecTracer
|
|
184
185
|
def generate_reports
|
185
186
|
puts 'RSpec tracer is generating reports'
|
186
187
|
|
187
|
-
|
188
|
-
|
188
|
+
process_dependency
|
189
|
+
process_coverage
|
189
190
|
runner.generate_report
|
190
191
|
RSpecTracer::HTMLReporter::Reporter.new.generate_report
|
191
192
|
end
|
192
193
|
|
193
|
-
def
|
194
|
+
def process_dependency
|
195
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
196
|
+
|
194
197
|
runner.register_deleted_examples
|
195
198
|
runner.register_dependency(coverage_reporter.examples_coverage)
|
196
199
|
runner.register_untraced_dependency(@traced_files)
|
200
|
+
|
201
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
202
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
203
|
+
|
204
|
+
puts "RSpec tracer processed dependency (took #{elpased})"
|
197
205
|
end
|
198
206
|
|
199
|
-
def
|
207
|
+
def process_coverage
|
208
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
209
|
+
|
200
210
|
coverage_reporter.generate_final_examples_coverage
|
201
211
|
coverage_reporter.merge_coverage(runner.generate_missed_coverage)
|
202
212
|
runner.register_examples_coverage(coverage_reporter.examples_coverage)
|
213
|
+
|
214
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
215
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
216
|
+
|
217
|
+
puts "RSpec tracer processed coverage (took #{elpased})"
|
203
218
|
end
|
204
219
|
|
205
220
|
def run_simplecov_exit_task
|
@@ -213,12 +228,18 @@ module RSpecTracer
|
|
213
228
|
end
|
214
229
|
|
215
230
|
def run_coverage_exit_task
|
231
|
+
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
232
|
+
|
216
233
|
coverage_reporter.generate_final_coverage
|
217
234
|
|
218
235
|
file_name = File.join(RSpecTracer.coverage_path, 'coverage.json')
|
219
236
|
|
220
237
|
write_coverage_report(file_name)
|
221
|
-
|
238
|
+
|
239
|
+
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
240
|
+
elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
|
241
|
+
|
242
|
+
print_coverage_stats(file_name, elpased)
|
222
243
|
end
|
223
244
|
|
224
245
|
def write_coverage_report(file_name)
|
@@ -229,15 +250,16 @@ module RSpecTracer
|
|
229
250
|
}
|
230
251
|
}
|
231
252
|
|
232
|
-
File.write(file_name, JSON.
|
253
|
+
File.write(file_name, JSON.generate(report))
|
233
254
|
end
|
234
255
|
|
235
|
-
def print_coverage_stats(file_name)
|
256
|
+
def print_coverage_stats(file_name, elpased)
|
236
257
|
stat = coverage_reporter.coverage_stat
|
237
258
|
|
238
259
|
puts <<-REPORT.strip.gsub(/\s+/, ' ')
|
239
260
|
Coverage report generated for RSpecTracer to #{file_name}. #{stat[:covered_lines]}
|
240
261
|
/ #{stat[:total_lines]} LOC (#{stat[:covered_percent]}%) covered
|
262
|
+
(took #{elpased})
|
241
263
|
REPORT
|
242
264
|
end
|
243
265
|
end
|
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.6.0
|
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-
|
11
|
+
date: 2021-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: docile
|
@@ -90,6 +90,7 @@ files:
|
|
90
90
|
- lib/rspec_tracer/html_reporter/views/examples.erb
|
91
91
|
- lib/rspec_tracer/html_reporter/views/examples_dependency.erb
|
92
92
|
- lib/rspec_tracer/html_reporter/views/files_dependency.erb
|
93
|
+
- lib/rspec_tracer/html_reporter/views/flaky_examples.erb
|
93
94
|
- lib/rspec_tracer/html_reporter/views/layout.erb
|
94
95
|
- lib/rspec_tracer/remote_cache/cache.rb
|
95
96
|
- lib/rspec_tracer/remote_cache/git.rb
|
@@ -99,13 +100,14 @@ files:
|
|
99
100
|
- lib/rspec_tracer/ruby_coverage.rb
|
100
101
|
- lib/rspec_tracer/runner.rb
|
101
102
|
- lib/rspec_tracer/source_file.rb
|
103
|
+
- lib/rspec_tracer/time_formatter.rb
|
102
104
|
- lib/rspec_tracer/version.rb
|
103
105
|
homepage: https://github.com/avmnu-sng/rspec-tracer
|
104
106
|
licenses:
|
105
107
|
- MIT
|
106
108
|
metadata:
|
107
109
|
homepage_uri: https://github.com/avmnu-sng/rspec-tracer
|
108
|
-
source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.
|
110
|
+
source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.6.0
|
109
111
|
changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
|
110
112
|
bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
|
111
113
|
post_install_message:
|