rspec-tracer 0.9.0 → 1.0.0

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