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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ module RSpecRunner
5
+ def run_specs(_example_groups)
6
+ actual_count = RSpec.world.example_count
7
+ filtered_examples, example_groups = RSpecTracer.filter_examples
8
+
9
+ RSpec.world.instance_variable_set(:@filtered_examples, filtered_examples)
10
+ RSpec.world.instance_variable_set(:@example_groups, example_groups)
11
+
12
+ current_count = RSpec.world.example_count
13
+
14
+ puts
15
+ puts <<-EXAMPLES.strip.gsub(/\s+/, ' ')
16
+ RSpec tracer is running #{current_count} examples (actual: #{actual_count},
17
+ skipped: #{actual_count - current_count})
18
+ EXAMPLES
19
+
20
+ RSpecTracer.running = true
21
+
22
+ super(example_groups)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ module RubyCoverage
5
+ def result
6
+ RSpecTracer.coverage_reporter.coverage
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cache'
4
+ require_relative 'reporter'
5
+
6
+ module RSpecTracer
7
+ class Runner
8
+ EXAMPLE_RUN_REASON = {
9
+ explicit_run: 'Explicit run',
10
+ no_cache: 'No cache',
11
+ failed_example: 'Failed previously',
12
+ pending_example: 'Pending previously',
13
+ files_changed: 'Files changed'
14
+ }.freeze
15
+
16
+ attr_reader :cache, :reporter
17
+
18
+ def initialize
19
+ @cache = RSpecTracer::Cache.new
20
+ @reporter = RSpecTracer::Reporter.new
21
+ @filtered_examples = {}
22
+
23
+ @cache.load_cache_for_run
24
+ filter_examples_to_run
25
+ end
26
+
27
+ def run_example?(example_id)
28
+ return true if explicit_run?
29
+
30
+ !@cache.all_examples.key?(example_id) || @filtered_examples.key?(example_id)
31
+ end
32
+
33
+ def run_example_reason(example_id)
34
+ return EXAMPLE_RUN_REASON[:explicit_run] if explicit_run?
35
+
36
+ @filtered_examples[example_id] || EXAMPLE_RUN_REASON[:no_cache]
37
+ end
38
+
39
+ def register_example(example)
40
+ @reporter.register_example(example)
41
+ end
42
+
43
+ def on_example_skipped(example_id)
44
+ @reporter.on_example_skipped(example_id)
45
+ end
46
+
47
+ def on_example_passed(example_id, execution_result)
48
+ @reporter.on_example_passed(example_id, execution_result)
49
+ end
50
+
51
+ def on_example_failed(example_id, execution_result)
52
+ @reporter.on_example_failed(example_id, execution_result)
53
+ end
54
+
55
+ def on_example_pending(example_id, execution_result)
56
+ @reporter.on_example_pending(example_id, execution_result)
57
+ end
58
+
59
+ def register_deleted_examples
60
+ @reporter.register_deleted_examples(@cache.all_examples)
61
+ end
62
+
63
+ # rubocop:disable Metrics/AbcSize
64
+ def generate_missed_coverage
65
+ missed_coverage = Hash.new do |files_coverage, file_path|
66
+ files_coverage[file_path] = Hash.new do |strength, line_number|
67
+ strength[line_number] = 0
68
+ end
69
+ end
70
+
71
+ @cache.cached_examples_coverage.each_pair do |example_id, example_coverage|
72
+ example_coverage.each_pair do |file_path, line_coverage|
73
+ next unless @reporter.example_skipped?(example_id)
74
+
75
+ file_name = RSpecTracer::SourceFile.file_name(file_path)
76
+
77
+ next if @reporter.file_deleted?(file_name)
78
+
79
+ line_coverage.each_pair do |line_number, strength|
80
+ missed_coverage[file_path][line_number] += strength
81
+ end
82
+ end
83
+ end
84
+
85
+ missed_coverage
86
+ end
87
+ # rubocop:enable Metrics/AbcSize
88
+
89
+ def register_dependency(examples_coverage)
90
+ examples_coverage.each_pair do |example_id, example_coverage|
91
+ register_example_files_dependency(example_id)
92
+
93
+ example_coverage.each_key do |file_path|
94
+ source_file = RSpecTracer::SourceFile.from_path(file_path)
95
+
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])
100
+ end
101
+ end
102
+
103
+ @reporter.pending_examples.each do |example_id|
104
+ register_example_files_dependency(example_id)
105
+ end
106
+ end
107
+
108
+ def register_untraced_dependency(trace_point_files)
109
+ untraced_files = generate_untraced_files(trace_point_files)
110
+
111
+ untraced_files.each do |file_path|
112
+ source_file = RSpecTracer::SourceFile.from_path(file_path)
113
+
114
+ next if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
115
+
116
+ @reporter.register_source_file(source_file)
117
+
118
+ @reporter.all_examples.each_key do |example_id|
119
+ @reporter.register_dependency(example_id, source_file[:file_name])
120
+ end
121
+ end
122
+ end
123
+
124
+ def register_examples_coverage(examples_coverage)
125
+ @reporter.register_examples_coverage(examples_coverage)
126
+ end
127
+
128
+ def generate_report
129
+ @reporter.generate_last_run_report
130
+
131
+ generate_failed_examples_report
132
+ generate_pending_examples_report
133
+
134
+ %i[all_files all_examples dependency examples_coverage].each do |report_type|
135
+ send("generate_#{report_type}_report")
136
+ end
137
+
138
+ @reporter.generate_reverse_dependency_report
139
+ @reporter.write_reports
140
+ end
141
+
142
+ private
143
+
144
+ def explicit_run?
145
+ ENV.fetch('RSPEC_TRACER_NO_SKIP', 'false') == 'true'
146
+ end
147
+
148
+ def filter_examples_to_run
149
+ add_previously_failed_examples
150
+ add_previously_pending_examples
151
+ filter_by_files_changed
152
+ end
153
+
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
158
+ end
159
+
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
164
+ end
165
+
166
+ def filter_by_files_changed
167
+ @cache.dependency.each_pair do |example_id, files|
168
+ next if @filtered_examples.key?(example_id)
169
+
170
+ files.each do |file_name|
171
+ break if filtered_by_file_changed?(example_id, file_name)
172
+ end
173
+ end
174
+ end
175
+
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]
179
+
180
+ return true
181
+ end
182
+
183
+ source_file = registered_source_file(file_name)
184
+
185
+ return false if source_file &&
186
+ @cache.all_files[file_name][:digest] == source_file[:digest]
187
+
188
+ @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:files_changed]
189
+
190
+ if source_file.nil?
191
+ @reporter.on_file_deleted(file_name)
192
+ else
193
+ @reporter.on_file_modified(file_name)
194
+ end
195
+
196
+ true
197
+ end
198
+
199
+ def generate_untraced_files(trace_point_files)
200
+ all_files = @reporter.all_files
201
+ .each_value
202
+ .with_object([]) { |source_file, files| files << source_file[:file_path] }
203
+
204
+ (trace_point_files | fetch_rspec_required_files) - all_files
205
+ end
206
+
207
+ def fetch_rspec_required_files
208
+ rspec_root = RSpec::Core::RubyProject.root
209
+ rspec_path = RSpec.configuration.default_path
210
+
211
+ RSpec.configuration.requires.map do |file_name|
212
+ file_name = "#{file_name}.rb" if File.extname(file_name).empty?
213
+
214
+ File.join(rspec_root, rspec_path, file_name)
215
+ end
216
+ end
217
+
218
+ def register_example_files_dependency(example_id)
219
+ example = @reporter.all_examples[example_id]
220
+
221
+ register_example_file_dependency(example_id, example[:file_name])
222
+
223
+ return if example[:rerun_file_name] == example[:file_name]
224
+
225
+ register_example_file_dependency(example_id, example[:rerun_file_name])
226
+ end
227
+
228
+ def register_example_file_dependency(example_id, file_name)
229
+ source_file = registered_source_file(file_name)
230
+
231
+ @reporter.register_source_file(source_file)
232
+ @reporter.register_dependency(example_id, file_name)
233
+ end
234
+
235
+ def registered_source_file(file_name)
236
+ @reporter.all_files[file_name] || RSpecTracer::SourceFile.from_name(file_name)
237
+ end
238
+
239
+ def generate_all_files_report
240
+ @cache.all_files.each_pair do |file_name, data|
241
+ next if @reporter.all_files.key?(file_name) ||
242
+ @reporter.file_deleted?(file_name)
243
+
244
+ @reporter.all_files[file_name] = data
245
+ end
246
+ end
247
+
248
+ def generate_all_examples_report
249
+ @cache.all_examples.each_pair do |example_id, data|
250
+ next if @reporter.all_examples.key?(example_id) ||
251
+ @reporter.example_deleted?(example_id)
252
+
253
+ @reporter.all_examples[example_id] = data
254
+ end
255
+ end
256
+
257
+ def generate_failed_examples_report
258
+ @cache.failed_examples.each do |example_id|
259
+ next if @reporter.example_deleted?(example_id) ||
260
+ @reporter.all_examples.key?(example_id)
261
+
262
+ @reporter.register_failed_example(example_id)
263
+ end
264
+ end
265
+
266
+ def generate_pending_examples_report
267
+ @cache.pending_examples.each do |example_id|
268
+ next if @reporter.example_deleted?(example_id) ||
269
+ @reporter.all_examples.key?(example_id)
270
+
271
+ @reporter.register_pending_example(example_id)
272
+ end
273
+ end
274
+
275
+ def generate_dependency_report
276
+ @cache.dependency.each_pair do |example_id, data|
277
+ next if @reporter.dependency.key?(example_id) ||
278
+ @reporter.example_deleted?(example_id)
279
+
280
+ @reporter.dependency[example_id] = data.reject do |file_name|
281
+ @reporter.file_deleted?(file_name)
282
+ end
283
+ end
284
+
285
+ @reporter.dependency.transform_values!(&:to_a)
286
+ end
287
+
288
+ def generate_examples_coverage_report
289
+ @cache.cached_examples_coverage.each_pair do |example_id, data|
290
+ next if @reporter.examples_coverage.key?(example_id) ||
291
+ @reporter.example_deleted?(example_id)
292
+
293
+ @reporter.examples_coverage[example_id] = data.reject do |file_name|
294
+ @reporter.file_deleted?(file_name)
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ module SourceFile
5
+ PROJECT_ROOT_REGEX = Regexp.new("^#{Regexp.escape(RSpecTracer.root)}").freeze
6
+
7
+ module_function
8
+
9
+ def from_path(file_path)
10
+ return unless File.file?(file_path)
11
+
12
+ {
13
+ file_path: file_path,
14
+ file_name: file_name(file_path),
15
+ digest: Digest::MD5.hexdigest(File.read(file_path))
16
+ }
17
+ end
18
+
19
+ def from_name(file_name)
20
+ from_path(file_path(file_name))
21
+ end
22
+
23
+ def file_name(file_path)
24
+ file_path.sub(PROJECT_ROOT_REGEX, '')
25
+ end
26
+
27
+ def file_path(file_name)
28
+ File.expand_path(file_name.sub(%r{^/}, ''), RSpecTracer.root)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ require 'digest/md5'
6
+ require 'docile'
7
+ require 'fileutils'
8
+ require 'forwardable'
9
+ require 'json'
10
+ require 'pry'
11
+ require 'set'
12
+
13
+ require_relative 'rspec_tracer/configuration'
14
+ RSpecTracer.extend RSpecTracer::Configuration
15
+
16
+ require_relative 'rspec_tracer/coverage_reporter'
17
+ require_relative 'rspec_tracer/defaults'
18
+ require_relative 'rspec_tracer/example'
19
+ require_relative 'rspec_tracer/html_reporter/reporter'
20
+ require_relative 'rspec_tracer/rspec_reporter'
21
+ require_relative 'rspec_tracer/rspec_runner'
22
+ require_relative 'rspec_tracer/ruby_coverage'
23
+ require_relative 'rspec_tracer/runner'
24
+ require_relative 'rspec_tracer/source_file'
25
+ require_relative 'rspec_tracer/version'
26
+
27
+ module RSpecTracer
28
+ class << self
29
+ attr_accessor :running, :pid
30
+
31
+ def start(&block)
32
+ RSpecTracer.running = false
33
+ RSpecTracer.pid = Process.pid
34
+
35
+ puts 'Started RSpec tracer'
36
+
37
+ configure(&block) if block
38
+
39
+ initial_setup
40
+ end
41
+
42
+ # rubocop:disable Metrics/AbcSize
43
+ def filter_examples
44
+ groups = Set.new
45
+ to_run = Hash.new { |hash, group| hash[group] = [] }
46
+
47
+ RSpec.world.filtered_examples.each_pair do |example_group, examples|
48
+ examples.each do |example|
49
+ tracer_example = RSpecTracer::Example.from(example)
50
+ example_id = tracer_example[:example_id]
51
+ example.metadata[:rspec_tracer_example_id] = example_id
52
+
53
+ if runner.run_example?(example_id)
54
+ run_reason = runner.run_example_reason(example_id)
55
+ tracer_example[:run_reason] = run_reason
56
+ example.metadata[:description] = "#{example.description} (#{run_reason})"
57
+
58
+ to_run[example_group] << example
59
+ groups << example.example_group.parent_groups.last
60
+
61
+ runner.register_example(tracer_example)
62
+ else
63
+ runner.on_example_skipped(example_id)
64
+ end
65
+ end
66
+ end
67
+
68
+ [to_run, groups.to_a]
69
+ end
70
+ # rubocop:enable Metrics/AbcSize
71
+
72
+ def at_exit_behavior
73
+ return unless RSpecTracer.pid == Process.pid && RSpecTracer.running
74
+
75
+ run_exit_tasks
76
+ end
77
+
78
+ def start_example_trace
79
+ trace_point.enable if trace_example?
80
+ end
81
+
82
+ def stop_example_trace(success)
83
+ return unless trace_example?
84
+
85
+ trace_point.disable
86
+
87
+ unless success
88
+ @traced_files = Set.new
89
+
90
+ return
91
+ end
92
+
93
+ @trace_example = false
94
+ end
95
+
96
+ def runner
97
+ return @runner if defined?(@runner)
98
+ end
99
+
100
+ def coverage_reporter
101
+ return @coverage_reporter if defined?(@coverage_reporter)
102
+ end
103
+
104
+ def trace_point
105
+ return @trace_point if defined?(@trace_point)
106
+ end
107
+
108
+ def traced_files
109
+ return @traced_files if defined?(@traced_files)
110
+ end
111
+
112
+ def trace_example?
113
+ defined?(@trace_example) ? @trace_example : false
114
+ end
115
+
116
+ def simplecov?
117
+ return @simplecov if defined?(@simplecov)
118
+ end
119
+
120
+ private
121
+
122
+ def initial_setup
123
+ unless setup_rspec
124
+ puts 'Could not find a running RSpec process'
125
+
126
+ return
127
+ end
128
+
129
+ setup_coverage
130
+ setup_trace_point
131
+
132
+ @runner = RSpecTracer::Runner.new
133
+ @coverage_reporter = RSpecTracer::CoverageReporter.new
134
+ end
135
+
136
+ def setup_rspec
137
+ runners = ObjectSpace.each_object(::RSpec::Core::Runner) do |runner|
138
+ runner_clazz = runner.singleton_class
139
+ clazz = RSpecTracer::RSpecRunner
140
+
141
+ runner_clazz.prepend(clazz) unless runner_clazz.ancestors.include?(clazz)
142
+
143
+ reporter_clazz = runner.configuration.reporter.singleton_class
144
+ clazz = RSpecTracer::RSpecReporter
145
+
146
+ reporter_clazz.prepend(clazz) unless reporter_clazz.ancestors.include?(clazz)
147
+ end
148
+
149
+ runners.positive?
150
+ end
151
+
152
+ def setup_coverage
153
+ @simplecov = defined?(SimpleCov) && SimpleCov.running
154
+
155
+ if simplecov?
156
+ # rubocop:disable Lint/EmptyBlock
157
+ SimpleCov.at_exit {}
158
+ # rubocop:enable Lint/EmptyBlock
159
+ else
160
+ require 'coverage'
161
+
162
+ ::Coverage.start
163
+ end
164
+ end
165
+
166
+ def setup_trace_point
167
+ @trace_example = true
168
+ @traced_files = Set.new
169
+
170
+ @trace_point = TracePoint.new(:call) do |tp|
171
+ RSpecTracer.traced_files << tp.path if tp.path.start_with?(RSpecTracer.root)
172
+ end
173
+ end
174
+
175
+ def run_exit_tasks
176
+ generate_reports
177
+
178
+ simplecov? ? run_simplecov_exit_task : run_coverage_exit_task
179
+ ensure
180
+ RSpecTracer.running = false
181
+ end
182
+
183
+ def generate_reports
184
+ puts 'RSpec tracer is generating reports'
185
+
186
+ generate_tracer_reports
187
+ generate_coverage_reports
188
+ runner.generate_report
189
+ RSpecTracer::HTMLReporter::Reporter.new.generate_report
190
+ end
191
+
192
+ def generate_tracer_reports
193
+ runner.register_deleted_examples
194
+ runner.register_dependency(coverage_reporter.examples_coverage)
195
+ runner.register_untraced_dependency(@traced_files)
196
+ end
197
+
198
+ def generate_coverage_reports
199
+ coverage_reporter.generate_final_examples_coverage
200
+ coverage_reporter.merge_coverage(runner.generate_missed_coverage)
201
+ runner.register_examples_coverage(coverage_reporter.examples_coverage)
202
+ end
203
+
204
+ def run_simplecov_exit_task
205
+ coverage_clazz = ::Coverage.singleton_class
206
+ clazz = RSpecTracer::RubyCoverage
207
+ coverage_clazz.prepend(clazz) unless coverage_clazz.ancestors.include?(clazz)
208
+
209
+ puts 'SimpleCov will now generate coverage report (<3 RSpec tracer)'
210
+
211
+ SimpleCov.result.format!
212
+ end
213
+
214
+ def run_coverage_exit_task
215
+ coverage_reporter.generate_final_coverage
216
+
217
+ file_name = File.join(RSpecTracer.coverage_path, 'coverage.json')
218
+
219
+ write_coverage_report(file_name)
220
+ print_coverage_stats(file_name)
221
+ end
222
+
223
+ def write_coverage_report(file_name)
224
+ report = {
225
+ RSpecTracer: {
226
+ coverage: coverage_reporter.coverage,
227
+ timestamp: Time.now.utc.to_i
228
+ }
229
+ }
230
+
231
+ File.write(file_name, JSON.pretty_generate(report))
232
+ end
233
+
234
+ def print_coverage_stats(file_name)
235
+ stat = coverage_reporter.coverage_stat
236
+
237
+ puts <<-REPORT.strip.gsub(/\s+/, ' ')
238
+ Coverage report generated for RSpecTracer to #{file_name}. #{stat[:covered_lines]}
239
+ / #{stat[:total_lines]} LOC (#{stat[:covered_percent]}%) covered
240
+ REPORT
241
+ end
242
+ end
243
+ end