rspec-tracer 0.5.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2817cccf4825e28177bb8794f8d261635f81ec5c1bd6ca0242ba171e702850ca
4
- data.tar.gz: 9c5f6af579eec43a994c28b5e8132fec5d71f7a2ddddfb531fafd5c51943d283
3
+ metadata.gz: 4825c225fdac05b09d4c56c5b6b191d0b32ad4198fc39b72a215bef3d066f7bd
4
+ data.tar.gz: 4bdcb243ed2cf200f6dae4b00b4dfd50fbc8bb70b9f59552056059ec3be643b1
5
5
  SHA512:
6
- metadata.gz: 2d3af598e09b8ac043b5f605acf39b588701c4f0e7b4d74646eb0da74c1c85746db60f215e02cd5fba1880bb75288d0d35df3986b08cc71cfbad4d45ad722d2f
7
- data.tar.gz: 7ce36336213d4d37337afa5b50fd774bd6ef7b235539f3a289f1a804a878fba3fa1e7972e9bd09bb8083c80929c0fc3b6bf0b30f257542fdb5e5c466c1247360
6
+ metadata.gz: 80dff35e9e15e2b28bd5c0658fdc027b659a9a1cf4265e17d7207ce0327bc975b05890d9e23fd9b22faa81b6a471d9a3d68d5bec0e501314907534796ff6088c
7
+ data.tar.gz: 77d1e9a4a9c4fe26713da06d594e7c6088655a2bc84c7e38ccd145c27f60247a9554043631c526c4a262637304c730c59ae767b3d4e8462ebc4e7e397e330055
data/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## [0.7.0] - 2021-09-10
2
+
3
+ ### Fixed
4
+
5
+ Missing spec files for the gem
6
+
7
+ ## [0.6.2] - 2021-09-07
8
+
9
+ ### Added
10
+
11
+ Improvements towards reducing dependency and coverage processing time (#26)
12
+
13
+ ## [0.6.1] - 2021-09-06
14
+
15
+ ### Fixed
16
+
17
+ Bug in time formatter (#24)
18
+
19
+ ### Added
20
+
21
+ Environment variable to control verbose output (#25)
22
+
23
+ ## [0.6.0] - 2021-09-05
24
+
25
+ ### Added
26
+
27
+ - Improved dependency change detection (#18)
28
+ - Flaky tests detection (#19)
29
+ - Exclude vendor files from analysis (#21)
30
+ - Report elapsed time at various stages (#23)
31
+
32
+ ### Note
33
+
34
+ The first run on this version will not use any cache on the CI because the number
35
+ of files changed from eight to nine, so there will be no appropriate cache to use.
36
+
1
37
  ## [0.5.0] - 2021-09-03
2
38
 
3
39
  ### 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,15 +33,18 @@ 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)
34
40
  * [RSPEC_TRACER_S3_URI](#rspec_tracer_s3_uri)
35
41
  * [RSPEC_TRACER_UPLOAD_LOCAL_CACHE](#rspec_tracer_upload_local_cache)
42
+ * [RSPEC_TRACER_VERBOSE](#rspec_tracer_verbose)
36
43
  * [TEST_SUITES](#test_suites)
37
44
  * [TEST_SUITE_ID](#test_suite_id)
38
45
  * [Sample Reports](#sample-reports)
39
46
  * [Examples](#examples)
47
+ * [Flaky Examples](#flaky-examples)
40
48
  * [Examples Dependency](#examples-dependency)
41
49
  * [Files Dependency](#files-dependency)
42
50
  * [Configuring RSpec Tracer](#configuring-rspec-tracer)
@@ -63,7 +71,7 @@ and **caching on CI**, etc.
63
71
 
64
72
  Add this line to your `Gemfile` and `bundle install`:
65
73
  ```ruby
66
- gem 'rspec-tracer', group: :test, require: false
74
+ gem 'rspec-tracer', '~> 0.6', group: :test, require: false
67
75
  ```
68
76
 
69
77
  And, add the followings to your `.gitignore`:
@@ -142,7 +150,15 @@ browser of your choice.
142
150
 
143
151
  ## Environment Variables
144
152
 
145
- To get better control on execution, you can use the following two environment variables:
153
+ To get better control on execution, you can use the following environment variables
154
+ whenever required.
155
+
156
+ ### BUNDLE_PATH
157
+
158
+ Since the bundler uses a vendor directory inside the project, it might cause slowness
159
+ depending on the vendor size. You can configure the bundle path outside of the project
160
+ using `BUNDLE_PATH` environment variable, for example, `BUNDLE_PATH=$HOME/vendor/bundle`.
161
+ Make sure to cache this directory in the CI configuration.
146
162
 
147
163
  ### CI
148
164
 
@@ -177,6 +193,14 @@ export RSPEC_TRACER_S3_URI=s3://ci-artifacts-bucket/rspec-tracer-cache
177
193
  By default, RSpec Tracer does not upload local cache files. You can set this
178
194
  environment variable to `true` to upload the local cache to S3.
179
195
 
196
+ ### RSPEC_TRACER_VERBOSE
197
+
198
+ To print the intermediate steps and time taken, use this environment variable:
199
+
200
+ ```sh
201
+ export RSPEC_TRACER_VERBOSE=true
202
+ ```
203
+
180
204
  ### TEST_SUITES
181
205
 
182
206
  Set this environment variable when using test suite id. It determines the total
@@ -190,7 +214,7 @@ export TEST_SUITES=8
190
214
 
191
215
  If you have a large set of tests to run, it is recommended to run them in
192
216
  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
217
+ cached data in the memory. Also, it generates and uses cache for specific test suites
194
218
  and not merge them.
195
219
 
196
220
  ```ruby
@@ -229,6 +253,19 @@ These reports provide basic test information:
229
253
 
230
254
  ![](./readme_files/examples_report_next_run.png)
231
255
 
256
+ ### Flaky Examples
257
+
258
+ These reports provide flaky tests information. Assuming **the following two tests
259
+ failed in the first run.**
260
+
261
+ **Next Run**
262
+
263
+ ![](./readme_files/flaky_examples_report_first_run.png)
264
+
265
+ **Another Run**
266
+
267
+ ![](./readme_files/flaky_examples_report_next_run.png)
268
+
232
269
  ### Examples Dependency
233
270
 
234
271
  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})" if RSpecTracer.verbose?
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
 
@@ -102,6 +102,12 @@ module RSpecTracer
102
102
  @coverage_filters ||= []
103
103
  end
104
104
 
105
+ def verbose?
106
+ return @verbose if defined?(@verbose)
107
+
108
+ @verbose = ENV.fetch('RSPEC_TRACER_VERBOSE', 'false') == 'true'
109
+ end
110
+
105
111
  def configure(&block)
106
112
  Docile.dsl_eval(self, &block)
107
113
  end
@@ -29,11 +29,15 @@ module RSpecTracer
29
29
 
30
30
  def compute_diff(example_id)
31
31
  peek_coverage.each_pair do |file_path, current_stats|
32
- if @coverage.key?(file_path)
33
- existing_file_diff_coverage(example_id, file_path, current_stats)
34
- else
32
+ unless @coverage.key?(file_path)
35
33
  missing_file_diff_coverage(example_id, file_path, current_stats)
34
+
35
+ next
36
36
  end
37
+
38
+ next if current_stats == @coverage[file_path]
39
+
40
+ existing_file_diff_coverage(example_id, file_path, current_stats)
37
41
  end
38
42
  end
39
43
 
@@ -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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :assets do
4
+ desc 'Compiles all assets'
5
+ task :precompile do
6
+ require 'sprockets'
7
+
8
+ assets = Sprockets::Environment.new do |env|
9
+ env.append_path 'assets/javascripts'
10
+ env.append_path 'assets/stylesheets'
11
+ env.js_compressor = :uglifier
12
+ env.css_compressor = :yui
13
+ end
14
+
15
+ assets['application.js'].write_to('public/application.js')
16
+ assets['application.css'].write_to('public/application.css')
17
+ end
18
+ end
@@ -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>
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rspec_tracer do
4
+ namespace :remote_cache do
5
+ desc 'Download cache'
6
+ task :download do
7
+ unless system('git', 'rev-parse', 'HEAD', out: File::NULL, err: File::NULL)
8
+ puts 'Not a git repository'
9
+
10
+ exit
11
+ end
12
+
13
+ require 'rspec_tracer'
14
+
15
+ RSpecTracer::RemoteCache::Cache.new.download
16
+ end
17
+
18
+ desc 'Upload cache'
19
+ task :upload do
20
+ unless system('git', 'rev-parse', 'HEAD', out: File::NULL, err: File::NULL)
21
+ puts 'Not a git repository'
22
+
23
+ exit
24
+ end
25
+
26
+ unless ENV.fetch('CI', 'false') == 'true' || ENV.fetch('RSPEC_TRACER_UPLOAD_LOCAL_CACHE', 'false') == 'true'
27
+ puts 'Skipping upload from local development environment'
28
+ puts 'Use RSPEC_TRACER_UPLOAD_LOCAL_CACHE=true to upload local cache'
29
+
30
+ exit
31
+ end
32
+
33
+ require 'rspec_tracer'
34
+
35
+ RSpecTracer::RemoteCache::Cache.new.upload
36
+ end
37
+ end
38
+ end
@@ -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
@@ -209,6 +232,12 @@ module RSpecTracer
209
232
  File.write(file_name, JSON.pretty_generate(@all_examples))
210
233
  end
211
234
 
235
+ def write_flaky_examples_report
236
+ file_name = File.join(@cache_dir, 'flaky_examples.json')
237
+
238
+ File.write(file_name, JSON.pretty_generate(@flaky_examples.to_a))
239
+ end
240
+
212
241
  def write_failed_examples_report
213
242
  file_name = File.join(@cache_dir, 'failed_examples.json')
214
243
 
@@ -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
@@ -87,16 +90,15 @@ module RSpecTracer
87
90
  # rubocop:enable Metrics/AbcSize
88
91
 
89
92
  def register_dependency(examples_coverage)
93
+ filtered_files = Set.new
94
+
90
95
  examples_coverage.each_pair do |example_id, example_coverage|
91
96
  register_example_files_dependency(example_id)
92
97
 
93
98
  example_coverage.each_key do |file_path|
94
- source_file = RSpecTracer::SourceFile.from_path(file_path)
99
+ next if filtered_files.include?(file_path)
95
100
 
96
- next if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
97
-
98
- @reporter.register_source_file(source_file)
99
- @reporter.register_dependency(example_id, source_file[:file_name])
101
+ filtered_files << file_path unless register_file_dependency(example_id, file_path)
100
102
  end
101
103
  end
102
104
 
@@ -127,15 +129,19 @@ module RSpecTracer
127
129
 
128
130
  def generate_report
129
131
  @reporter.generate_last_run_report
132
+ generate_examples_status_report
130
133
 
131
- generate_failed_examples_report
132
- generate_pending_examples_report
134
+ %i[all_files all_examples dependency examples_coverage reverse_dependency].each do |report_type|
135
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
133
136
 
134
- %i[all_files all_examples dependency examples_coverage].each do |report_type|
135
137
  send("generate_#{report_type}_report")
138
+
139
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
140
+ elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
141
+
142
+ puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elpased})" if RSpecTracer.verbose?
136
143
  end
137
144
 
138
- @reporter.generate_reverse_dependency_report
139
145
  @reporter.write_reports
140
146
  end
141
147
 
@@ -146,54 +152,74 @@ module RSpecTracer
146
152
  end
147
153
 
148
154
  def filter_examples_to_run
149
- add_previously_failed_examples
150
- add_previously_pending_examples
155
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
+ @changed_files = fetch_changed_files
157
+
158
+ filter_by_example_status
151
159
  filter_by_files_changed
152
- end
153
160
 
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
161
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
162
+ elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
163
+
164
+ puts "RSpec tracer processed cache (took #{elpased})" if RSpecTracer.verbose?
158
165
  end
159
166
 
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
167
+ def filter_by_example_status
168
+ add_previously_flaky_examples
169
+ add_previously_failed_examples
170
+ add_previously_pending_examples
164
171
  end
165
172
 
166
173
  def filter_by_files_changed
167
174
  @cache.dependency.each_pair do |example_id, files|
168
175
  next if @filtered_examples.key?(example_id)
176
+ next if (@changed_files & files).empty?
169
177
 
170
- files.each do |file_name|
171
- break if filtered_by_file_changed?(example_id, file_name)
172
- end
178
+ @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:files_changed]
173
179
  end
174
180
  end
175
181
 
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]
182
+ def add_previously_flaky_examples
183
+ @cache.flaky_examples.each do |example_id|
184
+ @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:flaky_example]
185
+
186
+ next unless (@changed_files & @cache.dependency[example_id]).empty?
179
187
 
180
- return true
188
+ @reporter.register_possibly_flaky_example(example_id)
181
189
  end
190
+ end
191
+
192
+ def add_previously_failed_examples
193
+ @cache.failed_examples.each do |example_id|
194
+ next if @filtered_examples.key?(example_id)
195
+
196
+ @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:failed_example]
197
+
198
+ next unless (@changed_files & @cache.dependency[example_id]).empty?
182
199
 
183
- source_file = registered_source_file(file_name)
200
+ @reporter.register_possibly_flaky_example(example_id)
201
+ end
202
+ end
184
203
 
185
- return false if source_file &&
186
- @cache.all_files[file_name][:digest] == source_file[:digest]
204
+ def add_previously_pending_examples
205
+ @cache.pending_examples.each do |example_id|
206
+ @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:pending_example]
207
+ end
208
+ end
187
209
 
188
- @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:files_changed]
210
+ def fetch_changed_files
211
+ @cache.all_files.each_value do |cached_file|
212
+ file_name = cached_file[:file_name]
213
+ source_file = RSpecTracer::SourceFile.from_name(file_name)
189
214
 
190
- if source_file.nil?
191
- @reporter.on_file_deleted(file_name)
192
- else
193
- @reporter.on_file_modified(file_name)
215
+ if source_file.nil?
216
+ @reporter.on_file_deleted(file_name)
217
+ elsif cached_file[:digest] != source_file[:digest]
218
+ @reporter.on_file_modified(file_name)
219
+ end
194
220
  end
195
221
 
196
- true
222
+ @reporter.modified_files | @reporter.deleted_files
197
223
  end
198
224
 
199
225
  def generate_untraced_files(trace_point_files)
@@ -227,14 +253,34 @@ module RSpecTracer
227
253
  end
228
254
 
229
255
  def register_example_file_dependency(example_id, file_name)
230
- source_file = registered_source_file(file_name)
256
+ source_file = RSpecTracer::SourceFile.from_name(file_name)
231
257
 
232
258
  @reporter.register_source_file(source_file)
233
259
  @reporter.register_dependency(example_id, file_name)
234
260
  end
235
261
 
236
- def registered_source_file(file_name)
237
- @reporter.all_files[file_name] || RSpecTracer::SourceFile.from_name(file_name)
262
+ def register_file_dependency(example_id, file_path)
263
+ source_file = RSpecTracer::SourceFile.from_path(file_path)
264
+
265
+ return false if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
266
+
267
+ @reporter.register_source_file(source_file)
268
+ @reporter.register_dependency(example_id, source_file[:file_name])
269
+
270
+ true
271
+ end
272
+
273
+ def generate_examples_status_report
274
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
275
+
276
+ generate_flaky_examples_report
277
+ generate_failed_examples_report
278
+ generate_pending_examples_report
279
+
280
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
281
+ elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
282
+
283
+ puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elpased})" if RSpecTracer.verbose?
238
284
  end
239
285
 
240
286
  def generate_all_files_report
@@ -255,6 +301,16 @@ module RSpecTracer
255
301
  end
256
302
  end
257
303
 
304
+ def generate_flaky_examples_report
305
+ @reporter.possibly_flaky_examples.each do |example_id|
306
+ next if @reporter.example_deleted?(example_id)
307
+ next unless @cache.flaky_examples.include?(example_id) ||
308
+ @reporter.example_passed?(example_id)
309
+
310
+ @reporter.register_flaky_example(example_id)
311
+ end
312
+ end
313
+
258
314
  def generate_failed_examples_report
259
315
  @cache.failed_examples.each do |example_id|
260
316
  next if @reporter.example_deleted?(example_id) ||
@@ -296,5 +352,9 @@ module RSpecTracer
296
352
  end
297
353
  end
298
354
  end
355
+
356
+ def generate_reverse_dependency_report
357
+ @reporter.generate_reverse_dependency_report
358
+ end
299
359
  end
300
360
  end
@@ -0,0 +1,55 @@
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
+
25
+ next if remainder.zero?
26
+
27
+ duration << pluralize(format_duration(remainder), unit)
28
+ end
29
+
30
+ formatted_duration.reverse.join(' ')
31
+ end
32
+
33
+ def format_duration(duration)
34
+ return 0 if duration.negative?
35
+
36
+ precision = duration < 1 ? SECONDS_PRECISION : DEFAULT_PRECISION
37
+
38
+ strip_trailing_zeroes(format("%<duration>0.#{precision}f", duration: duration))
39
+ end
40
+
41
+ def strip_trailing_zeroes(formatted_duration)
42
+ formatted_duration.sub(/(?:(\..*[^0])0+|\.0+)$/, '\1')
43
+ end
44
+
45
+ def pluralize(duration, unit)
46
+ if (duration.to_f - 1).abs < Float::EPSILON
47
+ "#{duration} #{unit}"
48
+ else
49
+ "#{duration} #{unit}s"
50
+ end
51
+ end
52
+
53
+ private_class_method :format_duration, :strip_trailing_zeroes, :pluralize
54
+ end
55
+ 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.7.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})" if RSpecTracer.verbose?
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})" if RSpecTracer.verbose?
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)
@@ -232,12 +253,13 @@ module RSpecTracer
232
253
  File.write(file_name, JSON.pretty_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,55 +1,55 @@
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.7.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-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docile
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.1.0
20
17
  - - "~>"
21
18
  - !ruby/object:Gem::Version
22
19
  version: '1.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 1.1.0
30
27
  - - "~>"
31
28
  - !ruby/object:Gem::Version
32
29
  version: '1.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.0
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rspec-core
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: 3.6.0
40
37
  - - "~>"
41
38
  - !ruby/object:Gem::Version
42
39
  version: '3.6'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.6.0
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 3.6.0
50
47
  - - "~>"
51
48
  - !ruby/object:Gem::Version
52
49
  version: '3.6'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.6.0
53
53
  description: RSpec Tracer is a specs dependency analysis tool and a test skipper for
54
54
  RSpec. It maintains a list of files for each test, enabling itself to skip tests
55
55
  in the subsequent runs if none of the dependent files are changed.
@@ -69,6 +69,7 @@ files:
69
69
  - lib/rspec_tracer/defaults.rb
70
70
  - lib/rspec_tracer/example.rb
71
71
  - lib/rspec_tracer/filter.rb
72
+ - lib/rspec_tracer/html_reporter/Rakefile
72
73
  - lib/rspec_tracer/html_reporter/assets/javascripts/application.js
73
74
  - lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js
74
75
  - lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js
@@ -90,7 +91,9 @@ files:
90
91
  - lib/rspec_tracer/html_reporter/views/examples.erb
91
92
  - lib/rspec_tracer/html_reporter/views/examples_dependency.erb
92
93
  - lib/rspec_tracer/html_reporter/views/files_dependency.erb
94
+ - lib/rspec_tracer/html_reporter/views/flaky_examples.erb
93
95
  - lib/rspec_tracer/html_reporter/views/layout.erb
96
+ - lib/rspec_tracer/remote_cache/Rakefile
94
97
  - lib/rspec_tracer/remote_cache/cache.rb
95
98
  - lib/rspec_tracer/remote_cache/git.rb
96
99
  - lib/rspec_tracer/reporter.rb
@@ -99,13 +102,14 @@ files:
99
102
  - lib/rspec_tracer/ruby_coverage.rb
100
103
  - lib/rspec_tracer/runner.rb
101
104
  - lib/rspec_tracer/source_file.rb
105
+ - lib/rspec_tracer/time_formatter.rb
102
106
  - lib/rspec_tracer/version.rb
103
107
  homepage: https://github.com/avmnu-sng/rspec-tracer
104
108
  licenses:
105
109
  - MIT
106
110
  metadata:
107
111
  homepage_uri: https://github.com/avmnu-sng/rspec-tracer
108
- source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.5.0
112
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.7.0
109
113
  changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
110
114
  bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
111
115
  post_install_message:
@@ -123,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
127
  - !ruby/object:Gem::Version
124
128
  version: '0'
125
129
  requirements: []
126
- rubygems_version: 3.0.9
130
+ rubygems_version: 3.2.26
127
131
  signing_key:
128
132
  specification_version: 4
129
133
  summary: RSpec Tracer is a specs dependency analysis tool and a test skipper for RSpec