rspec-tracer 0.9.3 → 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.
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class ReportGenerator
5
+ def initialize(reporter, cache)
6
+ @reporter = reporter
7
+ @cache = cache
8
+ end
9
+
10
+ def reverse_dependency_report
11
+ reverse_dependency = Hash.new do |examples, file_name|
12
+ examples[file_name] = {
13
+ example_count: 0,
14
+ examples: Hash.new(0)
15
+ }
16
+ end
17
+
18
+ @reporter.dependency.each_pair do |example_id, files|
19
+ next if @reporter.interrupted_examples.include?(example_id)
20
+
21
+ example_file = @reporter.all_examples[example_id][:rerun_file_name]
22
+
23
+ files.each do |file_name|
24
+ reverse_dependency[file_name][:example_count] += 1
25
+ reverse_dependency[file_name][:examples][example_file] += 1
26
+ end
27
+ end
28
+
29
+ reverse_dependency.transform_values! do |data|
30
+ {
31
+ example_count: data[:example_count],
32
+ examples: data[:examples].sort_by { |file_name, count| [-count, file_name] }.to_h
33
+ }
34
+ end
35
+
36
+ reverse_dependency.sort_by { |file_name, data| [-data[:example_count], file_name] }.to_h
37
+ end
38
+
39
+ def generate_report
40
+ generate_last_run_report
41
+ generate_examples_status_report
42
+
43
+ %i[all_files all_examples dependency examples_coverage reverse_dependency].each do |report_type|
44
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
+
46
+ send("generate_#{report_type}_report")
47
+
48
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
50
+
51
+ puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elapsed})" if RSpecTracer.verbose?
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def generate_last_run_report
58
+ @reporter.last_run = {
59
+ pid: RSpecTracer.pid,
60
+ actual_count: RSpec.world.example_count + @reporter.skipped_examples.count,
61
+ example_count: RSpec.world.example_count,
62
+ duplicate_examples: @reporter.duplicate_examples.sum { |_, examples| examples.count },
63
+ interrupted_examples: @reporter.interrupted_examples.count,
64
+ failed_examples: @reporter.failed_examples.count,
65
+ skipped_examples: @reporter.skipped_examples.count,
66
+ pending_examples: @reporter.pending_examples.count,
67
+ flaky_examples: @reporter.flaky_examples.count
68
+ }
69
+ end
70
+
71
+ def generate_examples_status_report
72
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+
74
+ generate_flaky_examples_report
75
+ generate_failed_examples_report
76
+ generate_pending_examples_report
77
+
78
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
80
+
81
+ puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elapsed})" if RSpecTracer.verbose?
82
+ end
83
+
84
+ def generate_flaky_examples_report
85
+ @reporter.possibly_flaky_examples.each do |example_id|
86
+ next if @reporter.example_deleted?(example_id)
87
+ next unless @cache.flaky_examples.include?(example_id) ||
88
+ @reporter.example_passed?(example_id)
89
+
90
+ @reporter.register_flaky_example(example_id)
91
+ end
92
+ end
93
+
94
+ def generate_failed_examples_report
95
+ @cache.failed_examples.each do |example_id|
96
+ next if @reporter.example_deleted?(example_id) ||
97
+ @reporter.all_examples.key?(example_id)
98
+
99
+ @reporter.register_failed_example(example_id)
100
+ end
101
+ end
102
+
103
+ def generate_pending_examples_report
104
+ @cache.pending_examples.each do |example_id|
105
+ next if @reporter.example_deleted?(example_id) ||
106
+ @reporter.all_examples.key?(example_id)
107
+
108
+ @reporter.register_pending_example(example_id)
109
+ end
110
+ end
111
+
112
+ def generate_all_files_report
113
+ @cache.all_files.each_pair do |file_name, data|
114
+ next if @reporter.all_files.key?(file_name) ||
115
+ @reporter.file_deleted?(file_name)
116
+
117
+ @reporter.all_files[file_name] = data
118
+ end
119
+ end
120
+
121
+ def generate_all_examples_report
122
+ @cache.all_examples.each_pair do |example_id, data|
123
+ next if @reporter.all_examples.key?(example_id) ||
124
+ @reporter.example_deleted?(example_id)
125
+
126
+ @reporter.all_examples[example_id] = data
127
+ end
128
+ end
129
+
130
+ def generate_dependency_report
131
+ @cache.dependency.each_pair do |example_id, data|
132
+ next if @reporter.dependency.key?(example_id) ||
133
+ @reporter.example_deleted?(example_id)
134
+
135
+ @reporter.dependency[example_id] = data.reject do |file_name|
136
+ @reporter.file_deleted?(file_name)
137
+ end
138
+ end
139
+
140
+ @reporter.dependency.transform_values!(&:to_a)
141
+ end
142
+
143
+ def generate_examples_coverage_report
144
+ @cache.cached_examples_coverage.each_pair do |example_id, data|
145
+ next if @reporter.examples_coverage.key?(example_id) ||
146
+ @reporter.example_deleted?(example_id)
147
+
148
+ @reporter.examples_coverage[example_id] = data.reject do |file_name|
149
+ @reporter.file_deleted?(file_name)
150
+ end
151
+ end
152
+ end
153
+
154
+ def generate_reverse_dependency_report
155
+ @reporter.reverse_dependency = reverse_dependency_report
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class ReportMerger
5
+ attr_reader :all_examples, :duplicate_examples, :interrupted_examples,
6
+ :flaky_examples, :failed_examples, :pending_examples, :skipped_examples,
7
+ :all_files, :dependency, :reverse_dependency, :examples_coverage, :last_run
8
+
9
+ def initialize
10
+ @last_run = {}
11
+ @all_examples = {}
12
+ @duplicate_examples = {}
13
+ @interrupted_examples = Set.new
14
+ @flaky_examples = Set.new
15
+ @failed_examples = Set.new
16
+ @pending_examples = Set.new
17
+ @skipped_examples = Set.new
18
+ @all_files = {}
19
+ @dependency = Hash.new { |hash, key| hash[key] = Set.new }
20
+ @reverse_dependency = {}
21
+ @examples_coverage = {}
22
+ end
23
+
24
+ def merge(reports_dir)
25
+ reports_dir.each do |report_dir|
26
+ next unless File.directory?(report_dir)
27
+
28
+ merge_cache(load_cache(report_dir))
29
+ merge_last_run_report(File.dirname(report_dir))
30
+ end
31
+
32
+ @dependency.transform_values!(&:to_a)
33
+
34
+ @reverse_dependency = RSpecTracer::ReportGenerator.new(self, nil).reverse_dependency_report
35
+ end
36
+
37
+ private
38
+
39
+ def load_cache(cache_dir)
40
+ cache = RSpecTracer::Cache.new
41
+
42
+ cache.send(:load_all_examples_cache, cache_dir, discard_run_reason: false)
43
+ cache.send(:load_duplicate_examples_cache, cache_dir)
44
+ cache.send(:load_interrupted_examples_cache, cache_dir)
45
+ cache.send(:load_flaky_examples_cache, cache_dir)
46
+ cache.send(:load_failed_examples_cache, cache_dir)
47
+ cache.send(:load_pending_examples_cache, cache_dir)
48
+ cache.send(:load_skipped_examples_cache, cache_dir)
49
+ cache.send(:load_all_files_cache, cache_dir)
50
+ cache.send(:load_dependency_cache, cache_dir)
51
+ cache.send(:load_examples_coverage_cache, cache_dir)
52
+
53
+ cache
54
+ end
55
+
56
+ def merge_cache(cache)
57
+ @all_examples.merge!(cache.all_examples) { |_, v1, v2| v1[:run_reason] ? v1 : v2 }
58
+ @duplicate_examples.merge!(cache.duplicate_examples) { |_, v1, v2| v1 + v2 }
59
+ @interrupted_examples.merge(cache.interrupted_examples)
60
+ @flaky_examples.merge(cache.flaky_examples)
61
+ @failed_examples.merge(cache.failed_examples)
62
+ @pending_examples.merge(cache.pending_examples)
63
+ @skipped_examples.merge(cache.skipped_examples)
64
+ @all_files.merge!(cache.all_files)
65
+ @dependency.merge!(cache.dependency) { |_, v1, v2| v1.merge(v2) }
66
+ @examples_coverage.merge!(cache.examples_coverage) do |_, v1, v2|
67
+ v1.merge(v2) { |_, v3, v4| v3.merge(v4) { |_, v5, v6| v5 + v6 } }
68
+ end
69
+ end
70
+
71
+ def merge_last_run_report(cache_dir)
72
+ file_name = File.join(cache_dir, 'last_run.json')
73
+ cached_last_run = JSON.parse(File.read(file_name), symbolize_names: true)
74
+ cached_last_run[:pid] = [cached_last_run[:pid]]
75
+
76
+ cached_last_run.delete_if { |key, _| %i[run_id timestamp].include?(key) }
77
+
78
+ @last_run.merge!(cached_last_run) { |_, v1, v2| v1 + v2 }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class ReportWriter
5
+ def initialize(report_dir, reporter)
6
+ @report_dir = report_dir
7
+ @reporter = reporter
8
+ end
9
+
10
+ def write_report
11
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
+
13
+ @run_id = Digest::MD5.hexdigest(@reporter.all_examples.keys.sort.to_json)
14
+ @cache_dir = File.join(@report_dir, @run_id)
15
+
16
+ FileUtils.mkdir_p(@cache_dir)
17
+
18
+ write_all_examples_report
19
+ write_duplicate_examples_report
20
+ write_interrupted_examples_report
21
+ write_flaky_examples_report
22
+ write_failed_examples_report
23
+ write_pending_examples_report
24
+ write_skipped_examples_report
25
+ write_all_files_report
26
+ write_dependency_report
27
+ write_reverse_dependency_report
28
+ write_examples_coverage_report
29
+ write_last_run_report
30
+
31
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
33
+
34
+ puts "RSpec tracer reports written to #{@cache_dir} (took #{elapsed})"
35
+ end
36
+
37
+ def print_duplicate_examples
38
+ return if @reporter.duplicate_examples.empty?
39
+
40
+ total = @reporter.duplicate_examples.sum { |_, examples| examples.count }
41
+
42
+ puts '=' * 80
43
+ puts ' IMPORTANT NOTICE -- RSPEC TRACER COULD NOT IDENTIFY SOME EXAMPLES UNIQUELY'
44
+ puts '=' * 80
45
+ puts "RSpec tracer could not uniquely identify the following #{total} examples:"
46
+
47
+ justify = ' ' * 2
48
+ nested_justify = justify * 3
49
+
50
+ @reporter.duplicate_examples.each_pair do |example_id, examples|
51
+ puts "#{justify}- Example ID: #{example_id} (#{examples.count} examples)"
52
+
53
+ examples.each do |example|
54
+ description = example[:full_description].strip
55
+ file_name = example[:rerun_file_name].sub(%r{^/}, '')
56
+ line_number = example[:rerun_line_number]
57
+ location = "#{file_name}:#{line_number}"
58
+
59
+ puts "#{nested_justify}* #{description} (#{location})"
60
+ end
61
+ end
62
+
63
+ puts
64
+ end
65
+
66
+ private
67
+
68
+ def write_all_examples_report
69
+ file_name = File.join(@cache_dir, 'all_examples.json')
70
+
71
+ File.write(file_name, JSON.pretty_generate(@reporter.all_examples))
72
+ end
73
+
74
+ def write_duplicate_examples_report
75
+ file_name = File.join(@cache_dir, 'duplicate_examples.json')
76
+
77
+ File.write(file_name, JSON.pretty_generate(@reporter.duplicate_examples))
78
+ end
79
+
80
+ def write_interrupted_examples_report
81
+ file_name = File.join(@cache_dir, 'interrupted_examples.json')
82
+
83
+ File.write(file_name, JSON.pretty_generate(@reporter.interrupted_examples.sort.to_a))
84
+ end
85
+
86
+ def write_flaky_examples_report
87
+ file_name = File.join(@cache_dir, 'flaky_examples.json')
88
+
89
+ File.write(file_name, JSON.pretty_generate(@reporter.flaky_examples.sort.to_a))
90
+ end
91
+
92
+ def write_failed_examples_report
93
+ file_name = File.join(@cache_dir, 'failed_examples.json')
94
+
95
+ File.write(file_name, JSON.pretty_generate(@reporter.failed_examples.sort.to_a))
96
+ end
97
+
98
+ def write_pending_examples_report
99
+ file_name = File.join(@cache_dir, 'pending_examples.json')
100
+
101
+ File.write(file_name, JSON.pretty_generate(@reporter.pending_examples.sort.to_a))
102
+ end
103
+
104
+ def write_skipped_examples_report
105
+ file_name = File.join(@cache_dir, 'skipped_examples.json')
106
+
107
+ File.write(file_name, JSON.pretty_generate(@reporter.skipped_examples.sort.to_a))
108
+ end
109
+
110
+ def write_all_files_report
111
+ file_name = File.join(@cache_dir, 'all_files.json')
112
+
113
+ File.write(file_name, JSON.pretty_generate(@reporter.all_files))
114
+ end
115
+
116
+ def write_dependency_report
117
+ file_name = File.join(@cache_dir, 'dependency.json')
118
+
119
+ File.write(file_name, JSON.pretty_generate(@reporter.dependency))
120
+ end
121
+
122
+ def write_reverse_dependency_report
123
+ file_name = File.join(@cache_dir, 'reverse_dependency.json')
124
+
125
+ File.write(file_name, JSON.pretty_generate(@reporter.reverse_dependency))
126
+ end
127
+
128
+ def write_examples_coverage_report
129
+ file_name = File.join(@cache_dir, 'examples_coverage.json')
130
+
131
+ File.write(file_name, JSON.pretty_generate(@reporter.examples_coverage))
132
+ end
133
+
134
+ def write_last_run_report
135
+ file_name = File.join(@report_dir, 'last_run.json')
136
+ last_run_data = @reporter.last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
137
+
138
+ File.write(file_name, JSON.pretty_generate(last_run_data))
139
+ end
140
+ end
141
+ end
@@ -4,8 +4,10 @@ module RSpecTracer
4
4
  class Reporter
5
5
  attr_reader :all_examples, :interrupted_examples, :duplicate_examples,
6
6
  :possibly_flaky_examples, :flaky_examples, :pending_examples,
7
- :all_files, :modified_files, :deleted_files, :dependency,
8
- :reverse_dependency, :examples_coverage, :last_run
7
+ :skipped_examples, :failed_examples, :all_files, :modified_files,
8
+ :deleted_files, :dependency, :examples_coverage
9
+
10
+ attr_accessor :reverse_dependency, :last_run
9
11
 
10
12
  def initialize
11
13
  initialize_examples
@@ -151,92 +153,6 @@ module RSpecTracer
151
153
  @examples_coverage = examples_coverage
152
154
  end
153
155
 
154
- def generate_reverse_dependency_report
155
- @dependency.each_pair do |example_id, files|
156
- next if @interrupted_examples.include?(example_id)
157
-
158
- example_file = @all_examples[example_id][:rerun_file_name]
159
-
160
- files.each do |file_name|
161
- @reverse_dependency[file_name][:example_count] += 1
162
- @reverse_dependency[file_name][:examples][example_file] += 1
163
- end
164
- end
165
-
166
- format_reverse_dependency_report
167
- end
168
-
169
- def generate_last_run_report
170
- @last_run = {
171
- pid: RSpecTracer.pid,
172
- actual_count: RSpec.world.example_count + @skipped_examples.count,
173
- example_count: RSpec.world.example_count,
174
- duplicate_examples: @duplicate_examples.sum { |_, examples| examples.count },
175
- interrupted_examples: @interrupted_examples.count,
176
- failed_examples: @failed_examples.count,
177
- skipped_examples: @skipped_examples.count,
178
- pending_examples: @pending_examples.count,
179
- flaky_examples: @flaky_examples.count
180
- }
181
- end
182
-
183
- def write_reports
184
- starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
185
-
186
- @run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
187
- @cache_dir = File.join(RSpecTracer.cache_path, @run_id)
188
-
189
- FileUtils.mkdir_p(@cache_dir)
190
-
191
- %i[
192
- all_examples
193
- flaky_examples
194
- failed_examples
195
- pending_examples
196
- all_files
197
- dependency
198
- reverse_dependency
199
- examples_coverage
200
- last_run
201
- ].each { |report_type| send("write_#{report_type}_report") }
202
-
203
- ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
204
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
205
-
206
- puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
207
- end
208
-
209
- # rubocop:disable Metrics/AbcSize
210
- def print_duplicate_examples
211
- return if @duplicate_examples.empty?
212
-
213
- total = @duplicate_examples.sum { |_, examples| examples.count }
214
-
215
- puts '=' * 80
216
- puts ' IMPORTANT NOTICE -- RSPEC TRACER COULD NOT IDENTIFY SOME EXAMPLES UNIQUELY'
217
- puts '=' * 80
218
- puts "RSpec tracer could not uniquely identify the following #{total} examples:"
219
-
220
- justify = ' ' * 2
221
- nested_justify = justify * 3
222
-
223
- @duplicate_examples.each_pair do |example_id, examples|
224
- puts "#{justify}- Example ID: #{example_id} (#{examples.count} examples)"
225
-
226
- examples.each do |example|
227
- description = example[:full_description].strip
228
- file_name = example[:rerun_file_name].sub(%r{^/}, '')
229
- line_number = example[:rerun_line_number]
230
- location = "#{file_name}:#{line_number}"
231
-
232
- puts "#{nested_justify}* #{description} (#{location})"
233
- end
234
- end
235
-
236
- puts
237
- end
238
- # rubocop:enable Metrics/AbcSize
239
-
240
156
  private
241
157
 
242
158
  def initialize_examples
@@ -284,75 +200,5 @@ module RSpecTracer
284
200
  status: result.status.to_s
285
201
  }
286
202
  end
287
-
288
- def format_reverse_dependency_report
289
- @reverse_dependency.transform_values! do |data|
290
- {
291
- example_count: data[:example_count],
292
- examples: data[:examples].sort_by { |file_name, count| [-count, file_name] }.to_h
293
- }
294
- end
295
-
296
- report = @reverse_dependency.sort_by do |file_name, data|
297
- [-data[:example_count], file_name]
298
- end
299
-
300
- @reverse_dependency = report.to_h
301
- end
302
-
303
- def write_all_examples_report
304
- file_name = File.join(@cache_dir, 'all_examples.json')
305
-
306
- File.write(file_name, JSON.pretty_generate(@all_examples))
307
- end
308
-
309
- def write_flaky_examples_report
310
- file_name = File.join(@cache_dir, 'flaky_examples.json')
311
-
312
- File.write(file_name, JSON.pretty_generate(@flaky_examples.to_a))
313
- end
314
-
315
- def write_failed_examples_report
316
- file_name = File.join(@cache_dir, 'failed_examples.json')
317
-
318
- File.write(file_name, JSON.pretty_generate(@failed_examples.to_a))
319
- end
320
-
321
- def write_pending_examples_report
322
- file_name = File.join(@cache_dir, 'pending_examples.json')
323
-
324
- File.write(file_name, JSON.pretty_generate(@pending_examples.to_a))
325
- end
326
-
327
- def write_all_files_report
328
- file_name = File.join(@cache_dir, 'all_files.json')
329
-
330
- File.write(file_name, JSON.pretty_generate(@all_files))
331
- end
332
-
333
- def write_dependency_report
334
- file_name = File.join(@cache_dir, 'dependency.json')
335
-
336
- File.write(file_name, JSON.pretty_generate(@dependency))
337
- end
338
-
339
- def write_reverse_dependency_report
340
- file_name = File.join(@cache_dir, 'reverse_dependency.json')
341
-
342
- File.write(file_name, JSON.pretty_generate(@reverse_dependency))
343
- end
344
-
345
- def write_examples_coverage_report
346
- file_name = File.join(@cache_dir, 'examples_coverage.json')
347
-
348
- File.write(file_name, JSON.pretty_generate(@examples_coverage))
349
- end
350
-
351
- def write_last_run_report
352
- file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
353
- last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
354
-
355
- File.write(file_name, JSON.pretty_generate(last_run_data))
356
- end
357
203
  end
358
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