fast_ci 0.1.1 → 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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastCI
4
+ class ExtractDescriptions
5
+ def call(example_group, count: false)
6
+ data = {}
7
+
8
+ data[scoped_id(example_group)] = {
9
+ description: description(example_group),
10
+ line_number: line_number(example_group),
11
+ }
12
+
13
+ if count
14
+ data[:test_count] ||= 0
15
+ data[:test_count] += RSpec.world.example_count([example_group])
16
+ end
17
+
18
+ example_group.examples.each do |ex|
19
+ data[scoped_id(example_group)][scoped_id(ex)] = {
20
+ line_number: line_number(ex),
21
+ description: description(ex),
22
+ }
23
+ end
24
+
25
+ example_group.children.each do |child|
26
+ data[scoped_id(example_group)].merge! call(child)
27
+ end
28
+
29
+ data
30
+ end
31
+
32
+ def scoped_id(example_group)
33
+ example_group.metadata[:scoped_id].split(":").last
34
+ end
35
+
36
+ def line_number(example_group)
37
+ example_group.metadata[:line_number]
38
+ end
39
+
40
+ def description(example_group)
41
+ example_group.metadata[:description]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastCI
4
+ class RspecDryrunFormatter
5
+ RSpec::Core::Formatters.register self,
6
+ :start,
7
+ :message,
8
+ :example_passed,
9
+ :example_failed,
10
+ :example_pending,
11
+ :example_group_finished,
12
+ :example_group_started,
13
+ :dump_summary,
14
+ :close
15
+
16
+ def initialize(output)
17
+ @output = output
18
+ @current_group_path = []
19
+ @current_file = nil
20
+ @current_file_count = 0
21
+ @events = []
22
+ end
23
+
24
+ def message(message_notification)
25
+ if message_notification.message.include?('An error occurred')
26
+ msg(:message, Base64.strict_encode64("\nAn error occurred#{message_notification.message}"))
27
+ send_events
28
+ end
29
+ @output.print message_notification.message
30
+ @output.print "\n"
31
+ end
32
+
33
+ def start(example_count)
34
+ msg(:start, { "example_count" => example_count.count, "timestamp" => time_now })
35
+ end
36
+
37
+ def close(*args)
38
+ msg(:close, { "timestamp" => time_now })
39
+ send_events
40
+ end
41
+
42
+ def dump_summary(summary_notification)
43
+ end
44
+
45
+ def time_now
46
+ time_frozen? ? Timecop.return { Time.now } : Time.now
47
+ end
48
+
49
+ def time_frozen?
50
+ return unless defined?(Timecop)
51
+ Timecop.frozen?
52
+ end
53
+
54
+ def example_passed(example_notification)
55
+ example_finished(example_notification)
56
+ @output.print RSpec::Core::Formatters::ConsoleCodes.wrap('.', :success)
57
+ end
58
+
59
+ def example_failed(example_notification)
60
+ example_finished(example_notification)
61
+ @output.print RSpec::Core::Formatters::ConsoleCodes.wrap('F', :failure)
62
+ end
63
+
64
+ def example_pending(example_notification)
65
+ example_finished(example_notification)
66
+ @output.print RSpec::Core::Formatters::ConsoleCodes.wrap('*', :pending)
67
+ end
68
+
69
+ def start_dump(_notification)
70
+ @output.print "\n"
71
+ end
72
+
73
+ def example_group_finished(_group_notification)
74
+ @current_group_path.pop
75
+ if @current_group_path.empty? # its a file
76
+ msg(:file_examples_count, [@current_file, @current_file_count])
77
+ @current_file_count = 0
78
+ @current_file = nil
79
+ end
80
+ end
81
+
82
+ def example_group_started(group_notification)
83
+ if @current_group_path.size == 0
84
+ @current_file = group_notification.group.metadata[:file_path].gsub("./".freeze, "".freeze)
85
+ @current_file_count = 0
86
+ end
87
+ @current_group_path << 1
88
+ end
89
+
90
+ private
91
+
92
+ def example_finished(example_notification)
93
+ @current_file_count += 1
94
+ end
95
+
96
+ def send_events
97
+ return unless @events.length > 0
98
+
99
+ json_events = {
100
+ build_id: FastCI.configuration.orig_build_id,
101
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(@events), 9)),
102
+ }
103
+
104
+ FastCI.send_events(json_events)
105
+
106
+ @events = []
107
+ end
108
+
109
+ def msg(event, data)
110
+ @events << ["rspec_dryrun_#{event}".upcase, ['0', data]]
111
+ end
112
+
113
+ def get_scope_id(metadata)
114
+ metadata[:scoped_id].split(":").last || raise("No scoped id")
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastCI
4
+ class RspecFormatter
5
+ attr_reader :current_test_key
6
+
7
+ def initialize(output)
8
+ @output = output
9
+ @event_output = {}
10
+ @is_failed = false
11
+ @current_path = []
12
+ @current_path_started_at = []
13
+ @max_heap_live_num = 0
14
+ @dup_stdout = STDOUT.clone
15
+ @events = []
16
+
17
+ $stdout = StringIO.new()
18
+
19
+ @log_thread = Thread.new do
20
+ loop do
21
+ sleep 10
22
+ check_heap_live_num
23
+ @should_send_events = true
24
+ end
25
+ end
26
+ end
27
+
28
+ def time_now
29
+ time_frozen? ? Timecop.return { Time.now } : Time.now
30
+ end
31
+
32
+ def time_frozen?
33
+ return unless defined?(Timecop)
34
+ Timecop.frozen?
35
+ end
36
+
37
+ def rspec_runner_index
38
+ ENV["TEST_ENV_NUMBER"]
39
+ end
40
+
41
+ def send_events
42
+ @should_send_events = false
43
+
44
+ if @events.length > 0
45
+ json_events = {
46
+ build_id: FastCI.configuration.orig_build_id,
47
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(@events), 9)),
48
+ }
49
+
50
+ FastCI.send_events(json_events)
51
+
52
+ @events = []
53
+ end
54
+ end
55
+
56
+ def check_heap_live_num
57
+ @max_heap_live_num = [@max_heap_live_num, GC.stat[:heap_live_slots] || GC.stat[:heap_live_num]].max
58
+ end
59
+
60
+ def passed?
61
+ !@is_failed
62
+ end
63
+
64
+ def current_test_key=(value)
65
+ @current_test_key = value
66
+ end
67
+
68
+ def start(start_notification)
69
+ # $stderr = $stdout
70
+
71
+ data = {
72
+ load_time: start_notification.load_time,
73
+ example_count: start_notification.count,
74
+ started_at: time_now.to_s
75
+ }
76
+
77
+ return if running_only_failed? ||
78
+ running_gem_or_engine? ||
79
+ ENV["EXTRA_SLOWER_RUN"]
80
+
81
+ msg(:start, data)
82
+ end
83
+
84
+ def close(null_notification)
85
+ # check_heap_live_num
86
+ msg(:gc_stat, GC.stat.merge(max_heap_live_num: @max_heap_live_num))
87
+ unless running_only_failed? || ENV["EXTRA_SLOWER_RUN"] || running_gem_or_engine?
88
+ msg(:close, {final_output: get_output})
89
+ end
90
+ send_events
91
+ $stdout = @dup_stdout
92
+ end
93
+
94
+ def example_group_started(group_notification)
95
+ metadata = group_notification.group.metadata
96
+ @current_path_started_at << time_now
97
+
98
+ if @current_path.size == 0
99
+ @example_failed_index = 0
100
+ file_path = metadata[:file_path].gsub("./".freeze, "".freeze)
101
+ file_path = [ENV["DIR_PREFIX"], file_path].join("/") if ENV["DIR_PREFIX"]
102
+ @current_path << file_path
103
+ end
104
+
105
+ @current_path << id(metadata)
106
+
107
+ msg(:group_started, [
108
+ path_with_file(group_notification.group),
109
+ {
110
+ line_number: metadata[:line_number],
111
+ description: metadata[:description],
112
+ }
113
+ ])
114
+ end
115
+
116
+ def example_started(example_notification)
117
+ @output_before = get_output
118
+ end
119
+
120
+ def example_passed(example_notification)
121
+ metadata = example_notification.example.metadata
122
+ broadcast_example_finished(serialize_example(metadata, "passed".freeze), example_notification.example)
123
+ end
124
+
125
+ def example_failed(example_notification)
126
+ @example_failed_index += 1
127
+ metadata = example_notification.example.metadata
128
+ fully_formatted = example_notification.fully_formatted(@example_failed_index, ::RSpec::Core::Formatters::ConsoleCodes)
129
+
130
+ broadcast_example_finished(
131
+ serialize_example(metadata, "failed".freeze, fully_formatted),
132
+ example_notification.example
133
+ )
134
+ end
135
+
136
+ def example_pending(example_notification)
137
+ metadata = example_notification.example.metadata
138
+ broadcast_example_finished(
139
+ serialize_example(metadata, "pending".freeze),
140
+ example_notification.example
141
+ )
142
+ end
143
+
144
+ def example_group_finished(group_notification)
145
+ run_time = time_now - @current_path_started_at.pop
146
+ if (run_time < 0) || (run_time > 2400)
147
+ run_time = 0.525
148
+ end
149
+ msg(:group_finished, [path_with_file(group_notification.group), {run_time: run_time}])
150
+ # msg(:group_finished, [@current_path.map(&:to_s), {run_time: run_time}])
151
+ @current_path.pop
152
+ @current_path.pop if @current_path.size == 1 # Remove the file_path at the beggining
153
+ end
154
+
155
+ def example_finished(notification)
156
+ example = notification.example
157
+ metadata = example.metadata
158
+
159
+ *example_group_ids, example_id = metadata[:scoped_id].split(":")
160
+
161
+ file_output = @event_output[current_test_key] ||= {}
162
+
163
+ example_group = example_group_ids.reduce(file_output) do |output, scope_id|
164
+ output[scope_id] ||= {}
165
+ output[scope_id]
166
+ end
167
+
168
+ example_group[example_id] = {
169
+ run_time: example.execution_result.run_time,
170
+ status: example.execution_result.status
171
+ }
172
+
173
+ if example.execution_result.status == :failed
174
+ @is_failed = true
175
+ example_group[example_id][:fully_formatted] =
176
+ notification.fully_formatted(0, ::RSpec::Core::Formatters::ConsoleCodes)
177
+ elsif metadata[:retry_attempts] && metadata[:retry_attempts] > 0
178
+ example_group[example_id][:retry_attempts] = metadata[:retry_attempts]
179
+ example_group[example_id][:fully_formatted] =
180
+ example.set_exception metadata[:retry_exceptions].first.to_s
181
+ end
182
+
183
+ example_group[example_id]
184
+ end
185
+
186
+ def dump_and_reset
187
+ event_output = @event_output
188
+ @event_output = {}
189
+ event_output
190
+ end
191
+
192
+ def path_with_file(group)
193
+ file_path = group.parent_groups.last.file_path.gsub("./".freeze, "".freeze)
194
+
195
+ group.metadata[:scoped_id].split(":").unshift(file_path)
196
+ end
197
+
198
+ private
199
+
200
+ def running_gem_or_engine?
201
+ !!ENV["DIR_PREFIX"]
202
+ end
203
+
204
+ def running_only_failed?
205
+ !!ENV["RERUN_FAILED_FILES"]
206
+ end
207
+
208
+ def serialize_example(metadata, status, fully_formatted = nil)
209
+ run_time = metadata[:execution_result].run_time
210
+ if (run_time < 0) || (run_time > 2400)
211
+ run_time = 0.525
212
+ end
213
+
214
+ result = {
215
+ id: id(metadata),
216
+ status: status,
217
+ line_number: metadata[:line_number].to_s,
218
+ description: metadata[:description],
219
+ run_time: run_time,
220
+ fully_formatted: fully_formatted,
221
+ scoped_id: metadata[:scoped_id],
222
+ }.compact
223
+
224
+ result[:gem_or_engine] = true if running_gem_or_engine?
225
+
226
+ if running_only_failed?
227
+ result[:reruned] = true
228
+ elsif status == "failed" && !running_gem_or_engine?
229
+ File.write('tmp/rspec_failures', "#{@current_path.first}[#{metadata[:scoped_id]}]", mode: 'a+')
230
+ end
231
+
232
+ if status == "failed"
233
+ img_path = metadata.dig(:screenshot, :image) ||
234
+ fully_formatted&.scan(/\[Screenshot Image\]: (.*)$/).flatten.first&.strip&.chomp ||
235
+ fully_formatted&.scan(/\[Screenshot\]: (.*)$/).flatten.first&.strip&.chomp
236
+
237
+ if img_path && File.exist?(img_path)
238
+ STDOUT.puts "SCREENSHOT!"
239
+ result[:screenshots_base64] ||= []
240
+ result[:screenshots_base64] << Base64.strict_encode64(File.read(img_path))
241
+ end
242
+ end
243
+ @last_example_finished_at = time_now
244
+ # TODO annalyze this: run_time: metadata[:execution_result].run_time,
245
+ result
246
+ end
247
+
248
+ def msg(event, data)
249
+ @events << ["rspec_#{event}".upcase, [rspec_runner_index, data]]
250
+ end
251
+
252
+ def id(metadata)
253
+ metadata[:scoped_id].split(":").last || raise("No scoped id")
254
+ end
255
+
256
+ def broadcast_example_finished(data, example)
257
+ msg(:example_finished, [
258
+ path_with_file(example.example_group),
259
+ data.merge(output_inside: get_output, output_before: @output_before)
260
+ ])
261
+
262
+ send_events if @should_send_events
263
+ end
264
+
265
+ def get_output
266
+ return if $stdout.pos == 0
267
+ $stdout.rewind
268
+ res = $stdout.read
269
+ $stdout.flush
270
+ $stdout.rewind
271
+ res
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+ require "stringio"
3
+
4
+ module FastCI
5
+ class RspecRunFormatter
6
+ RSpec::Core::Formatters.register self,
7
+ :start,
8
+ :example_group_started,
9
+ :example_started,
10
+ :example_passed,
11
+ :example_failed,
12
+ :example_pending,
13
+ :example_group_finished,
14
+ :close
15
+
16
+ def initialize(output)
17
+ @output = output
18
+ @event_output = {}
19
+ @is_failed = false
20
+ @current_path = []
21
+ @current_path_started_at = []
22
+ @max_heap_live_num = 0
23
+ @dup_stdout = STDOUT.clone
24
+ @events = []
25
+
26
+ $stdout = StringIO.new()
27
+
28
+ @log_thread = Thread.new do
29
+ loop do
30
+ sleep 10
31
+ check_heap_live_num
32
+ @should_send_events = true
33
+ end
34
+ end
35
+ end
36
+
37
+ def time_now
38
+ time_frozen? ? Timecop.return { Time.now } : Time.now
39
+ end
40
+
41
+ def time_frozen?
42
+ return unless defined?(Timecop)
43
+ Timecop.frozen?
44
+ end
45
+
46
+ def rspec_runner_index
47
+ ENV["TEST_ENV_NUMBER"]
48
+ end
49
+
50
+ def send_events
51
+ @should_send_events = false
52
+
53
+ if @events.length > 0
54
+ json_events = {
55
+ build_id: FastCI.configuration.orig_build_id,
56
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(@events), 9)),
57
+ }
58
+
59
+ FastCI.send_events(json_events)
60
+
61
+ @events = []
62
+ end
63
+ end
64
+
65
+ def check_heap_live_num
66
+ @max_heap_live_num = [@max_heap_live_num, GC.stat[:heap_live_slots] || GC.stat[:heap_live_num]].max
67
+ end
68
+
69
+ def passed?
70
+ !@is_failed
71
+ end
72
+
73
+ def start(start_notification)
74
+ @output.print "Starting rspec run"
75
+ # $stderr = $stdout
76
+
77
+ data = {
78
+ load_time: start_notification.load_time,
79
+ example_count: start_notification.count,
80
+ started_at: time_now.to_s
81
+ }
82
+
83
+ return if running_only_failed? ||
84
+ running_gem_or_engine? ||
85
+ ENV["EXTRA_SLOWER_RUN"]
86
+
87
+ msg(:start, data)
88
+ end
89
+
90
+ def close(null_notification)
91
+ @output.print "Finished rspec run"
92
+ # check_heap_live_num
93
+ msg(:gc_stat, GC.stat.merge(max_heap_live_num: @max_heap_live_num))
94
+ unless running_only_failed? || ENV["EXTRA_SLOWER_RUN"] || running_gem_or_engine?
95
+ msg(:close, {final_output: get_output})
96
+ end
97
+ send_events
98
+ $stdout = @dup_stdout
99
+ end
100
+
101
+ def example_group_started(group_notification)
102
+ metadata = group_notification.group.metadata
103
+ @current_path_started_at << time_now
104
+
105
+ if @current_path.size == 0
106
+ @example_failed_index = 0
107
+ file_path = metadata[:file_path].gsub("./".freeze, "".freeze)
108
+ file_path = [ENV["DIR_PREFIX"], file_path].join("/") if ENV["DIR_PREFIX"]
109
+ @current_path << file_path
110
+ end
111
+
112
+ @current_path << id(metadata)
113
+
114
+ msg(:group_started, [
115
+ path_with_file(group_notification.group),
116
+ {
117
+ line_number: metadata[:line_number],
118
+ description: metadata[:description],
119
+ }
120
+ ])
121
+ end
122
+
123
+ def example_started(example_notification)
124
+ @output_before = get_output
125
+ end
126
+
127
+ def example_passed(example_notification)
128
+ metadata = example_notification.example.metadata
129
+ broadcast_example_finished(serialize_example(metadata, "passed".freeze), example_notification.example)
130
+ @output.print RSpec::Core::Formatters::ConsoleCodes.wrap('.', :success)
131
+ end
132
+
133
+ def example_failed(example_notification)
134
+ @example_failed_index += 1
135
+ metadata = example_notification.example.metadata
136
+ fully_formatted = example_notification.fully_formatted(@example_failed_index, ::RSpec::Core::Formatters::ConsoleCodes)
137
+
138
+ broadcast_example_finished(
139
+ serialize_example(metadata, "failed".freeze, fully_formatted),
140
+ example_notification.example
141
+ )
142
+ @output.print RSpec::Core::Formatters::ConsoleCodes.wrap('F', :failure)
143
+ end
144
+
145
+ def example_pending(example_notification)
146
+ metadata = example_notification.example.metadata
147
+ broadcast_example_finished(
148
+ serialize_example(metadata, "pending".freeze),
149
+ example_notification.example
150
+ )
151
+ @output.print RSpec::Core::Formatters::ConsoleCodes.wrap('*', :pending)
152
+ end
153
+
154
+ def example_group_finished(group_notification)
155
+ run_time = time_now - @current_path_started_at.pop
156
+ if (run_time < 0) || (run_time > 2400)
157
+ run_time = 0.525
158
+ end
159
+ msg(:group_finished, [path_with_file(group_notification.group), {run_time: run_time}])
160
+ # msg(:group_finished, [@current_path.map(&:to_s), {run_time: run_time}])
161
+ @current_path.pop
162
+ @current_path.pop if @current_path.size == 1 # Remove the file_path at the beggining
163
+ end
164
+
165
+ def path_with_file(group)
166
+ file_path = group.parent_groups.last.file_path.gsub("./".freeze, "".freeze)
167
+
168
+ group.metadata[:scoped_id].split(":").unshift(file_path)
169
+ end
170
+
171
+ private
172
+
173
+ def running_gem_or_engine?
174
+ !!ENV["DIR_PREFIX"]
175
+ end
176
+
177
+ def running_only_failed?
178
+ !!ENV["RERUN_FAILED_FILES"]
179
+ end
180
+
181
+ def serialize_example(metadata, status, fully_formatted = nil)
182
+ run_time = metadata[:execution_result].run_time
183
+ if (run_time < 0) || (run_time > 2400)
184
+ run_time = 0.525
185
+ end
186
+
187
+ result = {
188
+ id: id(metadata),
189
+ status: status,
190
+ line_number: metadata[:line_number].to_s,
191
+ description: metadata[:description],
192
+ run_time: run_time,
193
+ fully_formatted: fully_formatted,
194
+ scoped_id: metadata[:scoped_id],
195
+ }.compact
196
+
197
+ result[:gem_or_engine] = true if running_gem_or_engine?
198
+
199
+ if running_only_failed?
200
+ result[:reruned] = true
201
+ elsif status == "failed" && !running_gem_or_engine?
202
+ File.write('tmp/rspec_failures', "#{@current_path.first}[#{metadata[:scoped_id]}]", mode: 'a+')
203
+ end
204
+
205
+ if status == "failed"
206
+ img_path = metadata.dig(:screenshot, :image) ||
207
+ fully_formatted&.scan(/\[Screenshot Image\]: (.*)$/).flatten.first&.strip&.chomp ||
208
+ fully_formatted&.scan(/\[Screenshot\]: (.*)$/).flatten.first&.strip&.chomp
209
+
210
+ if img_path && File.exist?(img_path)
211
+ STDOUT.puts "SCREENSHOT!"
212
+ result[:screenshots_base64] ||= []
213
+ result[:screenshots_base64] << Base64.strict_encode64(File.read(img_path))
214
+ end
215
+ end
216
+ @last_example_finished_at = time_now
217
+ # TODO annalyze this: run_time: metadata[:execution_result].run_time,
218
+ result
219
+ end
220
+
221
+ def msg(event, data)
222
+ @events << ["rspec_#{event}".upcase, [rspec_runner_index, data]]
223
+ end
224
+
225
+ def id(metadata)
226
+ metadata[:scoped_id].split(":").last || raise("No scoped id")
227
+ end
228
+
229
+ def broadcast_example_finished(data, example)
230
+ msg(:example_finished, [
231
+ path_with_file(example.example_group),
232
+ data.merge(output_inside: get_output, output_before: @output_before)
233
+ ])
234
+
235
+ send_events if @should_send_events
236
+ end
237
+
238
+ def get_output
239
+ return if $stdout.pos == 0
240
+ $stdout.rewind
241
+ res = $stdout.read
242
+ $stdout.flush
243
+ $stdout.rewind
244
+ res
245
+ end
246
+ end
247
+ end