rspec-tracer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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