rspec-tracer 0.4.0 → 0.6.2

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: 34f47b6527f2c8f3c1cb3524adf8d81a9fe7e3833c202605a206598dee8758b2
4
- data.tar.gz: e647137006f5f8af18999ea8f2665c09d703bf2cd8ea1e84dec85e66f87ba5e2
3
+ metadata.gz: a86a9027964446fd10fc51379221d718e15699f6f93ba0c56af654273ccf632d
4
+ data.tar.gz: 1ade4a1b9a14b8cf0a10467889e0631d4b4e30871118734a6d2b4705138b19b2
5
5
  SHA512:
6
- metadata.gz: 05c204bd00cfa15258a1770790360184c9481d670cc8ef3542b06024976a9e13cf3595e79ecefa0d22c8337d35dee9ef6963b145cf1aa16268f3de467c7d171b
7
- data.tar.gz: b2f7e88fbf404793a9e2daaade7448d51bd63150701a6735c9ae8bdcb17eaf25c926bde411cd61bc5dd20133829d1576d16b7588336f05ec968ae4f3d82a5453
6
+ metadata.gz: c251e47b35e667c0b847c54bb7a7c96ad33c39b8c91f48bcaf96c06cb9b017155ed760398f24e382ee93c94fe8bf7b40289fd9c87518bbd467d42b2bae91daac
7
+ data.tar.gz: a314d23f2b7998cb64b5183a5a0da4d1d100039accfd28998ccdf15d451f633fb4181688951de400bbaf82095253f56d73e585632e411657ada157d0e285a1b0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## [0.6.2] - 2021-09-07
2
+
3
+ ### Added
4
+
5
+ Improvements towards reducing dependency and coverage processing time (#26)
6
+
7
+ ## [0.6.1] - 2021-09-06
8
+
9
+ ### Fixed
10
+
11
+ Bug in time formatter (#24)
12
+
13
+ ### Added
14
+
15
+ Environment variable to control verbose output (#25)
16
+
17
+ ## [0.6.0] - 2021-09-05
18
+
19
+ ### Added
20
+
21
+ - Improved dependency change detection (#18)
22
+ - Flaky tests detection (#19)
23
+ - Exclude vendor files from analysis (#21)
24
+ - Report elapsed time at various stages (#23)
25
+
26
+ ### Note
27
+
28
+ The first run on this version will not use any cache on the CI because the number
29
+ of files changed from eight to nine, so there will be no appropriate cache to use.
30
+
31
+ ## [0.5.0] - 2021-09-03
32
+
33
+ ### Fixed
34
+
35
+ - Limit number of cached files download (#16)
36
+
1
37
  ## [0.4.0] - 2021-09-03
2
38
 
3
39
  ### Added
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)
36
- * [TEST_SUITE_ID](#test_suite_id)
42
+ * [RSPEC_TRACER_VERBOSE](#rspec_tracer_verbose)
37
43
  * [TEST_SUITES](#test_suites)
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', version: '~> 0.6', group: :test, require: false
67
75
  ```
68
76
 
69
77
  And, add the followings to your `.gitignore`:
@@ -128,13 +136,13 @@ Rakefile in your project to have the following:
128
136
  ```
129
137
  3. Before running tests, download the remote cache using the following rake task:
130
138
 
131
- ```ruby
139
+ ```sh
132
140
  bundle exec rake rspec_tracer:remote_cache:download
133
141
  ```
134
142
  4. Run the tests with RSpec using `bundle exec rspec`.
135
143
  5. After running tests, upload the local cache using the following rake task:
136
144
 
137
- ```ruby
145
+ ```sh
138
146
  bundle exec rake rspec_tracer:remote_cache:upload
139
147
  ```
140
148
  6. After running your tests, open `rspec_tracer_report/index.html` in the
@@ -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,11 +193,28 @@ 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
+
204
+ ### TEST_SUITES
205
+
206
+ Set this environment variable when using test suite id. It determines the total
207
+ number of different test suites you are running.
208
+
209
+ ```ruby
210
+ export TEST_SUITES=8
211
+ ```
212
+
180
213
  ### TEST_SUITE_ID
181
214
 
182
215
  If you have a large set of tests to run, it is recommended to run them in
183
216
  separate groups. This way, RSpec Tracer is not overwhelmed with loading massive
184
- 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
185
218
  and not merge them.
186
219
 
187
220
  ```ruby
@@ -189,13 +222,19 @@ TEST_SUITE_ID=1 bundle exec rspec spec/models
189
222
  TEST_SUITE_ID=2 bundle exec rspec spec/helpers
190
223
  ```
191
224
 
192
- ### TEST_SUITES
225
+ If you run parallel builds on the CI, you should specify the test suite ID and
226
+ the total number of test suites when downloading the cache files.
193
227
 
194
- Set this environment variable when using test suite id. It determines the total
195
- number of different test suites you are running.
228
+ ```sh
229
+ $ TEST_SUITES=5 TEST_SUITE_ID=1 bundle exec rake rspec_tracer:remote_cache:download
230
+ ```
196
231
 
197
- ```ruby
198
- export TEST_SUITES=8
232
+ In this case, the appropriate cache should have all the cache files available on
233
+ the S3 for each test suite, not just for the current one. Also, while uploading,
234
+ make sure to provide the test suite id.
235
+
236
+ ```sh
237
+ $ TEST_SUITE_ID=1 bundle exec rake rspec_tracer:remote_cache:upload
199
238
  ```
200
239
 
201
240
  ## Sample Reports
@@ -214,6 +253,19 @@ These reports provide basic test information:
214
253
 
215
254
  ![](./readme_files/examples_report_next_run.png)
216
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
+
217
269
  ### Examples Dependency
218
270
 
219
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
@@ -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>
@@ -20,9 +20,6 @@ module RSpecTracer
20
20
  else
21
21
  'aws'
22
22
  end
23
- @test_suite_id = ENV['TEST_SUITE_ID'].to_s
24
- @test_suites = ENV.fetch('TEST_SUITES', '1').to_i
25
- @total_objects = CACHE_FILES_PER_TEST_SUITE * @test_suites
26
23
  end
27
24
 
28
25
  def download
@@ -32,10 +29,7 @@ module RSpecTracer
32
29
  return
33
30
  end
34
31
 
35
- @git = RSpecTracer::RemoteCache::Git.new
36
- @git.prepare_for_download
37
-
38
- @cache_sha = nearest_cache_sha
32
+ prepare_for_download
39
33
 
40
34
  if @cache_sha.nil?
41
35
  puts 'Could not find a suitable cache sha to download'
@@ -57,9 +51,7 @@ module RSpecTracer
57
51
  return
58
52
  end
59
53
 
60
- @run_id = last_run_id
61
- @git = RSpecTracer::RemoteCache::Git.new
62
-
54
+ prepare_for_upload
63
55
  upload_files
64
56
 
65
57
  puts "Uploaded cache from #{@upload_path} to #{@upload_prefix}"
@@ -69,20 +61,57 @@ module RSpecTracer
69
61
 
70
62
  private
71
63
 
64
+ def prepare_for_download
65
+ @test_suite_id = ENV['TEST_SUITE_ID']
66
+ @test_suites = ENV['TEST_SUITES']
67
+
68
+ if @test_suite_id.nil? ^ @test_suites.nil?
69
+ raise(
70
+ CacheDownloadError,
71
+ 'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
72
+ )
73
+ end
74
+
75
+ @git = RSpecTracer::RemoteCache::Git.new
76
+ @git.prepare_for_download
77
+
78
+ generate_cached_files_count_and_regex
79
+
80
+ @cache_sha = nearest_cache_sha
81
+ end
82
+
83
+ def generate_cached_files_count_and_regex
84
+ if @test_suites.nil?
85
+ @last_run_files_count = 1
86
+ @last_run_files_regex = '/%<ref>s/last_run.json$'
87
+ @cached_files_count = CACHE_FILES_PER_TEST_SUITE
88
+ @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
89
+ else
90
+ @test_suites = @test_suites.to_i
91
+ @test_suites_regex = (1..@test_suites).to_a.join('|')
92
+
93
+ @last_run_files_count = @test_suites
94
+ @last_run_files_regex = "/%<ref>s/(#{@test_suites_regex})/last_run.json$"
95
+ @cached_files_count = CACHE_FILES_PER_TEST_SUITE * @test_suites.to_i
96
+ @cached_files_regex = "/%<ref>s/(#{@test_suites_regex})/[0-9a-f]{32}/.+.json$"
97
+ end
98
+ end
99
+
72
100
  def nearest_cache_sha
73
101
  @git.ref_list.detect do |ref|
74
- prefix = "#{@s3_uri}/#{ref}/#{@test_suite_id}/".sub(%r{/+$}, '/')
102
+ prefix = "#{@s3_uri}/#{ref}/"
75
103
 
76
104
  puts "Testing prefix #{prefix}"
77
105
 
78
- command = <<-COMMAND.strip.gsub(/\s+/, ' ')
79
- #{@aws_s3} s3 ls #{prefix}
80
- --recursive
81
- --summarize
82
- | grep 'Total Objects'
83
- COMMAND
106
+ objects = `#{@aws_s3} s3 ls #{prefix} --recursive`.chomp.split("\n")
107
+
108
+ last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
109
+
110
+ next if objects.count { |object| object.match?(last_run_regex) } != @last_run_files_count
84
111
 
85
- @total_objects == `#{command}`.chomp.split('Total Objects:').last.to_s.strip.to_i
112
+ cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
113
+
114
+ objects.count { |object| object.match?(cache_regex) } == @cached_files_count
86
115
  end
87
116
  end
88
117
 
@@ -90,10 +119,19 @@ module RSpecTracer
90
119
  @download_prefix = "#{@s3_uri}/#{@cache_sha}/#{@test_suite_id}/".sub(%r{/+$}, '/')
91
120
  @download_path = RSpecTracer.cache_path
92
121
 
93
- return if system(
122
+ raise CacheDownloadError, 'Failed to download cache files' unless system(
94
123
  @aws_s3, 's3', 'cp',
95
- @download_prefix,
124
+ File.join(@download_prefix, 'last_run.json'),
96
125
  @download_path,
126
+ out: File::NULL, err: File::NULL
127
+ )
128
+
129
+ @run_id = last_run_id
130
+
131
+ return if system(
132
+ @aws_s3, 's3', 'cp',
133
+ File.join(@download_prefix, @run_id),
134
+ File.join(@download_path, @run_id),
97
135
  '--recursive',
98
136
  out: File::NULL, err: File::NULL
99
137
  )
@@ -103,22 +141,20 @@ module RSpecTracer
103
141
  raise CacheDownloadError, 'Failed to download cache files'
104
142
  end
105
143
 
106
- def last_run_id
107
- file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
108
-
109
- return unless File.file?(file_name)
110
-
111
- run_id = JSON.parse(File.read(file_name))['run_id']
112
-
113
- raise LocalCacheNotFoundError, 'Could not find any local cache to upload' if run_id.nil?
144
+ def prepare_for_upload
145
+ @git = RSpecTracer::RemoteCache::Git.new
146
+ @test_suite_id = ENV['TEST_SUITE_ID']
147
+ @upload_prefix = if @test_suite_id.nil?
148
+ "#{@s3_uri}/#{@git.branch_ref}/"
149
+ else
150
+ "#{@s3_uri}/#{@git.branch_ref}/#{@test_suite_id}/"
151
+ end
114
152
 
115
- run_id
153
+ @upload_path = RSpecTracer.cache_path
154
+ @run_id = last_run_id
116
155
  end
117
156
 
118
157
  def upload_files
119
- @upload_prefix = "#{@s3_uri}/#{@git.branch_ref}/#{@test_suite_id}/".sub(%r{/+$}, '/')
120
- @upload_path = RSpecTracer.cache_path
121
-
122
158
  return if system(
123
159
  @aws_s3, 's3', 'cp',
124
160
  File.join(@upload_path, 'last_run.json'),
@@ -127,13 +163,25 @@ module RSpecTracer
127
163
  ) && system(
128
164
  @aws_s3, 's3', 'cp',
129
165
  File.join(@upload_path, @run_id),
130
- "#{@upload_prefix}/#{@run_id}",
166
+ File.join(@upload_prefix, @run_id),
131
167
  '--recursive',
132
168
  out: File::NULL, err: File::NULL
133
169
  )
134
170
 
135
171
  raise CacheUploadError, 'Failed to upload cache files'
136
172
  end
173
+
174
+ def last_run_id
175
+ file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
176
+
177
+ return unless File.file?(file_name)
178
+
179
+ run_id = JSON.parse(File.read(file_name))['run_id']
180
+
181
+ raise LocalCacheNotFoundError, 'Could not find any local cache to upload' if run_id.nil?
182
+
183
+ run_id
184
+ end
137
185
  end
138
186
  end
139
187
  end
@@ -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.4.0'
4
+ VERSION = '0.6.2'
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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-tracer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.2
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-02 00:00:00.000000000 Z
11
+ date: 2021-09-07 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.4.0
110
+ source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v0.6.2
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: