rspec-tracer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/LICENSE +21 -0
  4. data/README.md +248 -0
  5. data/lib/rspec_tracer/cache.rb +109 -0
  6. data/lib/rspec_tracer/configuration.rb +134 -0
  7. data/lib/rspec_tracer/coverage_reporter.rb +179 -0
  8. data/lib/rspec_tracer/defaults.rb +10 -0
  9. data/lib/rspec_tracer/example.rb +58 -0
  10. data/lib/rspec_tracer/filter.rb +68 -0
  11. data/lib/rspec_tracer/html_reporter/assets/javascripts/application.js +56 -0
  12. data/lib/rspec_tracer/html_reporter/assets/javascripts/libraries/jquery.js +10881 -0
  13. data/lib/rspec_tracer/html_reporter/assets/javascripts/plugins/datatables.js +15381 -0
  14. data/lib/rspec_tracer/html_reporter/assets/stylesheets/application.css +196 -0
  15. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/datatables.css +459 -0
  16. data/lib/rspec_tracer/html_reporter/assets/stylesheets/plugins/jquery-ui.css +436 -0
  17. data/lib/rspec_tracer/html_reporter/assets/stylesheets/print.css +92 -0
  18. data/lib/rspec_tracer/html_reporter/assets/stylesheets/reset.css +265 -0
  19. data/lib/rspec_tracer/html_reporter/public/application.css +5 -0
  20. data/lib/rspec_tracer/html_reporter/public/application.js +6 -0
  21. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc.png +0 -0
  22. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_asc_disabled.png +0 -0
  23. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_both.png +0 -0
  24. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc.png +0 -0
  25. data/lib/rspec_tracer/html_reporter/public/datatables/images/sort_desc_disabled.png +0 -0
  26. data/lib/rspec_tracer/html_reporter/public/favicon.png +0 -0
  27. data/lib/rspec_tracer/html_reporter/public/loading.gif +0 -0
  28. data/lib/rspec_tracer/html_reporter/reporter.rb +180 -0
  29. data/lib/rspec_tracer/html_reporter/views/examples.erb +53 -0
  30. data/lib/rspec_tracer/html_reporter/views/examples_dependency.erb +36 -0
  31. data/lib/rspec_tracer/html_reporter/views/files_dependency.erb +36 -0
  32. data/lib/rspec_tracer/html_reporter/views/layout.erb +32 -0
  33. data/lib/rspec_tracer/reporter.rb +255 -0
  34. data/lib/rspec_tracer/rspec_reporter.rb +43 -0
  35. data/lib/rspec_tracer/rspec_runner.rb +25 -0
  36. data/lib/rspec_tracer/ruby_coverage.rb +9 -0
  37. data/lib/rspec_tracer/runner.rb +299 -0
  38. data/lib/rspec_tracer/source_file.rb +31 -0
  39. data/lib/rspec_tracer/version.rb +5 -0
  40. data/lib/rspec_tracer.rb +243 -0
  41. metadata +122 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fd68858a56e3f240a0710c3dc4d7af601268bc2f4029c3a67f5e93ccd301dc48
4
+ data.tar.gz: b89cb78a0fef102e99dd60ccb53b45238d0785d912750c13b9546e0e2fcbe6f3
5
+ SHA512:
6
+ metadata.gz: 6e203ace8d29199999af09cd82418e0160160dae0077b5f4cb56c9c4de7bf40c25c5506e5fbc2d9081af4e7e69c47a80b62ce0a5de94aaee473f9506c62b4d83
7
+ data.tar.gz: f955b25c90b5fad1a0f0d4dd1c46f6c3b8c7e1c7d9d5a330a2cb4a13ba543d5264cd0fcdf7fcd545f44fa8d5c636dc6194341cb1ea484a22fd37c925b254d05c
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-08-27
4
+
5
+ **Initial Release**
6
+
7
+ ### Added
8
+
9
+ - Ability to run RSpec Tracer with SimpleCov and without SimpleCov
10
+ - Support for HTML reports
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Abhimanyu Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,248 @@
1
+ ![](./readme_files/rspec_tracer.png)
2
+
3
+ RSpec Tracer is a **specs dependency analysis tool** and a **test skipper for RSpec**.
4
+ It maintains a list of files for each test, enabling itself to skip tests in the
5
+ subsequent runs if none of the dependent files are changed.
6
+
7
+ It uses [Ruby's built-in coverage library](https://ruby-doc.org/stdlib/libdoc/coverage/rdoc/Coverage.html)
8
+ to keep track of the coverage for each test. For each test executed, the coverage
9
+ diff provides the desired file list. RSpec Tracer takes care of reporting the
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.
12
+
13
+ Knowing the examples and files dependency gives us a better insight into the codebase,
14
+ and we have **a clear idea of what to test for when making any changes**. With this data,
15
+ we can also analyze the coupling between different components and much more.
16
+
17
+ **First Run**
18
+ ![](./readme_files/first_run.gif)
19
+
20
+ **Next Run**
21
+ ![](./readme_files/next_run.gif)
22
+
23
+ ## Note
24
+
25
+ **RSpec Tracer is currently available for use in the local development
26
+ environment only.**
27
+
28
+ ## Installation
29
+
30
+ Add this line to your `Gemfile` and `bundle install`:
31
+ ```ruby
32
+ gem 'rspec-tracer', group: :test, require: false
33
+ ```
34
+
35
+ And, add the followings to your `.gitignore`:
36
+ ```
37
+ /rspec_tracer_cache/
38
+ /rspec_tracer_coverage/
39
+ /rspec_tracer_report/
40
+ ```
41
+
42
+ ### Compatibility
43
+
44
+ RSpec Tracer requires **Ruby 2.5+** and **rspec-core >= 3.6.0**. If you are using
45
+ SimpleCov, it is recommended to use **simplecov >= 0.12.0**.
46
+
47
+ ## Getting Started
48
+
49
+ 1. **Load and Start RSpec Tracer**
50
+
51
+ - **With SimpleCov**
52
+
53
+ If you are using `SimpleCov`, load RSpec Tracer right after the SimpleCov load
54
+ and launch:
55
+
56
+ ```ruby
57
+ require 'simplecov'
58
+ SimpleCov.start
59
+
60
+ # Load RSpec Tracer
61
+ require 'rspec-tracer'
62
+ RSpecTracer.start
63
+ ```
64
+
65
+ Currently using RSpec Tracer with SimpleCov has the following two limitations:
66
+ - SimpleCov **won't be able to provide branch coverage report** even when enabled.
67
+ - RSpec Tracer **nullifies the `SimpleCov.at_exit`** callback.
68
+
69
+ - **Without SimpleCov**
70
+
71
+ Load and launch RSpec Tracer at the very top of `spec_helper.rb` (or `rails_helper.rb`,
72
+ `test/test_helper.rb`). Note that `RSpecTracer.start` must be issued **before loading
73
+ any of the application code.**
74
+
75
+ ```ruby
76
+ # Load RSpec Tracer
77
+ require 'rspec-tracer'
78
+ RSpecTracer.start
79
+ ```
80
+
81
+ 2. Run the tests with RSpec using `bundle exec rspec`.
82
+ 3. After running your tests, open `rspec_tracer_report/index.html` in the
83
+ browser of your choice.
84
+
85
+ ## Environment Variables
86
+
87
+ To get better control on execution, you can use the following two environment variables:
88
+
89
+ ### RSPEC_TRACER_NO_SKIP
90
+
91
+ The default value is `false.` If set to `true`, the RSpec Tracer will not skip
92
+ any tests. Note that it will continue to maintain cache files and generate reports.
93
+
94
+ ```ruby
95
+ RSPEC_TRACER_NO_SKIP=true bundle exec rspec
96
+ ```
97
+
98
+ ### TEST_SUITE_ID
99
+
100
+ If you have a large set of tests to run, it is recommended to run them in
101
+ separate groups. This way, RSpec Tracer is not overwhelmed with loading massive
102
+ cached data in the memory. Also, it generate and use cache for specific test suites
103
+ and not merge them.
104
+
105
+ ```ruby
106
+ TEST_SUITE_ID=1 bundle exec rspec spec/models
107
+ TEST_SUITE_ID=2 bundle exec rspec spec/helpers
108
+ ```
109
+
110
+ ## Sample Reports
111
+
112
+ You get the following three reports:
113
+
114
+ ### Examples
115
+
116
+ These reports provide basic test information:
117
+
118
+ **First Run**
119
+
120
+ ![](./readme_files/examples_report_first_run.png)
121
+
122
+ **Next Run**
123
+
124
+ ![](./readme_files/examples_report_next_run.png)
125
+
126
+ ### Examples Dependency
127
+
128
+ These reports show a list of dependent files for each test.
129
+
130
+ ![](./readme_files/examples_dependency_report.png)
131
+
132
+ ### Files Dependency
133
+
134
+ These reports provide information on the total number of tests that will run after changing this particular file.
135
+
136
+ ![](./readme_files/files_dependency_report.png)
137
+
138
+ ## Configuring RSpec Tracer
139
+
140
+ Configuration settings can be applied in three formats, which are completely equivalent:
141
+
142
+ - The most common way is to configure it directly in your start block:
143
+
144
+ ```ruby
145
+ RSpecTracer.start do
146
+ config_option 'foo'
147
+ end
148
+ ```
149
+ - You can also set all configuration options directly:
150
+
151
+ ```ruby
152
+ RSpecTracer.config_option 'foo'
153
+ ```
154
+
155
+ - If you do not want to start tracer immediately after launch or want to add
156
+ additional configuration later on in a concise way, use:
157
+
158
+ ```ruby
159
+ RSpecTracer.configure do
160
+ config_option 'foo'
161
+ end
162
+ ```
163
+
164
+ ## Filters
165
+
166
+ RSpec Tracer supports two types of filters:
167
+
168
+ - To exclude selected files from the dependency list of tests:
169
+
170
+ ```ruby
171
+ RSpecTracer.start do
172
+ add_filter %r{^/helpers/}
173
+ end
174
+ ```
175
+ - To exclude selected files from the coverage data. You should only use this
176
+ when not using SimpleCov.
177
+
178
+ ```ruby
179
+ RSpecTracer.start do
180
+ add_coverage_filter %r{^/tasks/}
181
+ end
182
+ ```
183
+
184
+ By default, a filter is applied that removes all files OUTSIDE of your project's
185
+ root directory - otherwise you'd end up with the source files in the gems you are
186
+ using as tests dependency.
187
+
188
+ ### Defining Custom Filteres
189
+
190
+ You can currently define a filter using either a String or Regexp (that will then
191
+ be Regexp-matched against each source file's path), a block or by passing in your
192
+ own Filter class.
193
+
194
+ #### String Filter
195
+
196
+ ```ruby
197
+ RSpecTracer.start do
198
+ add_filter '/helpers/'
199
+ end
200
+ ```
201
+
202
+ This simple string filter will remove all files that match "/helpers/" in their path.
203
+
204
+ #### Regex Filter
205
+
206
+ ```ruby
207
+ RSpecTracer.start do
208
+ add_filter %r{^/helpers/}
209
+ end
210
+ ```
211
+
212
+ This simple regex filter will remove all files that start with /helper/ in their path.
213
+
214
+ #### Block Filter
215
+
216
+ ```ruby
217
+ RSpecTracer.start do
218
+ add_filter do |source_file|
219
+ source_file[:file_path].include?('/helpers/')
220
+ end
221
+ end
222
+ ```
223
+
224
+ Block filters receive a `Hash` object and expect your block to return either true
225
+ (if the file is to be removed from the result) or false (if the result should be kept).
226
+ In the above example, the filter will remove all files that match "/helpers/" in their path.
227
+
228
+ #### Array Filter
229
+
230
+ ```ruby
231
+ RSpecTracer.start do
232
+ add_filter ['/helpers/', %r{^/utils/}]
233
+ end
234
+ ```
235
+
236
+ You can pass in an array containing any of the other filter types.
237
+
238
+ ## Contributing
239
+
240
+ Read the [contribution guide](https://github.com/avmnu-sng/rspec-tracer/blob/main/.github/CONTRIBUTING.md).
241
+
242
+ ## License
243
+
244
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
245
+
246
+ ## Code of Conduct
247
+
248
+ Everyone interacting in the Rspec Tracer project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [Code of Conduct](https://github.com/avmnu-sng/rspec-tracer/blob/main/.github/CODE_OF_CONDUCT.md).
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class Cache
5
+ attr_reader :all_examples, :failed_examples, :pending_examples, :all_files, :dependency
6
+
7
+ def initialize
8
+ @run_id = last_run_id
9
+ @cache_dir = File.join(RSpecTracer.cache_path, @run_id) if @run_id
10
+
11
+ @cached = false
12
+
13
+ @all_examples = {}
14
+ @failed_examples = Set.new
15
+ @pending_examples = Set.new
16
+ @all_files = {}
17
+ @dependency = Hash.new { |hash, key| hash[key] = Set.new }
18
+ end
19
+
20
+ def load_cache_for_run
21
+ return if @run_id.nil? || @cached
22
+
23
+ load_all_examples_cache
24
+ load_failed_examples_cache
25
+ load_pending_examples_cache
26
+ load_all_files_cache
27
+ load_dependency_cache
28
+
29
+ @cached = true
30
+
31
+ puts "RSpec tracer loaded cache from #{@cache_dir}" if @run_id
32
+ end
33
+
34
+ def cached_examples_coverage
35
+ return @examples_coverage if defined?(@examples_coverage)
36
+ return @examples_coverage = {} if @run_id.nil?
37
+
38
+ load_examples_coverage_cache
39
+ end
40
+
41
+ private
42
+
43
+ def last_run_id
44
+ file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
45
+
46
+ return unless File.file?(file_name)
47
+
48
+ JSON.parse(File.read(file_name))['run_id']
49
+ end
50
+
51
+ def load_all_examples_cache
52
+ file_name = File.join(@cache_dir, 'all_examples.json')
53
+
54
+ return unless File.file?(file_name)
55
+
56
+ @all_examples = JSON.parse(File.read(file_name)).transform_values do |examples|
57
+ examples.transform_keys(&:to_sym)
58
+ end
59
+
60
+ @all_examples.each_value do |example|
61
+ example[:execution_result].transform_keys!(&:to_sym)
62
+
63
+ example[:run_reason] = nil
64
+ end
65
+ end
66
+
67
+ def load_failed_examples_cache
68
+ file_name = File.join(@cache_dir, 'failed_examples.json')
69
+
70
+ return unless File.file?(file_name)
71
+
72
+ @failed_examples = JSON.parse(File.read(file_name)).to_set
73
+ end
74
+
75
+ def load_pending_examples_cache
76
+ file_name = File.join(@cache_dir, 'pending_examples.json')
77
+
78
+ return unless File.file?(file_name)
79
+
80
+ @pending_examples = JSON.parse(File.read(file_name)).to_set
81
+ end
82
+
83
+ def load_all_files_cache
84
+ file_name = File.join(@cache_dir, 'all_files.json')
85
+
86
+ return unless File.file?(file_name)
87
+
88
+ @all_files = JSON.parse(File.read(file_name)).transform_values do |files|
89
+ files.transform_keys(&:to_sym)
90
+ end
91
+ end
92
+
93
+ def load_dependency_cache
94
+ file_name = File.join(@cache_dir, 'dependency.json')
95
+
96
+ return unless File.file?(file_name)
97
+
98
+ @dependency = JSON.parse(File.read(file_name)).transform_values(&:to_set)
99
+ end
100
+
101
+ def load_examples_coverage_cache
102
+ file_name = File.join(@cache_dir, 'examples_coverage.json')
103
+
104
+ return unless File.file?(file_name)
105
+
106
+ @examples_coverage = JSON.parse(File.read(file_name))
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'filter'
4
+
5
+ module RSpecTracer
6
+ module Configuration
7
+ DEFAULT_CACHE_DIR = 'rspec_tracer_cache'
8
+ DEFAULT_REPORT_DIR = 'rspec_tracer_report'
9
+ DEFAULT_COVERAGE_DIR = 'rspec_tracer_coverage'
10
+
11
+ attr_writer :filters, :coverage_filters
12
+
13
+ def root(root = nil)
14
+ return @root if defined?(@root) && root.nil?
15
+
16
+ @cache_path = nil
17
+ @root = File.expand_path(root || Dir.getwd)
18
+ end
19
+
20
+ def project_name(proj_name = nil)
21
+ return @project_name if defined?(@project_name) && proj_name.nil?
22
+
23
+ @project_name = proj_name if proj_name.is_a?(String)
24
+ @project_name ||= File.basename(root).capitalize
25
+ end
26
+
27
+ def cache_dir(dir = nil)
28
+ return @cache_dir if defined?(@cache_dir) && dir.nil?
29
+
30
+ @cache_path = nil
31
+ @cache_dir = dir || DEFAULT_CACHE_DIR
32
+ end
33
+
34
+ def cache_path
35
+ @cache_path ||= begin
36
+ cache_path = File.expand_path(cache_dir, root)
37
+ cache_path = File.join(cache_path, ENV['TEST_SUITE_ID'].to_s)
38
+
39
+ FileUtils.mkdir_p(cache_path)
40
+
41
+ cache_path
42
+ end
43
+ end
44
+
45
+ def report_dir(dir = nil)
46
+ return @report_dir if defined?(@report_dir) && dir.nil?
47
+
48
+ @report_path = nil
49
+ @report_dir = dir || DEFAULT_REPORT_DIR
50
+ end
51
+
52
+ def report_path
53
+ @report_path || begin
54
+ report_path = File.expand_path(report_dir, root)
55
+ report_path = File.join(report_path, ENV['TEST_SUITE_ID'].to_s)
56
+
57
+ FileUtils.mkdir_p(report_path)
58
+
59
+ report_path
60
+ end
61
+ end
62
+
63
+ def coverage_dir(dir = nil)
64
+ return @coverage_dir if defined?(@coverage_dir) && dir.nil?
65
+
66
+ @coverage_path = nil
67
+ @coverage_dir = dir || DEFAULT_COVERAGE_DIR
68
+ end
69
+
70
+ def coverage_path
71
+ @coverage_path ||= begin
72
+ coverage_path = File.expand_path(coverage_dir, root)
73
+ coverage_path = File.join(coverage_path, ENV['TEST_SUITE_ID'].to_s)
74
+
75
+ FileUtils.mkdir_p(coverage_path)
76
+
77
+ coverage_path
78
+ end
79
+ end
80
+
81
+ def coverage_track_files(glob)
82
+ @coverage_track_files = glob
83
+ end
84
+
85
+ def coverage_tracked_files
86
+ @coverage_track_files if defined?(@coverage_track_files)
87
+ end
88
+
89
+ def add_filter(filter = nil, &block)
90
+ filters << parse_filter(filter, &block)
91
+ end
92
+
93
+ def filters
94
+ @filters ||= []
95
+ end
96
+
97
+ def add_coverage_filter(filter = nil, &block)
98
+ coverage_filters << parse_filter(filter, &block)
99
+ end
100
+
101
+ def coverage_filters
102
+ @coverage_filters ||= []
103
+ end
104
+
105
+ def configure(&block)
106
+ Docile.dsl_eval(self, &block)
107
+ end
108
+
109
+ private
110
+
111
+ def test_suite_id
112
+ suite_id = ENV.fetch('TEST_SUITE_ID', '')
113
+
114
+ return if suite_id.empty?
115
+
116
+ suite_id
117
+ end
118
+
119
+ def at_exit(&block)
120
+ return Proc.new unless RSpecTracer.running || block
121
+
122
+ @at_exit = block if block
123
+ @at_exit ||= proc { RSpecTracer.at_exit_behavior }
124
+ end
125
+
126
+ def parse_filter(filter = nil, &block)
127
+ arg = filter || block
128
+
129
+ raise ArgumentError, 'Either a filter or a block required' if arg.nil?
130
+
131
+ RSpecTracer::Filter.register(arg)
132
+ end
133
+ end
134
+ end