rspec-tracer 0.4.0 → 0.6.2

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: 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: