rspec-tracer 0.3.0 → 0.6.1

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,38 @@
1
+ <div class="report_container" id="<%= title_id %>">
2
+ <h2>
3
+ <span class="group_name"><%= title %></span>
4
+ (
5
+ <span class="blue">
6
+ <strong><%= flaky_examples.count %></strong>
7
+ </span> examples
8
+ )
9
+ </h2>
10
+
11
+ <a name="<%= title_id %>"></a>
12
+
13
+ <div class="report-table--responsive">
14
+ <table class="report-table">
15
+ <thead>
16
+ <tr>
17
+ <th>ID</th>
18
+ <th>Description</th>
19
+ <th>Location</th>
20
+ <th>Result</th>
21
+ <th>Run At</th>
22
+ </tr>
23
+ </thead>
24
+
25
+ <tbody>
26
+ <% flaky_examples.each do |example| %>
27
+ <tr>
28
+ <td><%= example[:id] %></td>
29
+ <td><%= example[:description] %></td>
30
+ <td><%= example[:location] %></td>
31
+ <td><strong class="<%= example_result_css_class(example[:result]) %>"><%= example[:result] %></strong></td>
32
+ <td width="8%"><%= example[:last_run] %></td>
33
+ </tr>
34
+ <% end %>
35
+ </tbody>
36
+ </table>
37
+ </div>
38
+ </div>
@@ -19,6 +19,9 @@
19
19
 
20
20
  <div id="content">
21
21
  <%= formatted_examples('Examples', examples.values) %>
22
+ <% unless flaky_examples.empty? %>
23
+ <%= formatted_flaky_examples('Flaky Examples', flaky_examples) %>
24
+ <% end %>
22
25
  <%= formatted_examples_dependency('Examples Dependency', examples_dependency) %>
23
26
  <%= formatted_files_dependency("Files Dependency", files_dependency) %>
24
27
  </div>
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'git'
4
+
5
+ module RSpecTracer
6
+ module RemoteCache
7
+ class Cache
8
+ class CacheDownloadError < StandardError; end
9
+
10
+ class CacheUploadError < StandardError; end
11
+
12
+ class LocalCacheNotFoundError < StandardError; end
13
+
14
+ CACHE_FILES_PER_TEST_SUITE = 8
15
+
16
+ def initialize
17
+ @s3_uri = ENV['RSPEC_TRACER_S3_URI']
18
+ @aws_s3 = if ENV.fetch('LOCAL_AWS', 'false') == 'true'
19
+ 'awslocal'
20
+ else
21
+ 'aws'
22
+ end
23
+ end
24
+
25
+ def download
26
+ if @s3_uri.nil?
27
+ puts 'S3 URI is not configured'
28
+
29
+ return
30
+ end
31
+
32
+ prepare_for_download
33
+
34
+ if @cache_sha.nil?
35
+ puts 'Could not find a suitable cache sha to download'
36
+
37
+ return
38
+ end
39
+
40
+ download_files
41
+
42
+ puts "Downloaded cache from #{@download_prefix} to #{@download_path}"
43
+ rescue StandardError => e
44
+ puts "Errored: #{e.message}"
45
+ end
46
+
47
+ def upload
48
+ if @s3_uri.nil?
49
+ puts 'S3 URI is not configured'
50
+
51
+ return
52
+ end
53
+
54
+ prepare_for_upload
55
+ upload_files
56
+
57
+ puts "Uploaded cache from #{@upload_path} to #{@upload_prefix}"
58
+ rescue CacheUploadError => e
59
+ puts "Errored: #{e.message}"
60
+ end
61
+
62
+ private
63
+
64
+ def prepare_for_download
65
+ @test_suite_id = ENV['TEST_SUITE_ID']
66
+ @test_suites = ENV['TEST_SUITES']
67
+
68
+ if @test_suite_id.nil? ^ @test_suites.nil?
69
+ raise(
70
+ CacheDownloadError,
71
+ 'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
72
+ )
73
+ end
74
+
75
+ @git = RSpecTracer::RemoteCache::Git.new
76
+ @git.prepare_for_download
77
+
78
+ generate_cached_files_count_and_regex
79
+
80
+ @cache_sha = nearest_cache_sha
81
+ end
82
+
83
+ def generate_cached_files_count_and_regex
84
+ if @test_suites.nil?
85
+ @last_run_files_count = 1
86
+ @last_run_files_regex = '/%<ref>s/last_run.json$'
87
+ @cached_files_count = CACHE_FILES_PER_TEST_SUITE
88
+ @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
89
+ else
90
+ @test_suites = @test_suites.to_i
91
+ @test_suites_regex = (1..@test_suites).to_a.join('|')
92
+
93
+ @last_run_files_count = @test_suites
94
+ @last_run_files_regex = "/%<ref>s/(#{@test_suites_regex})/last_run.json$"
95
+ @cached_files_count = CACHE_FILES_PER_TEST_SUITE * @test_suites.to_i
96
+ @cached_files_regex = "/%<ref>s/(#{@test_suites_regex})/[0-9a-f]{32}/.+.json$"
97
+ end
98
+ end
99
+
100
+ def nearest_cache_sha
101
+ @git.ref_list.detect do |ref|
102
+ prefix = "#{@s3_uri}/#{ref}/"
103
+
104
+ puts "Testing prefix #{prefix}"
105
+
106
+ objects = `#{@aws_s3} s3 ls #{prefix} --recursive`.chomp.split("\n")
107
+
108
+ last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
109
+
110
+ next if objects.count { |object| object.match?(last_run_regex) } != @last_run_files_count
111
+
112
+ cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
113
+
114
+ objects.count { |object| object.match?(cache_regex) } == @cached_files_count
115
+ end
116
+ end
117
+
118
+ def download_files
119
+ @download_prefix = "#{@s3_uri}/#{@cache_sha}/#{@test_suite_id}/".sub(%r{/+$}, '/')
120
+ @download_path = RSpecTracer.cache_path
121
+
122
+ raise CacheDownloadError, 'Failed to download cache files' unless system(
123
+ @aws_s3, 's3', 'cp',
124
+ File.join(@download_prefix, 'last_run.json'),
125
+ @download_path,
126
+ out: File::NULL, err: File::NULL
127
+ )
128
+
129
+ @run_id = last_run_id
130
+
131
+ return if system(
132
+ @aws_s3, 's3', 'cp',
133
+ File.join(@download_prefix, @run_id),
134
+ File.join(@download_path, @run_id),
135
+ '--recursive',
136
+ out: File::NULL, err: File::NULL
137
+ )
138
+
139
+ FileUtils.rm_rf(@download_path)
140
+
141
+ raise CacheDownloadError, 'Failed to download cache files'
142
+ end
143
+
144
+ def prepare_for_upload
145
+ @git = RSpecTracer::RemoteCache::Git.new
146
+ @test_suite_id = ENV['TEST_SUITE_ID']
147
+ @upload_prefix = if @test_suite_id.nil?
148
+ "#{@s3_uri}/#{@git.branch_ref}/"
149
+ else
150
+ "#{@s3_uri}/#{@git.branch_ref}/#{@test_suite_id}/"
151
+ end
152
+
153
+ @upload_path = RSpecTracer.cache_path
154
+ @run_id = last_run_id
155
+ end
156
+
157
+ def upload_files
158
+ return if system(
159
+ @aws_s3, 's3', 'cp',
160
+ File.join(@upload_path, 'last_run.json'),
161
+ @upload_prefix,
162
+ out: File::NULL, err: File::NULL
163
+ ) && system(
164
+ @aws_s3, 's3', 'cp',
165
+ File.join(@upload_path, @run_id),
166
+ File.join(@upload_prefix, @run_id),
167
+ '--recursive',
168
+ out: File::NULL, err: File::NULL
169
+ )
170
+
171
+ raise CacheUploadError, 'Failed to upload cache files'
172
+ end
173
+
174
+ def last_run_id
175
+ file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
176
+
177
+ return unless File.file?(file_name)
178
+
179
+ run_id = JSON.parse(File.read(file_name))['run_id']
180
+
181
+ raise LocalCacheNotFoundError, 'Could not find any local cache to upload' if run_id.nil?
182
+
183
+ run_id
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ module RemoteCache
5
+ class Git
6
+ class GitOperationError < StandardError; end
7
+
8
+ attr_reader :branch_ref, :ref_list
9
+
10
+ def initialize
11
+ fetch_head_ref
12
+ fetch_branch_ref
13
+ end
14
+
15
+ def prepare_for_download
16
+ fetch_unreachable_refs
17
+ fetch_ancestry_refs
18
+ fetch_ordered_refs
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_head_ref
24
+ @head_ref = `git rev-parse HEAD`.chomp
25
+
26
+ raise GitOperationError, 'Could not find HEAD commit sha' unless $CHILD_STATUS.success?
27
+ end
28
+
29
+ def fetch_branch_ref
30
+ @merged_parents = []
31
+ @ignored_refs = []
32
+
33
+ unless merged?
34
+ @branch_ref = @head_ref
35
+
36
+ return
37
+ end
38
+
39
+ @ignored_refs << @head_ref
40
+
41
+ fetch_merged_parents
42
+ fetch_merged_branch_ref
43
+ end
44
+
45
+ def merged?
46
+ system('git', 'rev-parse', 'HEAD^2', out: File::NULL, err: File::NULL)
47
+ end
48
+
49
+ def fetch_merged_parents
50
+ first_parent = `git rev-parse HEAD^1`.chomp
51
+ @merged_parents << first_parent if $CHILD_STATUS.success?
52
+
53
+ second_parent = `git rev-parse HEAD^2`.chomp
54
+ @merged_parents << second_parent if $CHILD_STATUS.success?
55
+
56
+ raise GitOperationError, 'Could not find merged commit parents' if @merged_parents.length != 2
57
+ end
58
+
59
+ def fetch_merged_branch_ref
60
+ @origin_head_ref = `git rev-parse origin/HEAD`.chomp
61
+ @branch_ref = nil
62
+
63
+ if @merged_parents.first != @origin_head_ref
64
+ @branch_ref = @head_ref
65
+ @ignored_refs = []
66
+
67
+ return
68
+ end
69
+
70
+ @branch_ref = @merged_parents.last
71
+ @ignored_refs = @ignored_refs.to_set | `git rev-list #{@branch_ref}..origin/HEAD`.chomp.split
72
+
73
+ raise GitOperationError, 'Could not find ignored refs' unless $CHILD_STATUS.success?
74
+ end
75
+
76
+ def fetch_unreachable_refs
77
+ command = <<-COMMAND.strip.gsub(/\s+/, ' ')
78
+ git fsck
79
+ --no-progress
80
+ --unreachable
81
+ --connectivity-only #{@branch_ref}
82
+ | awk '/commit/ { print $3 }'
83
+ | head -n 25
84
+ COMMAND
85
+
86
+ @unreachable_refs = `#{command}`.chomp.split
87
+
88
+ raise GitOperationError, 'Could not find unreachable refs' unless $CHILD_STATUS.success?
89
+ end
90
+
91
+ def fetch_ancestry_refs
92
+ @ancestry_refs = `git rev-list --max-count=25 #{@branch_ref}`.chomp.split
93
+
94
+ raise GitOperationError, 'Could not find ancestry refs' unless $CHILD_STATUS.success?
95
+ end
96
+
97
+ def fetch_ordered_refs
98
+ unordered_refs = (@unreachable_refs.to_set | @ancestry_refs) - @ignored_refs
99
+
100
+ command = <<-COMMAND.strip.gsub(/\s+/, ' ')
101
+ git rev-list
102
+ --topo-order
103
+ --no-walk=sorted
104
+ #{unordered_refs.to_a.join(' ')}
105
+ COMMAND
106
+
107
+ @ref_list = `#{command}`.chomp.split
108
+
109
+ raise GitOperationError, 'Could not find refs to download cache' unless $CHILD_STATUS.success?
110
+ end
111
+ end
112
+ end
113
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Reporter
5
- attr_reader :all_examples, :pending_examples, :all_files, :dependency,
6
- :reverse_dependency, :examples_coverage, :last_run
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
7
8
 
8
9
  def initialize
9
10
  initialize_examples
@@ -21,6 +22,7 @@ module RSpecTracer
21
22
  end
22
23
 
23
24
  def on_example_passed(example_id, result)
25
+ @passed_examples << example_id
24
26
  @all_examples[example_id][:execution_result] = formatted_execution_result(result)
25
27
  end
26
28
 
@@ -44,6 +46,14 @@ module RSpecTracer
44
46
  end
45
47
  end
46
48
 
49
+ def register_possibly_flaky_example(example_id)
50
+ @possibly_flaky_examples << example_id
51
+ end
52
+
53
+ def register_flaky_example(example_id)
54
+ @flaky_examples << example_id
55
+ end
56
+
47
57
  def register_failed_example(example_id)
48
58
  @failed_examples << example_id
49
59
  end
@@ -52,6 +62,10 @@ module RSpecTracer
52
62
  @pending_examples << example_id
53
63
  end
54
64
 
65
+ def example_passed?(example_id)
66
+ @passed_examples.include?(example_id)
67
+ end
68
+
55
69
  def example_skipped?(example_id)
56
70
  @skipped_examples.include?(example_id)
57
71
  end
@@ -126,6 +140,8 @@ module RSpecTracer
126
140
  end
127
141
 
128
142
  def write_reports
143
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
144
+
129
145
  @run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
130
146
  @cache_dir = File.join(RSpecTracer.cache_path, @run_id)
131
147
 
@@ -133,6 +149,7 @@ module RSpecTracer
133
149
 
134
150
  %i[
135
151
  all_examples
152
+ flaky_examples
136
153
  failed_examples
137
154
  pending_examples
138
155
  all_files
@@ -142,13 +159,19 @@ module RSpecTracer
142
159
  last_run
143
160
  ].each { |report_type| send("write_#{report_type}_report") }
144
161
 
145
- puts "RSpec tracer reports generated to #{@cache_dir}"
162
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
163
+ elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
164
+
165
+ puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
146
166
  end
147
167
 
148
168
  private
149
169
 
150
170
  def initialize_examples
151
171
  @all_examples = {}
172
+ @passed_examples = Set.new
173
+ @possibly_flaky_examples = Set.new
174
+ @flaky_examples = Set.new
152
175
  @failed_examples = Set.new
153
176
  @skipped_examples = Set.new
154
177
  @pending_examples = Set.new
@@ -206,50 +229,56 @@ module RSpecTracer
206
229
  def write_all_examples_report
207
230
  file_name = File.join(@cache_dir, 'all_examples.json')
208
231
 
209
- File.write(file_name, JSON.pretty_generate(@all_examples))
232
+ File.write(file_name, JSON.generate(@all_examples))
233
+ end
234
+
235
+ def write_flaky_examples_report
236
+ file_name = File.join(@cache_dir, 'flaky_examples.json')
237
+
238
+ File.write(file_name, JSON.generate(@flaky_examples.to_a))
210
239
  end
211
240
 
212
241
  def write_failed_examples_report
213
242
  file_name = File.join(@cache_dir, 'failed_examples.json')
214
243
 
215
- File.write(file_name, JSON.pretty_generate(@failed_examples.to_a))
244
+ File.write(file_name, JSON.generate(@failed_examples.to_a))
216
245
  end
217
246
 
218
247
  def write_pending_examples_report
219
248
  file_name = File.join(@cache_dir, 'pending_examples.json')
220
249
 
221
- File.write(file_name, JSON.pretty_generate(@pending_examples.to_a))
250
+ File.write(file_name, JSON.generate(@pending_examples.to_a))
222
251
  end
223
252
 
224
253
  def write_all_files_report
225
254
  file_name = File.join(@cache_dir, 'all_files.json')
226
255
 
227
- File.write(file_name, JSON.pretty_generate(@all_files))
256
+ File.write(file_name, JSON.generate(@all_files))
228
257
  end
229
258
 
230
259
  def write_dependency_report
231
260
  file_name = File.join(@cache_dir, 'dependency.json')
232
261
 
233
- File.write(file_name, JSON.pretty_generate(@dependency))
262
+ File.write(file_name, JSON.generate(@dependency))
234
263
  end
235
264
 
236
265
  def write_reverse_dependency_report
237
266
  file_name = File.join(@cache_dir, 'reverse_dependency.json')
238
267
 
239
- File.write(file_name, JSON.pretty_generate(@reverse_dependency))
268
+ File.write(file_name, JSON.generate(@reverse_dependency))
240
269
  end
241
270
 
242
271
  def write_examples_coverage_report
243
272
  file_name = File.join(@cache_dir, 'examples_coverage.json')
244
273
 
245
- File.write(file_name, JSON.pretty_generate(@examples_coverage))
274
+ File.write(file_name, JSON.generate(@examples_coverage))
246
275
  end
247
276
 
248
277
  def write_last_run_report
249
278
  file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
250
279
  last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
251
280
 
252
- File.write(file_name, JSON.pretty_generate(last_run_data))
281
+ File.write(file_name, JSON.generate(last_run_data))
253
282
  end
254
283
  end
255
284
  end