rspec-buildkite-analytics 0.3.5 → 0.6.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -5
- data/README.md +1 -4
- data/lib/rspec/buildkite/analytics/ci.rb +4 -2
- data/lib/rspec/buildkite/analytics/reporter.rb +75 -1
- data/lib/rspec/buildkite/analytics/session.rb +139 -37
- data/lib/rspec/buildkite/analytics/socket_connection.rb +14 -2
- data/lib/rspec/buildkite/analytics/tracer.rb +6 -4
- data/lib/rspec/buildkite/analytics/uploader.rb +14 -23
- data/lib/rspec/buildkite/analytics/version.rb +1 -1
- data/lib/rspec/buildkite/analytics.rb +6 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ca7801b3e7bc48f52a372df29c553c9e2dad5bffdab1bf8dff0b20f0173b987
|
|
4
|
+
data.tar.gz: 22d6197b37f465afa1a2a309ba4f3780bafef928d626768a5d4d3fa6d214a809
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0380c462ae87c2dd2dbb830bbcdc93bef621b74f2f516c465b37e633679e9e0c887fe8557356aaf85abd8e7fe179110b86aabceb339057cd89e37c00afcac107'
|
|
7
|
+
data.tar.gz: 9b6810ff8529454f543b70f521b34906223e2ee02ad1f0d4cd261d4bfe6e2c4ed21eb76c8eddfb08818c9c8a07d07cb62c3c206c958c48743a47c1dfde83f9b3
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rspec-buildkite-analytics (0.
|
|
4
|
+
rspec-buildkite-analytics (0.6.0)
|
|
5
5
|
activesupport (>= 5.2, <= 7.0)
|
|
6
6
|
rspec-core (~> 3.10)
|
|
7
7
|
rspec-expectations (~> 3.10)
|
|
@@ -18,10 +18,10 @@ GEM
|
|
|
18
18
|
zeitwerk (~> 2.3)
|
|
19
19
|
concurrent-ruby (1.1.9)
|
|
20
20
|
diff-lcs (1.4.4)
|
|
21
|
-
i18n (1.8.
|
|
21
|
+
i18n (1.8.11)
|
|
22
22
|
concurrent-ruby (~> 1.0)
|
|
23
23
|
minitest (5.14.4)
|
|
24
|
-
rake (13.0.
|
|
24
|
+
rake (13.0.6)
|
|
25
25
|
rspec (3.10.0)
|
|
26
26
|
rspec-core (~> 3.10.0)
|
|
27
27
|
rspec-expectations (~> 3.10.0)
|
|
@@ -31,10 +31,10 @@ GEM
|
|
|
31
31
|
rspec-expectations (3.10.1)
|
|
32
32
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
33
33
|
rspec-support (~> 3.10.0)
|
|
34
|
-
rspec-mocks (3.10.
|
|
34
|
+
rspec-mocks (3.10.2)
|
|
35
35
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
36
36
|
rspec-support (~> 3.10.0)
|
|
37
|
-
rspec-support (3.10.
|
|
37
|
+
rspec-support (3.10.3)
|
|
38
38
|
tzinfo (2.0.4)
|
|
39
39
|
concurrent-ruby (~> 1.0)
|
|
40
40
|
websocket (1.2.9)
|
data/README.md
CHANGED
|
@@ -15,10 +15,7 @@ end
|
|
|
15
15
|
|
|
16
16
|
Configure your API key:
|
|
17
17
|
```ruby
|
|
18
|
-
RSpec::Buildkite::Analytics.configure
|
|
19
|
-
config.suite_key = "........"
|
|
20
|
-
# other config
|
|
21
|
-
end
|
|
18
|
+
RSpec::Buildkite::Analytics.configure(token: "........")
|
|
22
19
|
```
|
|
23
20
|
|
|
24
21
|
Run bundler to install the gem and update your `Gemfile.lock`:
|
|
@@ -13,12 +13,14 @@ module RSpec::Buildkite::Analytics::CI
|
|
|
13
13
|
"commit_sha" => ENV["BUILDKITE_COMMIT"],
|
|
14
14
|
"number" => ENV["BUILDKITE_BUILD_NUMBER"],
|
|
15
15
|
"job_id" => ENV["BUILDKITE_JOB_ID"],
|
|
16
|
-
"message" => ENV["BUILDKITE_MESSAGE"]
|
|
16
|
+
"message" => ENV["BUILDKITE_MESSAGE"],
|
|
17
|
+
"debug" => ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"]
|
|
17
18
|
}
|
|
18
19
|
else
|
|
19
20
|
{
|
|
20
21
|
"CI" => nil,
|
|
21
|
-
"key" => SecureRandom.uuid
|
|
22
|
+
"key" => SecureRandom.uuid,
|
|
23
|
+
"debug" => ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"]
|
|
22
24
|
}
|
|
23
25
|
end
|
|
24
26
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
1
3
|
module RSpec::Buildkite::Analytics
|
|
2
4
|
class Reporter
|
|
3
|
-
RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending
|
|
5
|
+
RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending, :dump_summary
|
|
6
|
+
|
|
7
|
+
attr_reader :output
|
|
4
8
|
|
|
5
9
|
def initialize(output)
|
|
6
10
|
@output = output
|
|
@@ -14,12 +18,82 @@ module RSpec::Buildkite::Analytics
|
|
|
14
18
|
|
|
15
19
|
if trace
|
|
16
20
|
trace.example = example
|
|
21
|
+
trace.failure_reason, trace.failure_expanded = failure_info(notification) if example.execution_result.status == :failed
|
|
17
22
|
RSpec::Buildkite::Analytics.session&.write_result(trace)
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
def dump_summary(notification)
|
|
27
|
+
if RSpec::Buildkite::Analytics.session.present?
|
|
28
|
+
examples_count = {
|
|
29
|
+
examples: notification.examples.count,
|
|
30
|
+
failed: notification.failed_examples.count,
|
|
31
|
+
pending: notification.pending_examples.count,
|
|
32
|
+
errors_outside_examples: notification.errors_outside_of_examples_count
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
RSpec::Buildkite::Analytics.session.close(examples_count)
|
|
36
|
+
|
|
37
|
+
# Write the debug file, if debug mode is enabled
|
|
38
|
+
if RSpec::Buildkite::Analytics.debug_enabled
|
|
39
|
+
filename = "#{RSpec::Buildkite::Analytics.debug_filepath}/bk-analytics-#{Time.now.strftime("%F-%R:%S")}-#{ENV["BUILDKITE_JOB_ID"]}.log.gz"
|
|
40
|
+
|
|
41
|
+
File.open(filename, "wb") do |f|
|
|
42
|
+
gz = Zlib::GzipWriter.new(f)
|
|
43
|
+
gz.puts(RSpec::Buildkite::Analytics.session.logger.to_array)
|
|
44
|
+
gz.close
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
21
50
|
alias_method :example_passed, :handle_example
|
|
22
51
|
alias_method :example_failed, :handle_example
|
|
23
52
|
alias_method :example_pending, :handle_example
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def failure_info(notification)
|
|
57
|
+
failure_expanded = []
|
|
58
|
+
|
|
59
|
+
if notification.exception.class == RSpec::Expectations::MultipleExpectationsNotMetError
|
|
60
|
+
failure_reason = notification.exception.summary
|
|
61
|
+
notification.exception.all_exceptions.each do |exception|
|
|
62
|
+
# an example with multiple failures doesn't give us a
|
|
63
|
+
# separate message lines and backtrace object to send, so
|
|
64
|
+
# I've reached into RSpec internals and duplicated the
|
|
65
|
+
# construction of these
|
|
66
|
+
message_lines = RSpec::Core::Formatters::ExceptionPresenter.new(exception, notification.example).colorized_message_lines
|
|
67
|
+
|
|
68
|
+
failure_expanded << {
|
|
69
|
+
expanded: format_message_lines(message_lines),
|
|
70
|
+
backtrace: RSpec.configuration.backtrace_formatter.format_backtrace(exception.backtrace)
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
failure_reason = strip_diff_colors(notification.colorized_message_lines[0])
|
|
75
|
+
|
|
76
|
+
# the second line is always whitespace padding
|
|
77
|
+
message_lines = notification.colorized_message_lines[2..]
|
|
78
|
+
|
|
79
|
+
failure_expanded << {
|
|
80
|
+
expanded: format_message_lines(message_lines),
|
|
81
|
+
backtrace: notification.formatted_backtrace
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return failure_reason, failure_expanded
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_message_lines(message_lines)
|
|
89
|
+
message_lines.map! { |l| strip_diff_colors(l) }
|
|
90
|
+
# the last line is sometimes blank, depending on the error reported
|
|
91
|
+
message_lines.pop if message_lines.last.blank?
|
|
92
|
+
message_lines
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def strip_diff_colors(string)
|
|
96
|
+
string.gsub(/\e\[([;\d]+)?m/, '')
|
|
97
|
+
end
|
|
24
98
|
end
|
|
25
99
|
end
|
|
@@ -12,26 +12,52 @@ module RSpec::Buildkite::Analytics
|
|
|
12
12
|
class RejectedSubscription < StandardError; end
|
|
13
13
|
class InitialConnectionFailure < StandardError; end
|
|
14
14
|
|
|
15
|
+
class Logger
|
|
16
|
+
def initialize
|
|
17
|
+
@log = Queue.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def write(str)
|
|
21
|
+
@log << "#{Time.now.strftime("%F-%R:%S.%9N")} #{Thread.current} #{str}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_array
|
|
25
|
+
# This empty check is important cos calling pop on a Queue is blocking until
|
|
26
|
+
# it's not empty
|
|
27
|
+
if @log.empty?
|
|
28
|
+
[]
|
|
29
|
+
else
|
|
30
|
+
Array.new(@log.size) { @log.pop }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :logger
|
|
36
|
+
|
|
15
37
|
def initialize(url, authorization_header, channel)
|
|
16
|
-
@
|
|
38
|
+
@establish_subscription_queue = Queue.new
|
|
17
39
|
@channel = channel
|
|
18
40
|
|
|
19
41
|
@unconfirmed_idents = {}
|
|
20
42
|
@idents_mutex = Mutex.new
|
|
43
|
+
@send_queue = Queue.new
|
|
21
44
|
@empty = ConditionVariable.new
|
|
22
45
|
@closing = false
|
|
46
|
+
@eot_queued = false
|
|
47
|
+
@eot_queued_mutex = Mutex.new
|
|
23
48
|
@reconnection_mutex = Mutex.new
|
|
24
49
|
|
|
25
50
|
@url = url
|
|
26
51
|
@authorization_header = authorization_header
|
|
27
52
|
|
|
53
|
+
@logger = Logger.new
|
|
54
|
+
|
|
28
55
|
connect
|
|
29
56
|
rescue TimeoutError, InitialConnectionFailure => e
|
|
30
57
|
$stderr.puts "rspec-buildkite-analytics could not establish an initial connection with Buildkite due to #{e.message}. You may be missing some data for this test suite, please contact support."
|
|
31
58
|
end
|
|
32
59
|
|
|
33
60
|
def disconnected(connection)
|
|
34
|
-
reconnection_count = 0
|
|
35
61
|
@reconnection_mutex.synchronize do
|
|
36
62
|
# When the first thread detects a disconnection, it calls the disconnect method
|
|
37
63
|
# with the current connection. This thread grabs the reconnection mutex and does the
|
|
@@ -43,16 +69,21 @@ module RSpec::Buildkite::Analytics
|
|
|
43
69
|
# time the mutex is released, the value of @connection has been refreshed, and so
|
|
44
70
|
# the second thread returns early and does not reattempt the reconnection.
|
|
45
71
|
return unless connection == @connection
|
|
72
|
+
@logger.write("starting reconnection")
|
|
73
|
+
|
|
74
|
+
reconnection_count = 0
|
|
46
75
|
|
|
47
76
|
begin
|
|
48
77
|
reconnection_count += 1
|
|
49
78
|
connect
|
|
50
79
|
rescue SocketConnection::HandshakeError, RejectedSubscription, TimeoutError, SocketConnection::SocketError => e
|
|
80
|
+
@logger.write("failed reconnection attempt #{reconnection_count} due to #{e}")
|
|
51
81
|
if reconnection_count > MAX_RECONNECTION_ATTEMPTS
|
|
52
82
|
$stderr.puts "rspec-buildkite-analytics experienced a disconnection and could not reconnect to Buildkite due to #{e.message}. Please contact support."
|
|
53
83
|
raise e
|
|
54
84
|
else
|
|
55
85
|
sleep(WAIT_BETWEEN_RECONNECTIONS)
|
|
86
|
+
@logger.write("retrying reconnection")
|
|
56
87
|
retry
|
|
57
88
|
end
|
|
58
89
|
end
|
|
@@ -60,8 +91,10 @@ module RSpec::Buildkite::Analytics
|
|
|
60
91
|
retransmit
|
|
61
92
|
end
|
|
62
93
|
|
|
63
|
-
def close()
|
|
94
|
+
def close(examples_count)
|
|
64
95
|
@closing = true
|
|
96
|
+
@examples_count = examples_count
|
|
97
|
+
@logger.write("closing socket connection")
|
|
65
98
|
|
|
66
99
|
# Because the server only sends us confirmations after every 10mb of
|
|
67
100
|
# data it uploads to S3, we'll never get confirmation of the
|
|
@@ -70,7 +103,10 @@ module RSpec::Buildkite::Analytics
|
|
|
70
103
|
send_eot
|
|
71
104
|
|
|
72
105
|
@idents_mutex.synchronize do
|
|
73
|
-
|
|
106
|
+
@logger.write("waiting for last confirm")
|
|
107
|
+
# Here, we sleep for 75 seconds while waiting for the send
|
|
108
|
+
# queue to be drained and for the server to confirm the last
|
|
109
|
+
# idents.
|
|
74
110
|
# We are woken up when the unconfirmed_idents is empty, and given back the mutex to
|
|
75
111
|
# continue operation.
|
|
76
112
|
@empty.wait(@idents_mutex, CONFIRMATION_TIMEOUT) unless @unconfirmed_idents.empty?
|
|
@@ -78,6 +114,9 @@ module RSpec::Buildkite::Analytics
|
|
|
78
114
|
|
|
79
115
|
# Then we always disconnect cos we can't wait forever? 🤷♀️
|
|
80
116
|
@connection.close
|
|
117
|
+
# We kill the write thread cos it's got a while loop in it, so it won't finish otherwise
|
|
118
|
+
@write_thread&.kill
|
|
119
|
+
@logger.write("socket connection closed")
|
|
81
120
|
end
|
|
82
121
|
|
|
83
122
|
def handle(_connection, data)
|
|
@@ -86,11 +125,14 @@ module RSpec::Buildkite::Analytics
|
|
|
86
125
|
when "ping"
|
|
87
126
|
# In absence of other message, the server sends us a ping every 3 seconds
|
|
88
127
|
# We are currently not doing anything with these
|
|
128
|
+
@logger.write("received ping")
|
|
89
129
|
when "welcome", "confirm_subscription"
|
|
90
130
|
# Push these two messages onto the queue, so that we block on waiting for the
|
|
91
131
|
# initializing phase to complete
|
|
92
|
-
@
|
|
132
|
+
@establish_subscription_queue.push(data)
|
|
133
|
+
@logger.write("received #{data['type']}")
|
|
93
134
|
when "reject_subscription"
|
|
135
|
+
@logger.write("received rejected_subscription")
|
|
94
136
|
raise RejectedSubscription
|
|
95
137
|
else
|
|
96
138
|
process_message(data)
|
|
@@ -98,11 +140,9 @@ module RSpec::Buildkite::Analytics
|
|
|
98
140
|
end
|
|
99
141
|
|
|
100
142
|
def write_result(result)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
add_unconfirmed_idents(result.id, result_as_json)
|
|
143
|
+
queue_and_track_result(result.id, result.as_hash)
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
@logger.write("added #{result.id} to send queue")
|
|
106
146
|
end
|
|
107
147
|
|
|
108
148
|
def unconfirmed_idents_count
|
|
@@ -113,18 +153,9 @@ module RSpec::Buildkite::Analytics
|
|
|
113
153
|
|
|
114
154
|
private
|
|
115
155
|
|
|
116
|
-
def transmit_results(results_as_json)
|
|
117
|
-
@connection.transmit({
|
|
118
|
-
"identifier" => @channel,
|
|
119
|
-
"command" => "message",
|
|
120
|
-
"data" => {
|
|
121
|
-
"action" => "record_results",
|
|
122
|
-
"results" => results_as_json
|
|
123
|
-
}.to_json
|
|
124
|
-
})
|
|
125
|
-
end
|
|
126
|
-
|
|
127
156
|
def connect
|
|
157
|
+
@logger.write("starting socket connection process")
|
|
158
|
+
|
|
128
159
|
@connection = SocketConnection.new(self, @url, {
|
|
129
160
|
"Authorization" => @authorization_header,
|
|
130
161
|
})
|
|
@@ -137,11 +168,54 @@ module RSpec::Buildkite::Analytics
|
|
|
137
168
|
})
|
|
138
169
|
|
|
139
170
|
wait_for_confirm
|
|
171
|
+
|
|
172
|
+
@logger.write("connected")
|
|
173
|
+
|
|
174
|
+
# As this connect method can be called multiple times in the
|
|
175
|
+
# reconnection process, kill prev write threads (if any) before
|
|
176
|
+
# setting up the new one
|
|
177
|
+
@write_thread&.kill
|
|
178
|
+
|
|
179
|
+
@write_thread = Thread.new do
|
|
180
|
+
@logger.write("hello from write thread")
|
|
181
|
+
# Pretty sure this eternal loop is fine cos the call to queue.pop is blocking
|
|
182
|
+
loop do
|
|
183
|
+
data = @send_queue.pop
|
|
184
|
+
message_type = data["action"]
|
|
185
|
+
|
|
186
|
+
if message_type == "end_of_transmission"
|
|
187
|
+
# Because of the unpredictable sequencing between the test suite finishing
|
|
188
|
+
# (EOT gets queued) and disconnections happening (retransmit results gets
|
|
189
|
+
# queued), we don't want to send an EOT before any retransmits are sent.
|
|
190
|
+
if @send_queue.length > 0
|
|
191
|
+
@send_queue << data
|
|
192
|
+
@logger.write("putting eot at back of queue")
|
|
193
|
+
next
|
|
194
|
+
end
|
|
195
|
+
@eot_queued_mutex.synchronize do
|
|
196
|
+
@eot_queued = false
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
@connection.transmit({
|
|
201
|
+
"identifier" => @channel,
|
|
202
|
+
"command" => "message",
|
|
203
|
+
"data" => data.to_json
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if RSpec::Buildkite::Analytics.debug_enabled
|
|
207
|
+
ids = if message_type == "record_results"
|
|
208
|
+
data["results"].map { |result| result["id"] }
|
|
209
|
+
end
|
|
210
|
+
@logger.write("transmitted #{message_type} #{ids}")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
140
214
|
end
|
|
141
215
|
|
|
142
216
|
def pop_with_timeout(message_type)
|
|
143
217
|
Timeout.timeout(30, RSpec::Buildkite::Analytics::TimeoutError, "Timeout: Waited 30 seconds for #{message_type}") do
|
|
144
|
-
@
|
|
218
|
+
@establish_subscription_queue.pop
|
|
145
219
|
end
|
|
146
220
|
end
|
|
147
221
|
|
|
@@ -161,9 +235,16 @@ module RSpec::Buildkite::Analytics
|
|
|
161
235
|
end
|
|
162
236
|
end
|
|
163
237
|
|
|
164
|
-
def
|
|
238
|
+
def queue_and_track_result(ident, result_as_hash)
|
|
165
239
|
@idents_mutex.synchronize do
|
|
166
|
-
@unconfirmed_idents[ident] =
|
|
240
|
+
@unconfirmed_idents[ident] = result_as_hash
|
|
241
|
+
|
|
242
|
+
data = {
|
|
243
|
+
"action" => "record_results",
|
|
244
|
+
"results" => [result_as_hash]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@send_queue << data
|
|
167
248
|
end
|
|
168
249
|
end
|
|
169
250
|
|
|
@@ -174,26 +255,37 @@ module RSpec::Buildkite::Analytics
|
|
|
174
255
|
# Remove received idents from unconfirmed_idents
|
|
175
256
|
idents.each { |key| @unconfirmed_idents.delete(key) }
|
|
176
257
|
|
|
258
|
+
@logger.write("received confirm for indentifiers: #{idents.join(", ")}")
|
|
259
|
+
|
|
177
260
|
# This @empty ConditionVariable broadcasts every time that @unconfirmed_idents is
|
|
178
261
|
# empty, which will happen about every 10mb of data as that's when the server
|
|
179
262
|
# sends back confirmations.
|
|
180
263
|
#
|
|
181
264
|
# However, there aren't any threads waiting on this signal until after we
|
|
182
265
|
# send the EOT message, so the prior broadcasts shouldn't do anything.
|
|
183
|
-
|
|
266
|
+
if @unconfirmed_idents.empty?
|
|
267
|
+
@empty.broadcast
|
|
268
|
+
@logger.write("broadcast empty")
|
|
269
|
+
else
|
|
270
|
+
@logger.write("still waiting on confirm for #{@unconfirmed_idents.keys}")
|
|
271
|
+
end
|
|
184
272
|
end
|
|
185
273
|
end
|
|
186
274
|
|
|
187
275
|
def send_eot
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
276
|
+
@eot_queued_mutex.synchronize do
|
|
277
|
+
return if @eot_queued
|
|
278
|
+
# Expect server to respond with data of indentifiers last upload part
|
|
279
|
+
data = {
|
|
280
|
+
"action" => "end_of_transmission",
|
|
281
|
+
"examples_count" => @examples_count.to_json
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@send_queue << data
|
|
285
|
+
@eot_queued = true
|
|
286
|
+
|
|
287
|
+
@logger.write("added EOT to send queue")
|
|
288
|
+
end
|
|
197
289
|
end
|
|
198
290
|
|
|
199
291
|
def process_message(data)
|
|
@@ -205,16 +297,26 @@ module RSpec::Buildkite::Analytics
|
|
|
205
297
|
remove_unconfirmed_idents(data["message"]["confirm"])
|
|
206
298
|
else
|
|
207
299
|
# unhandled message
|
|
300
|
+
@logger.write("received unhandled message #{data["message"]}")
|
|
208
301
|
end
|
|
209
302
|
end
|
|
210
303
|
|
|
211
304
|
def retransmit
|
|
212
|
-
|
|
213
|
-
@unconfirmed_idents.values
|
|
305
|
+
@idents_mutex.synchronize do
|
|
306
|
+
results = @unconfirmed_idents.values
|
|
307
|
+
|
|
308
|
+
# queue the contents of the buffer, unless it's empty
|
|
309
|
+
unless results.empty?
|
|
310
|
+
data = {
|
|
311
|
+
"action" => "record_results",
|
|
312
|
+
"results" => results
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@send_queue << data
|
|
316
|
+
@logger.write("queueing up retransmitted results #{@unconfirmed_idents.keys}")
|
|
317
|
+
end
|
|
214
318
|
end
|
|
215
319
|
|
|
216
|
-
# send the contents of the buffer, unless it's empty
|
|
217
|
-
transmit_results(data) unless data.empty?
|
|
218
320
|
# if we were disconnected in the closing phase, then resend the EOT
|
|
219
321
|
# message so the server can persist the last upload part
|
|
220
322
|
send_eot if @closing
|
|
@@ -66,6 +66,7 @@ module RSpec::Buildkite::Analytics
|
|
|
66
66
|
# Setting up a new thread that listens on the socket, and processes incoming
|
|
67
67
|
# comms from the server
|
|
68
68
|
@thread = Thread.new do
|
|
69
|
+
@session.logger.write("listening in on socket")
|
|
69
70
|
frame = WebSocket::Frame::Incoming::Client.new
|
|
70
71
|
|
|
71
72
|
while @socket
|
|
@@ -75,20 +76,25 @@ module RSpec::Buildkite::Analytics
|
|
|
75
76
|
@session.handle(self, data.data)
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
|
-
rescue EOFError
|
|
79
|
+
rescue EOFError, Errno::ECONNRESET => e
|
|
80
|
+
@session.logger.write("#{e}")
|
|
79
81
|
if @socket
|
|
82
|
+
@session.logger.write("attempting disconnected flow")
|
|
80
83
|
@session.disconnected(self)
|
|
81
84
|
disconnect
|
|
82
85
|
end
|
|
83
86
|
rescue IOError
|
|
84
87
|
# This is fine to ignore
|
|
88
|
+
@session.logger.write("IOError")
|
|
85
89
|
rescue IndexError
|
|
86
90
|
# I don't like that we're doing this but I think it's the best of the options
|
|
87
91
|
#
|
|
88
92
|
# This relates to this issue https://github.com/ruby/openssl/issues/452
|
|
89
93
|
# A fix for it has been released but the repercussions of overriding
|
|
90
94
|
# the OpenSSL version in the stdlib seem worse than catching this error here.
|
|
95
|
+
@session.logger.write("IndexError")
|
|
91
96
|
if @socket
|
|
97
|
+
@session.logger.write("attempting disconnected flow")
|
|
92
98
|
@session.disconnected(self)
|
|
93
99
|
disconnect
|
|
94
100
|
end
|
|
@@ -102,8 +108,10 @@ module RSpec::Buildkite::Analytics
|
|
|
102
108
|
raw_data = data.to_json
|
|
103
109
|
frame = WebSocket::Frame::Outgoing::Client.new(data: raw_data, type: :text, version: @version)
|
|
104
110
|
@socket.write(frame.to_s)
|
|
105
|
-
rescue Errno::EPIPE, OpenSSL::SSL::SSLError => e
|
|
111
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError => e
|
|
106
112
|
return unless @socket
|
|
113
|
+
return if type == :close
|
|
114
|
+
@session.logger.write("got #{e}, attempting disconnected flow")
|
|
107
115
|
@session.disconnected(self)
|
|
108
116
|
disconnect
|
|
109
117
|
rescue IndexError
|
|
@@ -112,13 +120,16 @@ module RSpec::Buildkite::Analytics
|
|
|
112
120
|
# This relates to this issue https://github.com/ruby/openssl/issues/452
|
|
113
121
|
# A fix for it has been released but the repercussions of overriding
|
|
114
122
|
# the OpenSSL version in the stdlib seem worse than catching this error here.
|
|
123
|
+
@session.logger.write("IndexError")
|
|
115
124
|
if @socket
|
|
125
|
+
@session.logger.write("attempting disconnected flow")
|
|
116
126
|
@session.disconnected(self)
|
|
117
127
|
disconnect
|
|
118
128
|
end
|
|
119
129
|
end
|
|
120
130
|
|
|
121
131
|
def close
|
|
132
|
+
@session.logger.write("socket close")
|
|
122
133
|
transmit(nil, type: :close)
|
|
123
134
|
disconnect
|
|
124
135
|
end
|
|
@@ -126,6 +137,7 @@ module RSpec::Buildkite::Analytics
|
|
|
126
137
|
private
|
|
127
138
|
|
|
128
139
|
def disconnect
|
|
140
|
+
@session.logger.write("socket disconnect")
|
|
129
141
|
socket = @socket
|
|
130
142
|
@socket = nil
|
|
131
143
|
socket&.close
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
4
|
+
|
|
3
5
|
module RSpec::Buildkite::Analytics
|
|
4
6
|
class Tracer
|
|
5
7
|
class Span
|
|
@@ -13,15 +15,15 @@ module RSpec::Buildkite::Analytics
|
|
|
13
15
|
@children = []
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
def
|
|
18
|
+
def as_hash
|
|
17
19
|
{
|
|
18
20
|
section: section,
|
|
19
21
|
start_at: start_at,
|
|
20
22
|
end_at: end_at,
|
|
21
23
|
duration: end_at - start_at,
|
|
22
24
|
detail: detail,
|
|
23
|
-
children: children.map(&:
|
|
24
|
-
}
|
|
25
|
+
children: children.map(&:as_hash),
|
|
26
|
+
}.with_indifferent_access
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -57,7 +59,7 @@ module RSpec::Buildkite::Analytics
|
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
def history
|
|
60
|
-
@top.
|
|
62
|
+
@top.as_hash
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -22,22 +22,15 @@ require "securerandom"
|
|
|
22
22
|
module RSpec::Buildkite::Analytics
|
|
23
23
|
class Uploader
|
|
24
24
|
class Trace
|
|
25
|
-
attr_accessor :example
|
|
25
|
+
attr_accessor :example, :failure_reason, :failure_expanded
|
|
26
26
|
attr_reader :id, :history
|
|
27
27
|
|
|
28
28
|
def initialize(example, history)
|
|
29
29
|
@id = SecureRandom.uuid
|
|
30
30
|
@example = example
|
|
31
31
|
@history = history
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def failure_message
|
|
35
|
-
case example.exception
|
|
36
|
-
when RSpec::Expectations::ExpectationNotMetError
|
|
37
|
-
example.exception.message
|
|
38
|
-
when Exception
|
|
39
|
-
"#{example.exception.class}: #{example.exception.message}"
|
|
40
|
-
end
|
|
32
|
+
@failure_reason = nil
|
|
33
|
+
@failure_expanded = []
|
|
41
34
|
end
|
|
42
35
|
|
|
43
36
|
def result_state
|
|
@@ -48,7 +41,7 @@ module RSpec::Buildkite::Analytics
|
|
|
48
41
|
end
|
|
49
42
|
end
|
|
50
43
|
|
|
51
|
-
def
|
|
44
|
+
def as_hash
|
|
52
45
|
{
|
|
53
46
|
id: @id,
|
|
54
47
|
scope: example.example_group.metadata[:full_description],
|
|
@@ -57,15 +50,16 @@ module RSpec::Buildkite::Analytics
|
|
|
57
50
|
location: example.location,
|
|
58
51
|
file_name: generate_file_name(example),
|
|
59
52
|
result: result_state,
|
|
60
|
-
|
|
53
|
+
failure_reason: failure_reason,
|
|
54
|
+
failure_expanded: failure_expanded,
|
|
61
55
|
history: history,
|
|
62
|
-
}
|
|
56
|
+
}.with_indifferent_access.compact
|
|
63
57
|
end
|
|
64
58
|
|
|
65
59
|
private
|
|
66
60
|
|
|
67
61
|
def generate_file_name(example)
|
|
68
|
-
file_path_regex = /^(.*?\.rb)/
|
|
62
|
+
file_path_regex = /^(.*?\.(rb|feature))/
|
|
69
63
|
identifier_file_name = example.id[file_path_regex]
|
|
70
64
|
location_file_name = example.location[file_path_regex]
|
|
71
65
|
|
|
@@ -121,7 +115,8 @@ module RSpec::Buildkite::Analytics
|
|
|
121
115
|
"Content-Type" => "application/json",
|
|
122
116
|
})
|
|
123
117
|
contact.body = {
|
|
124
|
-
run_env: CI.env
|
|
118
|
+
run_env: CI.env,
|
|
119
|
+
format: "websocket"
|
|
125
120
|
}.to_json
|
|
126
121
|
|
|
127
122
|
response = begin
|
|
@@ -141,10 +136,12 @@ module RSpec::Buildkite::Analytics
|
|
|
141
136
|
end
|
|
142
137
|
else
|
|
143
138
|
request_id = response.to_hash["x-request-id"]
|
|
144
|
-
puts "
|
|
139
|
+
puts "rspec-buildkite-analytics could not establish an initial connection with Buildkite. You may be missing some data for this test suite, please contact support."
|
|
145
140
|
end
|
|
146
141
|
else
|
|
147
|
-
|
|
142
|
+
if !!ENV["BUILDKITE_BUILD_ID"]
|
|
143
|
+
puts "Buildkite Test Analytics: No Suite API key provided. You can get the API key from your Suite settings page."
|
|
144
|
+
end
|
|
148
145
|
end
|
|
149
146
|
end
|
|
150
147
|
|
|
@@ -162,12 +159,6 @@ module RSpec::Buildkite::Analytics
|
|
|
162
159
|
trace = RSpec::Buildkite::Analytics::Uploader::Trace.new(example, tracer.history)
|
|
163
160
|
RSpec::Buildkite::Analytics.uploader.traces << trace
|
|
164
161
|
end
|
|
165
|
-
|
|
166
|
-
config.after(:suite) do
|
|
167
|
-
# This needs the lonely operater as the session will be nil
|
|
168
|
-
# if auth against the API token fails
|
|
169
|
-
RSpec::Buildkite::Analytics.session&.close
|
|
170
|
-
end
|
|
171
162
|
end
|
|
172
163
|
|
|
173
164
|
RSpec::Buildkite::Analytics::Network.configure
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "timeout"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
|
|
5
6
|
require_relative "analytics/version"
|
|
6
7
|
|
|
@@ -15,11 +16,15 @@ module RSpec::Buildkite::Analytics
|
|
|
15
16
|
attr_accessor :url
|
|
16
17
|
attr_accessor :uploader
|
|
17
18
|
attr_accessor :session
|
|
19
|
+
attr_accessor :debug_enabled
|
|
20
|
+
attr_accessor :debug_filepath
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
def self.configure(token: nil, url: nil)
|
|
23
|
+
def self.configure(token: nil, url: nil, debug_enabled: false, debug_filepath: nil)
|
|
21
24
|
self.api_token = token || ENV["BUILDKITE_ANALYTICS_TOKEN"]
|
|
22
25
|
self.url = url || DEFAULT_URL
|
|
26
|
+
self.debug_enabled = debug_enabled || !!(ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"])
|
|
27
|
+
self.debug_filepath = debug_filepath || ENV["BUILDKITE_ANALYTICS_DEBUG_FILEPATH"] || Dir.tmpdir
|
|
23
28
|
|
|
24
29
|
require_relative "analytics/uploader"
|
|
25
30
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-buildkite-analytics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Buildkite
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-
|
|
11
|
+
date: 2021-12-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|