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