rspec-tracer 0.5.0 → 0.7.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: 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