rspec-tracer 0.9.0 → 1.0.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.
@@ -2,9 +2,12 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Reporter
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
5
+ attr_reader :all_examples, :interrupted_examples, :duplicate_examples,
6
+ :possibly_flaky_examples, :flaky_examples, :pending_examples,
7
+ :skipped_examples, :failed_examples, :all_files, :modified_files,
8
+ :deleted_files, :dependency, :examples_coverage
9
+
10
+ attr_accessor :reverse_dependency, :last_run
8
11
 
9
12
  def initialize
10
13
  initialize_examples
@@ -18,27 +21,54 @@ module RSpecTracer
18
21
  @duplicate_examples[example[:example_id]] << example
19
22
  end
20
23
 
24
+ def deregister_duplicate_examples
25
+ @duplicate_examples.select! { |_, examples| examples.count > 1 }
26
+
27
+ return if @duplicate_examples.empty?
28
+
29
+ @all_examples.reject! { |example_id, _| @duplicate_examples.key?(example_id) }
30
+ end
31
+
21
32
  def on_example_skipped(example_id)
22
33
  @skipped_examples << example_id
23
34
  end
24
35
 
25
36
  def on_example_passed(example_id, result)
37
+ return if @duplicate_examples.key?(example_id)
38
+
26
39
  @passed_examples << example_id
27
40
  @all_examples[example_id][:execution_result] = formatted_execution_result(result)
28
41
  end
29
42
 
30
43
  def on_example_failed(example_id, result)
44
+ return if @duplicate_examples.key?(example_id)
45
+
31
46
  @failed_examples << example_id
32
47
  @all_examples[example_id][:execution_result] = formatted_execution_result(result)
33
48
  end
34
49
 
35
50
  def on_example_pending(example_id, result)
51
+ return if @duplicate_examples.key?(example_id)
52
+
36
53
  @pending_examples << example_id
37
54
  @all_examples[example_id][:execution_result] = formatted_execution_result(result)
38
55
  end
39
56
 
57
+ def register_interrupted_examples
58
+ @all_examples.each_pair do |example_id, example|
59
+ next if example.key?(:execution_result)
60
+
61
+ @interrupted_examples << example_id
62
+ end
63
+
64
+ return if @interrupted_examples.empty?
65
+
66
+ puts "RSpec tracer is not processing #{@interrupted_examples.count} interrupted examples"
67
+ end
68
+
40
69
  def register_deleted_examples(seen_examples)
41
70
  @deleted_examples = seen_examples.keys.to_set - (@skipped_examples | @all_examples.keys)
71
+ @deleted_examples -= @interrupted_examples
42
72
 
43
73
  @deleted_examples.select! do |example_id|
44
74
  example = seen_examples[example_id]
@@ -63,6 +93,14 @@ module RSpecTracer
63
93
  @pending_examples << example_id
64
94
  end
65
95
 
96
+ def duplicate_example?(example_id)
97
+ @duplicate_examples.key?(example_id)
98
+ end
99
+
100
+ def example_interrupted?(example_id)
101
+ @interrupted_examples.include?(example_id)
102
+ end
103
+
66
104
  def example_passed?(example_id)
67
105
  @passed_examples.include?(example_id)
68
106
  end
@@ -107,17 +145,6 @@ module RSpecTracer
107
145
  file_deleted?(file_name) || file_modified?(file_name)
108
146
  end
109
147
 
110
- def incorrect_analysis?
111
- @duplicate_examples.select! { |_, examples| examples.count > 1 }
112
-
113
- return false if @duplicate_examples.empty?
114
-
115
- print_not_use_notice
116
- print_duplicate_examples
117
-
118
- true
119
- end
120
-
121
148
  def register_dependency(example_id, file_name)
122
149
  @dependency[example_id] << file_name
123
150
  end
@@ -126,62 +153,12 @@ module RSpecTracer
126
153
  @examples_coverage = examples_coverage
127
154
  end
128
155
 
129
- def generate_reverse_dependency_report
130
- @dependency.each_pair do |example_id, files|
131
- example_file = @all_examples[example_id][:rerun_file_name]
132
-
133
- files.each do |file_name|
134
- @reverse_dependency[file_name][:example_count] += 1
135
- @reverse_dependency[file_name][:examples][example_file] += 1
136
- end
137
- end
138
-
139
- format_reverse_dependency_report
140
- end
141
-
142
- def generate_last_run_report
143
- @last_run = {
144
- run_id: @run_id,
145
- pid: RSpecTracer.pid,
146
- actual_count: RSpec.world.example_count + @skipped_examples.count,
147
- example_count: RSpec.world.example_count,
148
- failed_examples: @failed_examples.count,
149
- skipped_examples: @skipped_examples.count,
150
- pending_examples: @pending_examples.count
151
- }
152
- end
153
-
154
- def write_reports
155
- starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
-
157
- @run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
158
- @cache_dir = File.join(RSpecTracer.cache_path, @run_id)
159
-
160
- FileUtils.mkdir_p(@cache_dir)
161
-
162
- %i[
163
- all_examples
164
- flaky_examples
165
- failed_examples
166
- pending_examples
167
- all_files
168
- dependency
169
- reverse_dependency
170
- examples_coverage
171
- last_run
172
- ].each { |report_type| send("write_#{report_type}_report") }
173
-
174
- ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
175
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
176
-
177
- puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
178
- end
179
-
180
156
  private
181
157
 
182
158
  def initialize_examples
183
159
  @all_examples = {}
184
160
  @duplicate_examples = Hash.new { |examples, example_id| examples[example_id] = [] }
161
+ @interrupted_examples = Set.new
185
162
  @passed_examples = Set.new
186
163
  @possibly_flaky_examples = Set.new
187
164
  @flaky_examples = Set.new
@@ -223,114 +200,5 @@ module RSpecTracer
223
200
  status: result.status.to_s
224
201
  }
225
202
  end
226
-
227
- def format_reverse_dependency_report
228
- @reverse_dependency.transform_values! do |data|
229
- {
230
- example_count: data[:example_count],
231
- examples: data[:examples].sort_by { |file_name, count| [-count, file_name] }.to_h
232
- }
233
- end
234
-
235
- report = @reverse_dependency.sort_by do |file_name, data|
236
- [-data[:example_count], file_name]
237
- end
238
-
239
- @reverse_dependency = report.to_h
240
- end
241
-
242
- def print_not_use_notice
243
- justify = ' ' * 4
244
- four_justify = justify * 4
245
-
246
- puts '=' * 80
247
- puts "#{four_justify}IMPORTANT NOTICE -- DO NOT USE RSPEC TRACER"
248
- puts '=' * 80
249
- puts "#{justify}It would be best to make changes so that the RSpec tracer can uniquely"
250
- puts "#{justify}identify all the examples, and then you can enable the RSpec tracer back."
251
- puts '=' * 80
252
- puts
253
- end
254
-
255
- # rubocop:disable Metrics/AbcSize
256
- def print_duplicate_examples
257
- total = @duplicate_examples.sum { |_, examples| examples.length }
258
-
259
- puts "RSpec tracer could not uniquely identify the following #{total} examples:"
260
-
261
- justify = ' ' * 2
262
- nested_justify = justify * 3
263
-
264
- @duplicate_examples.each_pair do |example_id, examples|
265
- puts "#{justify}- Example ID: #{example_id} (#{examples.count} examples)"
266
-
267
- examples.each do |example|
268
- description = example[:full_description].strip
269
- file_name = example[:rerun_file_name].sub(%r{^/}, '')
270
- line_number = example[:rerun_line_number]
271
- location = "#{file_name}:#{line_number}"
272
-
273
- puts "#{nested_justify}* #{description} (#{location})"
274
- end
275
- end
276
-
277
- puts
278
- end
279
- # rubocop:enable Metrics/AbcSize
280
-
281
- def write_all_examples_report
282
- file_name = File.join(@cache_dir, 'all_examples.json')
283
-
284
- File.write(file_name, JSON.pretty_generate(@all_examples))
285
- end
286
-
287
- def write_flaky_examples_report
288
- file_name = File.join(@cache_dir, 'flaky_examples.json')
289
-
290
- File.write(file_name, JSON.pretty_generate(@flaky_examples.to_a))
291
- end
292
-
293
- def write_failed_examples_report
294
- file_name = File.join(@cache_dir, 'failed_examples.json')
295
-
296
- File.write(file_name, JSON.pretty_generate(@failed_examples.to_a))
297
- end
298
-
299
- def write_pending_examples_report
300
- file_name = File.join(@cache_dir, 'pending_examples.json')
301
-
302
- File.write(file_name, JSON.pretty_generate(@pending_examples.to_a))
303
- end
304
-
305
- def write_all_files_report
306
- file_name = File.join(@cache_dir, 'all_files.json')
307
-
308
- File.write(file_name, JSON.pretty_generate(@all_files))
309
- end
310
-
311
- def write_dependency_report
312
- file_name = File.join(@cache_dir, 'dependency.json')
313
-
314
- File.write(file_name, JSON.pretty_generate(@dependency))
315
- end
316
-
317
- def write_reverse_dependency_report
318
- file_name = File.join(@cache_dir, 'reverse_dependency.json')
319
-
320
- File.write(file_name, JSON.pretty_generate(@reverse_dependency))
321
- end
322
-
323
- def write_examples_coverage_report
324
- file_name = File.join(@cache_dir, 'examples_coverage.json')
325
-
326
- File.write(file_name, JSON.pretty_generate(@examples_coverage))
327
- end
328
-
329
- def write_last_run_report
330
- file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
331
- last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
332
-
333
- File.write(file_name, JSON.pretty_generate(last_run_data))
334
- end
335
203
  end
336
204
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module RSpecTracer
4
4
  module RSpecRunner
5
- # rubocop:disable Metrics/AbcSize
6
5
  def run_specs(example_groups)
7
6
  actual_count = RSpec.world.example_count
8
7
  RSpecTracer.no_examples = actual_count.zero?
@@ -23,18 +22,17 @@ module RSpecTracer
23
22
 
24
23
  current_count = RSpec.world.example_count
25
24
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
26
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
25
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
27
26
 
28
27
  puts
29
28
  puts <<-EXAMPLES.strip.gsub(/\s+/, ' ')
30
29
  RSpec tracer is running #{current_count} examples (actual: #{actual_count},
31
- skipped: #{actual_count - current_count}) (took #{elpased})
30
+ skipped: #{actual_count - current_count}) (took #{elapsed})
32
31
  EXAMPLES
33
32
 
34
33
  RSpecTracer.running = true
35
34
 
36
35
  super(filtered_example_groups)
37
36
  end
38
- # rubocop:enable Metrics/AbcSize
39
37
  end
40
38
  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
+ interrupted: 'Interrupted previously',
11
12
  flaky_example: 'Flaky example',
12
13
  failed_example: 'Failed previously',
13
14
  pending_example: 'Pending previously',
@@ -21,8 +22,6 @@ module RSpecTracer
21
22
  @reporter = RSpecTracer::Reporter.new
22
23
  @filtered_examples = {}
23
24
 
24
- return if @cache.run_id.nil?
25
-
26
25
  @cache.load_cache_for_run
27
26
  filter_examples_to_run
28
27
  end
@@ -43,6 +42,10 @@ module RSpecTracer
43
42
  @reporter.register_example(example)
44
43
  end
45
44
 
45
+ def deregister_duplicate_examples
46
+ @reporter.deregister_duplicate_examples
47
+ end
48
+
46
49
  def on_example_skipped(example_id)
47
50
  @reporter.on_example_skipped(example_id)
48
51
  end
@@ -59,15 +62,14 @@ module RSpecTracer
59
62
  @reporter.on_example_pending(example_id, execution_result)
60
63
  end
61
64
 
62
- def register_deleted_examples
63
- @reporter.register_deleted_examples(@cache.all_examples)
65
+ def register_interrupted_examples
66
+ @reporter.register_interrupted_examples
64
67
  end
65
68
 
66
- def incorrect_analysis?
67
- @reporter.incorrect_analysis?
69
+ def register_deleted_examples
70
+ @reporter.register_deleted_examples(@cache.all_examples)
68
71
  end
69
72
 
70
- # rubocop:disable Metrics/AbcSize
71
73
  def generate_missed_coverage
72
74
  missed_coverage = Hash.new do |files_coverage, file_path|
73
75
  files_coverage[file_path] = Hash.new do |strength, line_number|
@@ -77,6 +79,9 @@ module RSpecTracer
77
79
 
78
80
  @cache.cached_examples_coverage.each_pair do |example_id, example_coverage|
79
81
  example_coverage.each_pair do |file_path, line_coverage|
82
+ next if @reporter.example_interrupted?(example_id) ||
83
+ @reporter.duplicate_example?(example_id)
84
+
80
85
  next unless @reporter.example_skipped?(example_id)
81
86
 
82
87
  file_name = RSpecTracer::SourceFile.file_name(file_path)
@@ -91,12 +96,14 @@ module RSpecTracer
91
96
 
92
97
  missed_coverage
93
98
  end
94
- # rubocop:enable Metrics/AbcSize
95
99
 
96
100
  def register_dependency(examples_coverage)
97
101
  filtered_files = Set.new
98
102
 
99
103
  examples_coverage.each_pair do |example_id, example_coverage|
104
+ next if @reporter.example_interrupted?(example_id) ||
105
+ @reporter.duplicate_example?(example_id)
106
+
100
107
  register_example_files_dependency(example_id)
101
108
 
102
109
  example_coverage.each_key do |file_path|
@@ -122,6 +129,9 @@ module RSpecTracer
122
129
  @reporter.register_source_file(source_file)
123
130
 
124
131
  @reporter.all_examples.each_key do |example_id|
132
+ next if @reporter.example_interrupted?(example_id) ||
133
+ @reporter.duplicate_example?(example_id)
134
+
125
135
  @reporter.register_dependency(example_id, source_file[:file_name])
126
136
  end
127
137
  end
@@ -131,22 +141,9 @@ module RSpecTracer
131
141
  @reporter.register_examples_coverage(examples_coverage)
132
142
  end
133
143
 
134
- def generate_report
135
- @reporter.generate_last_run_report
136
- generate_examples_status_report
137
-
138
- %i[all_files all_examples dependency examples_coverage reverse_dependency].each do |report_type|
139
- starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
140
-
141
- send("generate_#{report_type}_report")
142
-
143
- ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
144
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
145
-
146
- puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elpased})" if RSpecTracer.verbose?
147
- end
148
-
149
- @reporter.write_reports
144
+ def non_zero_exit_code?
145
+ !@reporter.duplicate_examples.empty? &&
146
+ ENV.fetch('RSPEC_TRACER_FAIL_ON_DUPLICATES', 'true') == 'true'
150
147
  end
151
148
 
152
149
  private
@@ -163,12 +160,13 @@ module RSpecTracer
163
160
  filter_by_files_changed
164
161
 
165
162
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
166
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
163
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
167
164
 
168
- puts "RSpec tracer processed cache (took #{elpased})" if RSpecTracer.verbose?
165
+ puts "RSpec tracer processed cache (took #{elapsed})" if RSpecTracer.verbose?
169
166
  end
170
167
 
171
168
  def filter_by_example_status
169
+ add_previously_interrupted_examples
172
170
  add_previously_flaky_examples
173
171
  add_previously_failed_examples
174
172
  add_previously_pending_examples
@@ -183,10 +181,17 @@ module RSpecTracer
183
181
  end
184
182
  end
185
183
 
184
+ def add_previously_interrupted_examples
185
+ @cache.interrupted_examples.each do |example_id|
186
+ @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:interrupted]
187
+ end
188
+ end
189
+
186
190
  def add_previously_flaky_examples
187
191
  @cache.flaky_examples.each do |example_id|
188
192
  @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:flaky_example]
189
193
 
194
+ next unless @cache.dependency.key?(example_id)
190
195
  next unless (@changed_files & @cache.dependency[example_id]).empty?
191
196
 
192
197
  @reporter.register_possibly_flaky_example(example_id)
@@ -199,6 +204,7 @@ module RSpecTracer
199
204
 
200
205
  @filtered_examples[example_id] = EXAMPLE_RUN_REASON[:failed_example]
201
206
 
207
+ next unless @cache.dependency.key?(example_id)
202
208
  next unless (@changed_files & @cache.dependency[example_id]).empty?
203
209
 
204
210
  @reporter.register_possibly_flaky_example(example_id)
@@ -247,6 +253,9 @@ module RSpecTracer
247
253
  end
248
254
 
249
255
  def register_example_files_dependency(example_id)
256
+ return if @reporter.example_interrupted?(example_id) ||
257
+ @reporter.duplicate_example?(example_id)
258
+
250
259
  example = @reporter.all_examples[example_id]
251
260
 
252
261
  register_example_file_dependency(example_id, example[:file_name])
@@ -257,6 +266,9 @@ module RSpecTracer
257
266
  end
258
267
 
259
268
  def register_example_file_dependency(example_id, file_name)
269
+ return if @reporter.example_interrupted?(example_id) ||
270
+ @reporter.duplicate_example?(example_id)
271
+
260
272
  source_file = RSpecTracer::SourceFile.from_name(file_name)
261
273
 
262
274
  @reporter.register_source_file(source_file)
@@ -264,6 +276,9 @@ module RSpecTracer
264
276
  end
265
277
 
266
278
  def register_file_dependency(example_id, file_path)
279
+ return if @reporter.example_interrupted?(example_id) ||
280
+ @reporter.duplicate_example?(example_id)
281
+
267
282
  source_file = RSpecTracer::SourceFile.from_path(file_path)
268
283
 
269
284
  return false if RSpecTracer.filters.any? { |filter| filter.match?(source_file) }
@@ -273,92 +288,5 @@ module RSpecTracer
273
288
 
274
289
  true
275
290
  end
276
-
277
- def generate_examples_status_report
278
- starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
279
-
280
- generate_flaky_examples_report
281
- generate_failed_examples_report
282
- generate_pending_examples_report
283
-
284
- ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
285
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
286
-
287
- puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elpased})" if RSpecTracer.verbose?
288
- end
289
-
290
- def generate_all_files_report
291
- @cache.all_files.each_pair do |file_name, data|
292
- next if @reporter.all_files.key?(file_name) ||
293
- @reporter.file_deleted?(file_name)
294
-
295
- @reporter.all_files[file_name] = data
296
- end
297
- end
298
-
299
- def generate_all_examples_report
300
- @cache.all_examples.each_pair do |example_id, data|
301
- next if @reporter.all_examples.key?(example_id) ||
302
- @reporter.example_deleted?(example_id)
303
-
304
- @reporter.all_examples[example_id] = data
305
- end
306
- end
307
-
308
- def generate_flaky_examples_report
309
- @reporter.possibly_flaky_examples.each do |example_id|
310
- next if @reporter.example_deleted?(example_id)
311
- next unless @cache.flaky_examples.include?(example_id) ||
312
- @reporter.example_passed?(example_id)
313
-
314
- @reporter.register_flaky_example(example_id)
315
- end
316
- end
317
-
318
- def generate_failed_examples_report
319
- @cache.failed_examples.each do |example_id|
320
- next if @reporter.example_deleted?(example_id) ||
321
- @reporter.all_examples.key?(example_id)
322
-
323
- @reporter.register_failed_example(example_id)
324
- end
325
- end
326
-
327
- def generate_pending_examples_report
328
- @cache.pending_examples.each do |example_id|
329
- next if @reporter.example_deleted?(example_id) ||
330
- @reporter.all_examples.key?(example_id)
331
-
332
- @reporter.register_pending_example(example_id)
333
- end
334
- end
335
-
336
- def generate_dependency_report
337
- @cache.dependency.each_pair do |example_id, data|
338
- next if @reporter.dependency.key?(example_id) ||
339
- @reporter.example_deleted?(example_id)
340
-
341
- @reporter.dependency[example_id] = data.reject do |file_name|
342
- @reporter.file_deleted?(file_name)
343
- end
344
- end
345
-
346
- @reporter.dependency.transform_values!(&:to_a)
347
- end
348
-
349
- def generate_examples_coverage_report
350
- @cache.cached_examples_coverage.each_pair do |example_id, data|
351
- next if @reporter.examples_coverage.key?(example_id) ||
352
- @reporter.example_deleted?(example_id)
353
-
354
- @reporter.examples_coverage[example_id] = data.reject do |file_name|
355
- @reporter.file_deleted?(file_name)
356
- end
357
- end
358
- end
359
-
360
- def generate_reverse_dependency_report
361
- @reporter.generate_reverse_dependency_report
362
- end
363
291
  end
364
292
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '0.9.0'
4
+ VERSION = '1.0.0'
5
5
  end