rspec-tracer 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2817cccf4825e28177bb8794f8d261635f81ec5c1bd6ca0242ba171e702850ca
4
- data.tar.gz: 9c5f6af579eec43a994c28b5e8132fec5d71f7a2ddddfb531fafd5c51943d283
3
+ metadata.gz: 885e868cf847ca1bab4ba2243cd4cd6a4bc3f936a3c8c10f677146433acc2068
4
+ data.tar.gz: d60fcdeef0067a37faec3ea876ffa519bdd7039a1765ad59e551427bcf2c8e29
5
5
  SHA512:
6
- metadata.gz: 2d3af598e09b8ac043b5f605acf39b588701c4f0e7b4d74646eb0da74c1c85746db60f215e02cd5fba1880bb75288d0d35df3986b08cc71cfbad4d45ad722d2f
7
- data.tar.gz: 7ce36336213d4d37337afa5b50fd774bd6ef7b235539f3a289f1a804a878fba3fa1e7972e9bd09bb8083c80929c0fc3b6bf0b30f257542fdb5e5c466c1247360
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 any tests which failed or were pending** in the last runs.
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 **skipping tests**, **managing coverage**,
21
- and **caching on CI**, etc.
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 two environment variables:
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 generate and use cache for specific test suites
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
  ![](./readme_files/examples_report_next_run.png)
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
+ ![](./readme_files/flaky_examples_report_first_run.png)
255
+
256
+ **Another Run**
257
+
258
+ ![](./readme_files/flaky_examples_report_next_run.png)
259
+
232
260
  ### Examples Dependency
233
261
 
234
262
  These reports show a list of dependent files for each test.
@@ -2,7 +2,8 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Cache
5
- attr_reader :all_examples, :failed_examples, :pending_examples, :all_files, :dependency
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
- puts "RSpec tracer loaded cache from #{@cache_dir}" if @run_id
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
- load_examples_coverage_cache
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
 
@@ -2,7 +2,13 @@
2
2
 
3
3
  RSpecTracer.configure do
4
4
  add_filter '/vendor/bundle/'
5
- add_coverage_filter %w[/autotest/ /features/ /spec/ /test/].freeze
5
+ add_coverage_filter %w[
6
+ /autotest/
7
+ /features/
8
+ /spec/
9
+ /test/
10
+ /vendor/bundle/
11
+ ].freeze
6
12
  end
7
13
 
8
14
  at_exit do
@@ -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
- Dir[File.join(File.dirname(__FILE__), 'public/*')].each do |path|
22
- FileUtils.cp_r(path, asset_output_path)
23
- end
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
- puts "RSpecTracer generated HTML report to #{file_name}"
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>
@@ -11,7 +11,7 @@ module RSpecTracer
11
11
 
12
12
  class LocalCacheNotFoundError < StandardError; end
13
13
 
14
- CACHE_FILES_PER_TEST_SUITE = 7
14
+ CACHE_FILES_PER_TEST_SUITE = 8
15
15
 
16
16
  def initialize
17
17
  @s3_uri = ENV['RSPEC_TRACER_S3_URI']
@@ -2,8 +2,9 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Reporter
5
- attr_reader :all_examples, :pending_examples, :all_files, :dependency,
6
- :reverse_dependency, :examples_coverage, :last_run
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
- puts "RSpec tracer reports generated to #{@cache_dir}"
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.pretty_generate(@all_examples))
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.pretty_generate(@failed_examples.to_a))
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.pretty_generate(@pending_examples.to_a))
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.pretty_generate(@all_files))
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.pretty_generate(@dependency))
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.pretty_generate(@reverse_dependency))
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.pretty_generate(@examples_coverage))
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.pretty_generate(last_run_data))
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
@@ -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
- generate_failed_examples_report
132
- generate_pending_examples_report
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
- add_previously_failed_examples
150
- add_previously_pending_examples
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
- def add_previously_failed_examples
155
- @cache.failed_examples.each do |example_id|
156
- @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:failed_example]
157
- end
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 add_previously_pending_examples
161
- @cache.pending_examples.each do |example_id|
162
- @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:pending_example]
163
- end
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
- files.each do |file_name|
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 filtered_by_file_changed?(example_id, file_name)
177
- if @reporter.file_changed?(file_name)
178
- @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:files_changed]
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
- return true
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
- source_file = registered_source_file(file_name)
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
- return false if source_file &&
186
- @cache.all_files[file_name][:digest] == source_file[:digest]
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
- @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:files_changed]
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
- if source_file.nil?
191
- @reporter.on_file_deleted(file_name)
192
- else
193
- @reporter.on_file_modified(file_name)
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
- true
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 = registered_source_file(file_name)
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 registered_source_file(file_name)
237
- @reporter.all_files[file_name] || RSpecTracer::SourceFile.from_name(file_name)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
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
- generate_tracer_reports
188
- generate_coverage_reports
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 generate_tracer_reports
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 generate_coverage_reports
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
- print_coverage_stats(file_name)
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.pretty_generate(report))
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.5.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-03 00:00:00.000000000 Z
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.5.0
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: