buildkite-test_collector 1.5.0 → 2.1.0.pre
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/CHANGELOG.md +9 -0
- data/Gemfile.lock +5 -5
- data/README.md +0 -4
- data/lib/buildkite/test_collector/ci.rb +0 -1
- data/lib/buildkite/test_collector/http_client.rb +22 -0
- data/lib/buildkite/test_collector/library_hooks/minitest.rb +1 -1
- data/lib/buildkite/test_collector/library_hooks/rspec.rb +0 -2
- data/lib/buildkite/test_collector/minitest_plugin/reporter.rb +3 -11
- data/lib/buildkite/test_collector/minitest_plugin/trace.rb +0 -2
- data/lib/buildkite/test_collector/rspec_plugin/reporter.rb +5 -12
- data/lib/buildkite/test_collector/rspec_plugin/trace.rb +0 -1
- data/lib/buildkite/test_collector/session.rb +33 -309
- data/lib/buildkite/test_collector/uploader.rb +22 -29
- data/lib/buildkite/test_collector/version.rb +1 -1
- data/lib/buildkite/test_collector.rb +4 -37
- metadata +9 -11
- data/lib/buildkite/test_collector/logger.rb +0 -16
- data/lib/buildkite/test_collector/socket_connection.rb +0 -153
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e85ba163eccc317f8e03168153e2018d6b3033b760c3479d7e569848dee8be64
|
4
|
+
data.tar.gz: 2ee8f7c0088bd8cf0eff5b94bff12eaf93a5eec5b34e55e25f4bc31d130dbb53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 302e0420cf87e321faaaa4cf7843d322d1b9589a31411e4e7239c781f6176ab7d2a8ff2c0d3650912bccd0f863946d8815d7f0667e1cf132497b35410dcfa247
|
7
|
+
data.tar.gz: ff0a31165893a7368465be84a8ba8fbc6f863198a3b8d8965cee9941a29b785a6c547eb70cfa6c6e1283d4378970c0e217dfff87b657cc91837736ccf6c7a978
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## v2.1.0.pre
|
4
|
+
- Minitest plugin to use HTTP Upload API instead of websocket connection to send test data #178 #179 - @niceking
|
5
|
+
|
6
|
+
## v2.0.0.pre
|
7
|
+
|
8
|
+
- Major change: RSpec plugin to use HTTP Upload API instead of websocket connection to send test data #174 #175 - @niceking
|
9
|
+
- `identifier` field removed from trace #176 - @amybiyuliu
|
10
|
+
- Only warn on EOF errors and also catch SSLErrors #160 - @gchan
|
11
|
+
|
3
12
|
## v1.5.0
|
4
13
|
|
5
14
|
- Send `failure_expanded` from minitest #171 - @nprizal
|
data/Gemfile.lock
CHANGED
@@ -1,23 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
buildkite-test_collector (1.
|
4
|
+
buildkite-test_collector (2.1.0.pre)
|
5
5
|
activesupport (>= 4.2)
|
6
6
|
websocket (~> 1.2)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
activesupport (7.0.4.
|
11
|
+
activesupport (7.0.4.3)
|
12
12
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
13
|
i18n (>= 1.6, < 2)
|
14
14
|
minitest (>= 5.1)
|
15
15
|
tzinfo (~> 2.0)
|
16
|
-
concurrent-ruby (1.2.
|
16
|
+
concurrent-ruby (1.2.2)
|
17
17
|
diff-lcs (1.4.4)
|
18
18
|
i18n (1.12.0)
|
19
19
|
concurrent-ruby (~> 1.0)
|
20
|
-
minitest (5.
|
20
|
+
minitest (5.18.0)
|
21
21
|
rake (13.0.6)
|
22
22
|
rspec (3.10.0)
|
23
23
|
rspec-core (~> 3.10.0)
|
@@ -47,4 +47,4 @@ DEPENDENCIES
|
|
47
47
|
rspec-expectations (~> 3.10)
|
48
48
|
|
49
49
|
BUNDLED WITH
|
50
|
-
2.
|
50
|
+
2.3.25
|
data/README.md
CHANGED
@@ -89,10 +89,6 @@ BUILDKITE_ANALYTICS_EXECUTION_NAME_PREFIX
|
|
89
89
|
BUILDKITE_ANALYTICS_EXECUTION_NAME_SUFFIX
|
90
90
|
```
|
91
91
|
|
92
|
-
## 🔍 Debugging
|
93
|
-
|
94
|
-
To enable debugging output, set the `BUILDKITE_ANALYTICS_DEBUG_ENABLED` environment variable to `true`.
|
95
|
-
|
96
92
|
## 🔜 Roadmap
|
97
93
|
|
98
94
|
See the [GitHub 'enhancement' issues](https://github.com/buildkite/test-collector-ruby/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) for planned features. Pull requests are always welcome, and we’ll give you feedback and guidance if you choose to contribute 💚
|
@@ -34,7 +34,6 @@ class Buildkite::TestCollector::CI
|
|
34
34
|
"number" => ENV["BUILDKITE_ANALYTICS_NUMBER"],
|
35
35
|
"job_id" => ENV["BUILDKITE_ANALYTICS_JOB_ID"],
|
36
36
|
"message" => ENV["BUILDKITE_ANALYTICS_MESSAGE"],
|
37
|
-
"debug" => ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"],
|
38
37
|
"execution_name_prefix" => ENV["BUILDKITE_ANALYTICS_EXECUTION_NAME_PREFIX"],
|
39
38
|
"execution_name_suffix" => ENV["BUILDKITE_ANALYTICS_EXECUTION_NAME_SUFFIX"],
|
40
39
|
"version" => Buildkite::TestCollector::VERSION,
|
@@ -28,6 +28,28 @@ module Buildkite::TestCollector
|
|
28
28
|
http.request(contact)
|
29
29
|
end
|
30
30
|
|
31
|
+
def post_json(data)
|
32
|
+
contact_uri = URI.parse(url)
|
33
|
+
|
34
|
+
http = Net::HTTP.new(contact_uri.host, contact_uri.port)
|
35
|
+
http.use_ssl = contact_uri.scheme == "https"
|
36
|
+
|
37
|
+
contact = Net::HTTP::Post.new(contact_uri.path, {
|
38
|
+
"Authorization" => authorization_header,
|
39
|
+
"Content-Type" => "application/json",
|
40
|
+
})
|
41
|
+
|
42
|
+
data_set = data.map(&:as_hash)
|
43
|
+
|
44
|
+
contact.body = {
|
45
|
+
run_env: Buildkite::TestCollector::CI.env,
|
46
|
+
format: "json",
|
47
|
+
data: data_set
|
48
|
+
}.to_json
|
49
|
+
|
50
|
+
http.request(contact)
|
51
|
+
end
|
52
|
+
|
31
53
|
private
|
32
54
|
|
33
55
|
attr :url
|
@@ -11,8 +11,6 @@ Buildkite::TestCollector.uploader = Buildkite::TestCollector::Uploader
|
|
11
11
|
RSpec.configure do |config|
|
12
12
|
config.before(:suite) do
|
13
13
|
config.add_formatter Buildkite::TestCollector::RSpecPlugin::Reporter
|
14
|
-
|
15
|
-
Buildkite::TestCollector.safe { Buildkite::TestCollector::Uploader.configure }
|
16
14
|
end
|
17
15
|
|
18
16
|
config.around(:each) do |example|
|
@@ -13,7 +13,7 @@ module Buildkite::TestCollector::MinitestPlugin
|
|
13
13
|
|
14
14
|
if Buildkite::TestCollector.uploader
|
15
15
|
if trace = Buildkite::TestCollector.uploader.traces[result.source_location]
|
16
|
-
Buildkite::TestCollector.session
|
16
|
+
Buildkite::TestCollector.session.add_example_to_send_queue(result.source_location)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -21,16 +21,8 @@ module Buildkite::TestCollector::MinitestPlugin
|
|
21
21
|
def report
|
22
22
|
super
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
examples: count,
|
27
|
-
failed: failures,
|
28
|
-
pending: skips,
|
29
|
-
errors_outside_examples: 0, # Minitest does not report this
|
30
|
-
}
|
31
|
-
|
32
|
-
Buildkite::TestCollector.session.close(examples_count)
|
33
|
-
end
|
24
|
+
Buildkite::TestCollector.session.send_remaining_data
|
25
|
+
Buildkite::TestCollector.session.close
|
34
26
|
end
|
35
27
|
end
|
36
28
|
end
|
@@ -34,7 +34,6 @@ module Buildkite::TestCollector::MinitestPlugin
|
|
34
34
|
id: id,
|
35
35
|
scope: example.class.name,
|
36
36
|
name: example.name,
|
37
|
-
identifier: identifier,
|
38
37
|
location: location,
|
39
38
|
file_name: file_name,
|
40
39
|
result: result,
|
@@ -51,7 +50,6 @@ module Buildkite::TestCollector::MinitestPlugin
|
|
51
50
|
"#{file_name}:#{line_number}"
|
52
51
|
end
|
53
52
|
end
|
54
|
-
alias_method :identifier, :location
|
55
53
|
|
56
54
|
def file_name
|
57
55
|
@file_name ||= File.join('./', source_location[0].delete_prefix(project_dir))
|
@@ -7,6 +7,7 @@ module Buildkite::TestCollector::RSpecPlugin
|
|
7
7
|
attr_reader :output
|
8
8
|
|
9
9
|
def initialize(output)
|
10
|
+
Buildkite::TestCollector.session = Buildkite::TestCollector::Session.new
|
10
11
|
@output = output
|
11
12
|
end
|
12
13
|
|
@@ -19,21 +20,13 @@ module Buildkite::TestCollector::RSpecPlugin
|
|
19
20
|
if example.execution_result.status == :failed
|
20
21
|
trace.failure_reason, trace.failure_expanded = failure_info(notification)
|
21
22
|
end
|
22
|
-
Buildkite::TestCollector.session
|
23
|
+
Buildkite::TestCollector.session.add_example_to_send_queue(example.id)
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
|
-
def dump_summary(
|
27
|
-
|
28
|
-
|
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
|
-
Buildkite::TestCollector.session.close(examples_count)
|
36
|
-
end
|
27
|
+
def dump_summary(_notification)
|
28
|
+
Buildkite::TestCollector.session.send_remaining_data
|
29
|
+
Buildkite::TestCollector.session.close
|
37
30
|
end
|
38
31
|
|
39
32
|
alias_method :example_passed, :handle_example
|
@@ -2,332 +2,56 @@
|
|
2
2
|
|
3
3
|
module Buildkite::TestCollector
|
4
4
|
class Session
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
WAIT_BETWEEN_RECONNECTIONS = ENV.fetch("BUILDKITE_ANALYTICS_RECONNECTION_WAIT") { 5 }.to_i
|
5
|
+
UPLOAD_THREAD_TIMEOUT = 60
|
6
|
+
UPLOAD_SESSION_TIMEOUT = 60
|
7
|
+
UPLOAD_API_MAX_RESULTS = 5000
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
class RejectedSubscription < StandardError; end
|
15
|
-
class InitialConnectionFailure < StandardError; end
|
16
|
-
|
17
|
-
DISCONNECTED_EXCEPTIONS = [
|
18
|
-
Buildkite::TestCollector::SocketConnection::HandshakeError,
|
19
|
-
Buildkite::TestCollector::TimeoutError,
|
20
|
-
Buildkite::TestCollector::SocketConnection::SocketError,
|
21
|
-
RejectedSubscription,
|
22
|
-
InitialConnectionFailure,
|
23
|
-
]
|
24
|
-
|
25
|
-
def initialize(url, authorization_header, channel)
|
26
|
-
@establish_subscription_queue = Queue.new
|
27
|
-
@channel = channel
|
28
|
-
|
29
|
-
@unconfirmed_idents = {}
|
30
|
-
@idents_mutex = Mutex.new
|
31
|
-
@send_queue = Queue.new
|
32
|
-
@empty = ConditionVariable.new
|
33
|
-
@closing = false
|
34
|
-
@eot_queued = false
|
35
|
-
@eot_queued_mutex = Mutex.new
|
36
|
-
@reconnection_mutex = Mutex.new
|
37
|
-
|
38
|
-
@url = url
|
39
|
-
@authorization_header = authorization_header
|
40
|
-
|
41
|
-
reconnection_count = 0
|
42
|
-
|
43
|
-
begin
|
44
|
-
reconnection_count += 1
|
45
|
-
connect
|
46
|
-
rescue Buildkite::TestCollector::TimeoutError, InitialConnectionFailure => e
|
47
|
-
Buildkite::TestCollector.logger.warn("buildkite-test_collector could not establish an initial connection with Buildkite due to #{e}. Attempting retry #{reconnection_count} of #{MAX_RECONNECTION_ATTEMPTS}...")
|
48
|
-
if reconnection_count > MAX_RECONNECTION_ATTEMPTS
|
49
|
-
Buildkite::TestCollector.logger.error "buildkite-test_collector could not establish an initial connection with Buildkite due to #{e.message} after #{MAX_RECONNECTION_ATTEMPTS} attempts. You may be missing some data for this test suite, please contact support if this issue persists."
|
50
|
-
else
|
51
|
-
sleep(WAIT_BETWEEN_RECONNECTIONS)
|
52
|
-
Buildkite::TestCollector.logger.warn("retrying reconnection")
|
53
|
-
retry
|
54
|
-
end
|
55
|
-
end
|
56
|
-
init_write_thread
|
9
|
+
def initialize
|
10
|
+
@send_queue_ids = []
|
11
|
+
@upload_threads = []
|
57
12
|
end
|
58
13
|
|
59
|
-
def
|
60
|
-
@
|
61
|
-
# When the first thread detects a disconnection, it calls the disconnect method
|
62
|
-
# with the current connection. This thread grabs the reconnection mutex and does the
|
63
|
-
# reconnection, which then updates the value of @connection.
|
64
|
-
#
|
65
|
-
# At some point in that process, the second thread would have detected the
|
66
|
-
# disconnection too, and it also calls it with the current connection. However, the
|
67
|
-
# second thread can't run the reconnection code because of the mutex. By the
|
68
|
-
# time the mutex is released, the value of @connection has been refreshed, and so
|
69
|
-
# the second thread returns early and does not reattempt the reconnection.
|
70
|
-
return unless connection == @connection
|
71
|
-
Buildkite::TestCollector.logger.debug("starting reconnection")
|
72
|
-
|
73
|
-
reconnection_count = 0
|
14
|
+
def add_example_to_send_queue(id)
|
15
|
+
@send_queue_ids << id
|
74
16
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
init_write_thread
|
79
|
-
rescue *DISCONNECTED_EXCEPTIONS => e
|
80
|
-
Buildkite::TestCollector.logger.warn("failed reconnection attempt #{reconnection_count} due to #{e}")
|
81
|
-
if reconnection_count > MAX_RECONNECTION_ATTEMPTS
|
82
|
-
Buildkite::TestCollector.logger.error "buildkite-test_collector experienced a disconnection and could not reconnect to Buildkite due to #{e.message}. Please contact support."
|
83
|
-
raise e
|
84
|
-
else
|
85
|
-
sleep(WAIT_BETWEEN_RECONNECTIONS)
|
86
|
-
Buildkite::TestCollector.logger.warn("retrying reconnection")
|
87
|
-
retry
|
88
|
-
end
|
89
|
-
end
|
17
|
+
if @send_queue_ids.size >= Buildkite::TestCollector.batch_size
|
18
|
+
send_ids = @send_queue_ids.shift(Buildkite::TestCollector.batch_size)
|
19
|
+
upload_data(send_ids)
|
90
20
|
end
|
91
|
-
retransmit
|
92
21
|
end
|
93
22
|
|
94
|
-
def
|
95
|
-
|
96
|
-
@examples_count = examples_count
|
97
|
-
Buildkite::TestCollector.logger.debug("closing socket connection")
|
23
|
+
def send_remaining_data
|
24
|
+
return if @send_queue_ids.empty?
|
98
25
|
|
99
|
-
|
100
|
-
# data it uploads to S3, we'll never get confirmation of the
|
101
|
-
# identifiers of the last upload part unless we send an explicit finish,
|
102
|
-
# to which the server will respond with the last bits of data
|
103
|
-
send_eot
|
104
|
-
|
105
|
-
# After EOT, we wait for 75 seconds for the send queue to be drained and for the
|
106
|
-
# server to confirm the last idents. If everything has already been confirmed we can
|
107
|
-
# proceed without waiting.
|
108
|
-
@idents_mutex.synchronize do
|
109
|
-
if @unconfirmed_idents.any?
|
110
|
-
Buildkite::TestCollector.logger.debug "Waiting for Buildkite Test Analytics to send results..."
|
111
|
-
Buildkite::TestCollector.logger.debug("waiting for last confirm")
|
112
|
-
|
113
|
-
@empty.wait(@idents_mutex, CONFIRMATION_TIMEOUT)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# Then we always disconnect cos we can't wait forever? 🤷♀️
|
118
|
-
@connection.close
|
119
|
-
# We kill the write thread cos it's got a while loop in it, so it won't finish otherwise
|
120
|
-
@write_thread&.kill
|
121
|
-
|
122
|
-
Buildkite::TestCollector.logger.info "Buildkite Test Analytics completed"
|
123
|
-
Buildkite::TestCollector.logger.debug("socket connection closed")
|
26
|
+
upload_data(@send_queue_ids)
|
124
27
|
end
|
125
28
|
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
@establish_subscription_queue.push(data)
|
137
|
-
Buildkite::TestCollector.logger.debug("received #{data['type']}")
|
138
|
-
when "reject_subscription"
|
139
|
-
Buildkite::TestCollector.logger.debug("received rejected_subscription")
|
140
|
-
raise RejectedSubscription
|
141
|
-
else
|
142
|
-
process_message(data)
|
143
|
-
end
|
144
|
-
end
|
29
|
+
def close
|
30
|
+
# There are two thread joins here, because the inner join will wait up to
|
31
|
+
# UPLOAD_THREAD_TIMEOUT seconds PER thread that is uploading data, i.e.
|
32
|
+
# n_threads x UPLOAD_THREAD_TIMEOUT latency if Buildkite happens to be
|
33
|
+
# down. By wrapping that in an outer thread join with the
|
34
|
+
# UPLOAD_SESSION_TIMEOUT, we ensure that we only wait a max of
|
35
|
+
# UPLOAD_SESSION_TIMEOUT seconds before the session exits.
|
36
|
+
Thread.new do
|
37
|
+
@upload_threads.each { |t| t.join(UPLOAD_THREAD_TIMEOUT) }
|
38
|
+
end.join(UPLOAD_SESSION_TIMEOUT)
|
145
39
|
|
146
|
-
|
147
|
-
queue_and_track_result(result.id, result.as_hash)
|
148
|
-
|
149
|
-
Buildkite::TestCollector.logger.debug("added #{result.id} to send queue")
|
150
|
-
end
|
151
|
-
|
152
|
-
def unconfirmed_idents_count
|
153
|
-
@idents_mutex.synchronize do
|
154
|
-
@unconfirmed_idents.count
|
155
|
-
end
|
40
|
+
@upload_threads.each { |t| t&.kill }
|
156
41
|
end
|
157
42
|
|
158
43
|
private
|
159
44
|
|
160
|
-
def
|
161
|
-
Buildkite::TestCollector.
|
162
|
-
|
163
|
-
@connection = SocketConnection.new(self, @url, {
|
164
|
-
"Authorization" => @authorization_header,
|
165
|
-
})
|
166
|
-
|
167
|
-
wait_for_welcome
|
168
|
-
|
169
|
-
@connection.transmit({
|
170
|
-
"command" => "subscribe",
|
171
|
-
"identifier" => @channel
|
172
|
-
})
|
173
|
-
|
174
|
-
wait_for_confirm
|
175
|
-
|
176
|
-
Buildkite::TestCollector.logger.info "Connected to Buildkite Test Analytics!"
|
177
|
-
Buildkite::TestCollector.logger.debug("connected")
|
178
|
-
end
|
179
|
-
|
180
|
-
def init_write_thread
|
181
|
-
# As this method can be called multiple times in the
|
182
|
-
# reconnection process, kill prev write threads (if any) before
|
183
|
-
# setting up the new one
|
184
|
-
@write_thread&.kill
|
45
|
+
def upload_data(ids)
|
46
|
+
data = Buildkite::TestCollector.uploader.traces.values_at(*ids).compact
|
185
47
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
if message_type == "end_of_transmission"
|
194
|
-
# Because of the unpredictable sequencing between the test suite finishing
|
195
|
-
# (EOT gets queued) and disconnections happening (retransmit results gets
|
196
|
-
# queued), we don't want to send an EOT before any retransmits are sent.
|
197
|
-
if @send_queue.length > 0
|
198
|
-
@send_queue << data
|
199
|
-
Buildkite::TestCollector.logger.debug("putting eot at back of queue")
|
200
|
-
next
|
201
|
-
end
|
202
|
-
@eot_queued_mutex.synchronize do
|
203
|
-
@eot_queued = false
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
@connection.transmit({
|
208
|
-
"identifier" => @channel,
|
209
|
-
"command" => "message",
|
210
|
-
"data" => data.to_json
|
211
|
-
})
|
212
|
-
|
213
|
-
if Buildkite::TestCollector.debug_enabled
|
214
|
-
ids = if message_type == "record_results"
|
215
|
-
data["results"].map { |result| result["id"] }
|
216
|
-
end
|
217
|
-
Buildkite::TestCollector.logger.debug("transmitted #{message_type} #{ids}")
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
def pop_with_timeout(message_type)
|
224
|
-
Timeout.timeout(30, Buildkite::TestCollector::TimeoutError, "Timeout: Waited 30 seconds for #{message_type}") do
|
225
|
-
@establish_subscription_queue.pop
|
48
|
+
# we do this in batches of UPLOAD_API_MAX_RESULTS in case the number of
|
49
|
+
# results exceeds this due to a bug, or user error in configuring the
|
50
|
+
# batch size
|
51
|
+
data.each_slice(UPLOAD_API_MAX_RESULTS) do |batch|
|
52
|
+
new_thread = Buildkite::TestCollector::Uploader.upload(batch)
|
53
|
+
@upload_threads << new_thread if new_thread
|
226
54
|
end
|
227
55
|
end
|
228
|
-
|
229
|
-
def wait_for_welcome
|
230
|
-
welcome = pop_with_timeout("welcome")
|
231
|
-
|
232
|
-
if welcome && welcome != { "type" => "welcome" }
|
233
|
-
raise InitialConnectionFailure.new("Wrong message received, expected a welcome, but received: #{welcome.inspect}")
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
def wait_for_confirm
|
238
|
-
confirm = pop_with_timeout("confirm")
|
239
|
-
|
240
|
-
if confirm && confirm != { "type" => "confirm_subscription", "identifier" => @channel }
|
241
|
-
raise InitialConnectionFailure.new("Wrong message received, expected a confirm, but received: #{confirm.inspect}")
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
def queue_and_track_result(ident, result_as_hash)
|
246
|
-
@idents_mutex.synchronize do
|
247
|
-
@unconfirmed_idents[ident] = result_as_hash
|
248
|
-
|
249
|
-
@send_queue << {
|
250
|
-
"action" => "record_results",
|
251
|
-
"results" => [result_as_hash]
|
252
|
-
}
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
def confirm_idents(idents)
|
257
|
-
retransmit_required = @closing
|
258
|
-
|
259
|
-
@idents_mutex.synchronize do
|
260
|
-
# Remove received idents from unconfirmed_idents
|
261
|
-
idents.each { |key| @unconfirmed_idents.delete(key) }
|
262
|
-
|
263
|
-
Buildkite::TestCollector.logger.debug("received confirm for indentifiers: #{idents}")
|
264
|
-
|
265
|
-
# This @empty ConditionVariable broadcasts every time that @unconfirmed_idents is
|
266
|
-
# empty, which will happen about every 10mb of data as that's when the server
|
267
|
-
# sends back confirmations.
|
268
|
-
#
|
269
|
-
# However, there aren't any threads waiting on this signal until after we
|
270
|
-
# send the EOT message, so the prior broadcasts shouldn't do anything.
|
271
|
-
if @unconfirmed_idents.empty?
|
272
|
-
@empty.broadcast
|
273
|
-
|
274
|
-
retransmit_required = false
|
275
|
-
|
276
|
-
Buildkite::TestCollector.logger.debug("all identifiers have been confirmed")
|
277
|
-
else
|
278
|
-
Buildkite::TestCollector.logger.debug("still waiting on confirm for identifiers: #{@unconfirmed_idents.keys}")
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
# If we're closing, any unconfirmed results need to be retransmitted.
|
283
|
-
retransmit if retransmit_required
|
284
|
-
end
|
285
|
-
|
286
|
-
def send_eot
|
287
|
-
@eot_queued_mutex.synchronize do
|
288
|
-
return if @eot_queued
|
289
|
-
|
290
|
-
@send_queue << {
|
291
|
-
"action" => "end_of_transmission",
|
292
|
-
"examples_count" => @examples_count.to_json
|
293
|
-
}
|
294
|
-
@eot_queued = true
|
295
|
-
|
296
|
-
Buildkite::TestCollector.logger.debug("added EOT to send queue")
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
def process_message(data)
|
301
|
-
# Check we're getting the data we expect
|
302
|
-
return unless data["identifier"] == @channel
|
303
|
-
|
304
|
-
case
|
305
|
-
when data["message"].key?("confirm")
|
306
|
-
confirm_idents(data["message"]["confirm"])
|
307
|
-
else
|
308
|
-
# unhandled message
|
309
|
-
Buildkite::TestCollector.logger.debug("received unhandled message #{data["message"]}")
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
def retransmit
|
314
|
-
@idents_mutex.synchronize do
|
315
|
-
results = @unconfirmed_idents.values
|
316
|
-
|
317
|
-
# queue the contents of the buffer, unless it's empty
|
318
|
-
if results.any?
|
319
|
-
@send_queue << {
|
320
|
-
"action" => "record_results",
|
321
|
-
"results" => results
|
322
|
-
}
|
323
|
-
|
324
|
-
Buildkite::TestCollector.logger.debug("queueing up retransmitted results #{@unconfirmed_idents.keys}")
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
# if we were disconnected in the closing phase, then resend the EOT
|
329
|
-
# message so the server can persist the last upload part
|
330
|
-
send_eot if @closing
|
331
|
-
end
|
332
56
|
end
|
333
57
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Buildkite::TestCollector
|
4
4
|
class Uploader
|
5
|
+
MAX_UPLOAD_ATTEMPTS = 3
|
6
|
+
|
5
7
|
def self.traces
|
6
8
|
@traces ||= {}
|
7
9
|
end
|
@@ -17,42 +19,33 @@ module Buildkite::TestCollector
|
|
17
19
|
EOFError
|
18
20
|
]
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
RETRYABLE_UPLOAD_ERRORS = [
|
23
|
+
Net::ReadTimeout,
|
24
|
+
Net::OpenTimeout,
|
25
|
+
OpenSSL::SSL::SSLError,
|
26
|
+
OpenSSL::SSL::SSLErrorWaitReadable,
|
27
|
+
EOFError
|
28
|
+
]
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
Buildkite::TestCollector.logger.error "Buildkite Test Analytics: Error communicating with the server: #{e.message}"
|
30
|
-
end
|
30
|
+
def self.tracer
|
31
|
+
Thread.current[:_buildkite_tracer]
|
32
|
+
end
|
31
33
|
|
32
|
-
|
34
|
+
def self.upload(data)
|
35
|
+
return false unless Buildkite::TestCollector.api_token
|
33
36
|
|
34
|
-
|
35
|
-
when "401"
|
36
|
-
Buildkite::TestCollector.logger.info "Buildkite Test Analytics: Invalid Suite API key. Please double check your Suite API key."
|
37
|
-
when "200"
|
38
|
-
json = JSON.parse(response.body)
|
37
|
+
http = Buildkite::TestCollector::HTTPClient.new(Buildkite::TestCollector.url)
|
39
38
|
|
40
|
-
|
41
|
-
|
39
|
+
Thread.new do
|
40
|
+
response = begin
|
41
|
+
upload_attempts ||= 0
|
42
|
+
http.post_json(data)
|
43
|
+
rescue *Buildkite::TestCollector::Uploader::RETRYABLE_UPLOAD_ERRORS => e
|
44
|
+
if (upload_attempts += 1) < MAX_UPLOAD_ATTEMPTS
|
45
|
+
retry
|
42
46
|
end
|
43
|
-
else
|
44
|
-
request_id = response.to_hash["x-request-id"]
|
45
|
-
Buildkite::TestCollector.logger.info "buildkite-test_collector could not establish an initial connection with Buildkite. You may be missing some data for this test suite, please contact support with request ID #{request_id}."
|
46
|
-
end
|
47
|
-
else
|
48
|
-
if !!ENV["BUILDKITE_BUILD_ID"]
|
49
|
-
Buildkite::TestCollector.logger.info "Buildkite Test Analytics: No Suite API key provided. You can get the API key from your Suite settings page."
|
50
47
|
end
|
51
48
|
end
|
52
49
|
end
|
53
|
-
|
54
|
-
def self.tracer
|
55
|
-
Thread.current[:_buildkite_tracer]
|
56
|
-
end
|
57
50
|
end
|
58
51
|
end
|
@@ -22,39 +22,37 @@ require "active_support/notifications"
|
|
22
22
|
|
23
23
|
require_relative "test_collector/version"
|
24
24
|
require_relative "test_collector/error"
|
25
|
-
require_relative "test_collector/logger"
|
26
25
|
require_relative "test_collector/ci"
|
27
26
|
require_relative "test_collector/http_client"
|
28
27
|
require_relative "test_collector/uploader"
|
29
28
|
require_relative "test_collector/network"
|
30
29
|
require_relative "test_collector/object"
|
31
30
|
require_relative "test_collector/tracer"
|
32
|
-
require_relative "test_collector/socket_connection"
|
33
31
|
require_relative "test_collector/session"
|
34
32
|
|
35
33
|
module Buildkite
|
36
34
|
module TestCollector
|
37
35
|
DEFAULT_URL = "https://analytics-api.buildkite.com/v1/uploads"
|
36
|
+
DEFAULT_UPLOAD_BATCH_SIZE = 500
|
38
37
|
|
39
38
|
class << self
|
40
39
|
attr_accessor :api_token
|
41
40
|
attr_accessor :url
|
42
41
|
attr_accessor :uploader
|
43
42
|
attr_accessor :session
|
44
|
-
attr_accessor :debug_enabled
|
45
43
|
attr_accessor :tracing_enabled
|
46
44
|
attr_accessor :artifact_path
|
47
45
|
attr_accessor :env
|
46
|
+
attr_accessor :batch_size
|
48
47
|
end
|
49
48
|
|
50
|
-
def self.configure(hook:, token: nil, url: nil,
|
49
|
+
def self.configure(hook:, token: nil, url: nil, tracing_enabled: true, artifact_path: nil, env: {})
|
51
50
|
self.api_token = (token || ENV["BUILDKITE_ANALYTICS_TOKEN"])&.strip
|
52
51
|
self.url = url || DEFAULT_URL
|
53
|
-
self.debug_enabled = debug_enabled || !!(ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"])
|
54
52
|
self.tracing_enabled = tracing_enabled
|
55
53
|
self.artifact_path = artifact_path
|
56
54
|
self.env = env
|
57
|
-
|
55
|
+
self.batch_size = ENV.fetch("BUILDKITE_ANALYTICS_UPLOAD_BATCH_SIZE") { DEFAULT_UPLOAD_BATCH_SIZE }.to_i
|
58
56
|
self.hook_into(hook)
|
59
57
|
end
|
60
58
|
|
@@ -71,30 +69,6 @@ module Buildkite
|
|
71
69
|
tracer&.leave
|
72
70
|
end
|
73
71
|
|
74
|
-
def self.log_formatter
|
75
|
-
@log_formatter ||= Buildkite::TestCollector::Logger::Formatter.new
|
76
|
-
end
|
77
|
-
|
78
|
-
def self.log_formatter=(log_formatter)
|
79
|
-
@log_formatter = log_formatter
|
80
|
-
logger.formatter = log_formatter
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.logger=(logger)
|
84
|
-
@logger = logger
|
85
|
-
end
|
86
|
-
|
87
|
-
def self.logger
|
88
|
-
return @logger if defined?(@logger)
|
89
|
-
|
90
|
-
debug_mode = ENV.fetch("BUILDKITE_ANALYTICS_DEBUG_ENABLED") do
|
91
|
-
$DEBUG
|
92
|
-
end
|
93
|
-
|
94
|
-
level = !!debug_mode ? ::Logger::DEBUG : ::Logger::WARN
|
95
|
-
@logger ||= Buildkite::TestCollector::Logger.new($stderr, level: level)
|
96
|
-
end
|
97
|
-
|
98
72
|
def self.enable_tracing!
|
99
73
|
return unless self.tracing_enabled
|
100
74
|
|
@@ -105,12 +79,5 @@ module Buildkite
|
|
105
79
|
Buildkite::TestCollector::Uploader.tracer&.backfill(:sql, finish - start, **{ query: payload[:sql] })
|
106
80
|
end
|
107
81
|
end
|
108
|
-
|
109
|
-
def self.safe(&block)
|
110
|
-
block.call
|
111
|
-
rescue StandardError => e
|
112
|
-
logger.error("Buildkite::TestCollector received exception: #{e}")
|
113
|
-
logger.error("Backtrace:\n#{e.backtrace.join("\n")}")
|
114
|
-
end
|
115
82
|
end
|
116
83
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: buildkite-test_collector
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 2.1.0.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Buildkite
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '3.10'
|
69
|
-
description:
|
69
|
+
description:
|
70
70
|
email:
|
71
71
|
- support+analytics@buildkite.com
|
72
72
|
executables: []
|
@@ -94,7 +94,6 @@ files:
|
|
94
94
|
- lib/buildkite/test_collector/http_client.rb
|
95
95
|
- lib/buildkite/test_collector/library_hooks/minitest.rb
|
96
96
|
- lib/buildkite/test_collector/library_hooks/rspec.rb
|
97
|
-
- lib/buildkite/test_collector/logger.rb
|
98
97
|
- lib/buildkite/test_collector/minitest_plugin.rb
|
99
98
|
- lib/buildkite/test_collector/minitest_plugin/reporter.rb
|
100
99
|
- lib/buildkite/test_collector/minitest_plugin/trace.rb
|
@@ -103,7 +102,6 @@ files:
|
|
103
102
|
- lib/buildkite/test_collector/rspec_plugin/reporter.rb
|
104
103
|
- lib/buildkite/test_collector/rspec_plugin/trace.rb
|
105
104
|
- lib/buildkite/test_collector/session.rb
|
106
|
-
- lib/buildkite/test_collector/socket_connection.rb
|
107
105
|
- lib/buildkite/test_collector/tracer.rb
|
108
106
|
- lib/buildkite/test_collector/uploader.rb
|
109
107
|
- lib/buildkite/test_collector/version.rb
|
@@ -114,7 +112,7 @@ licenses:
|
|
114
112
|
metadata:
|
115
113
|
homepage_uri: https://github.com/buildkite/test-collector-ruby
|
116
114
|
source_code_uri: https://github.com/buildkite/test-collector-ruby
|
117
|
-
post_install_message:
|
115
|
+
post_install_message:
|
118
116
|
rdoc_options: []
|
119
117
|
require_paths:
|
120
118
|
- lib
|
@@ -125,12 +123,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
125
123
|
version: 2.3.0
|
126
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
125
|
requirements:
|
128
|
-
- - "
|
126
|
+
- - ">"
|
129
127
|
- !ruby/object:Gem::Version
|
130
|
-
version:
|
128
|
+
version: 1.3.1
|
131
129
|
requirements: []
|
132
|
-
rubygems_version: 3.
|
133
|
-
signing_key:
|
130
|
+
rubygems_version: 3.1.6
|
131
|
+
signing_key:
|
134
132
|
specification_version: 4
|
135
133
|
summary: Track test executions and report to Buildkite Test Analytics
|
136
134
|
test_files: []
|
@@ -1,16 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Buildkite::TestCollector
|
4
|
-
class Logger < ::Logger
|
5
|
-
class Formatter < ::Logger::Formatter
|
6
|
-
def call(severity, time, _program, message)
|
7
|
-
"#{time.utc.iso8601(9)} pid=#{::Process.pid} tid=#{::Thread.current.object_id} #{severity}: #{message}\n"
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize(*args, **kwargs)
|
12
|
-
super
|
13
|
-
self.formatter = Buildkite::TestCollector.log_formatter
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,153 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Buildkite::TestCollector
|
4
|
-
class SocketConnection
|
5
|
-
class HandshakeError < StandardError; end
|
6
|
-
class SocketError < StandardError; end
|
7
|
-
|
8
|
-
def initialize(session, url, headers)
|
9
|
-
uri = URI.parse(url)
|
10
|
-
@session = session
|
11
|
-
protocol = "http"
|
12
|
-
|
13
|
-
begin
|
14
|
-
socket = TCPSocket.new(uri.host, uri.port || (uri.scheme == "wss" ? 443 : 80))
|
15
|
-
|
16
|
-
if uri.scheme == "wss"
|
17
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
18
|
-
protocol = "https"
|
19
|
-
|
20
|
-
ctx.min_version = :TLS1_2
|
21
|
-
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
22
|
-
ctx.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
|
23
|
-
|
24
|
-
socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
25
|
-
socket.connect
|
26
|
-
end
|
27
|
-
rescue
|
28
|
-
# We are rescuing all here, as there are a range of Errno errors that could be
|
29
|
-
# raised when we fail to establish a TCP connection
|
30
|
-
raise SocketError
|
31
|
-
end
|
32
|
-
|
33
|
-
@socket = socket
|
34
|
-
|
35
|
-
headers = { "Origin" => "#{protocol}://#{uri.host}" }.merge(headers)
|
36
|
-
handshake = WebSocket::Handshake::Client.new(url: url, headers: headers)
|
37
|
-
|
38
|
-
@socket.write handshake.to_s
|
39
|
-
|
40
|
-
until handshake.finished?
|
41
|
-
if byte = @socket.getc
|
42
|
-
handshake << byte
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# The errors below are raised when we establish the TCP connection, but get back
|
47
|
-
# an error, i.e. in dev we can still connect to puma-dev while nginx isn't
|
48
|
-
# running, or in prod we can hit a load balancer while app is down
|
49
|
-
unless handshake.valid?
|
50
|
-
case handshake.error
|
51
|
-
when Exception, String
|
52
|
-
raise HandshakeError.new(handshake.error)
|
53
|
-
when nil
|
54
|
-
raise HandshakeError.new("Invalid handshake")
|
55
|
-
else
|
56
|
-
raise HandshakeError.new(handshake.error.inspect)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
@version = handshake.version
|
61
|
-
|
62
|
-
# Setting up a new thread that listens on the socket, and processes incoming
|
63
|
-
# comms from the server
|
64
|
-
@read_thread = Thread.new do
|
65
|
-
Buildkite::TestCollector.logger.debug("listening in on socket")
|
66
|
-
frame = WebSocket::Frame::Incoming::Client.new
|
67
|
-
|
68
|
-
while @socket
|
69
|
-
frame << @socket.readpartial(4096)
|
70
|
-
|
71
|
-
while data = frame.next
|
72
|
-
@session.handle(self, data.data)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
# These get re-raise from session, we should fail gracefully
|
76
|
-
rescue *Buildkite::TestCollector::Session::DISCONNECTED_EXCEPTIONS => e
|
77
|
-
Buildkite::TestCollector.logger.error("We could not establish a connection with Buildkite Test Analytics. The error was: #{e.message}. If this is a problem, please contact support.")
|
78
|
-
rescue EOFError => e
|
79
|
-
Buildkite::TestCollector.logger.warn("#{e}")
|
80
|
-
if @socket
|
81
|
-
Buildkite::TestCollector.logger.error("attempting disconnected flow")
|
82
|
-
@session.disconnected(self)
|
83
|
-
disconnect
|
84
|
-
end
|
85
|
-
rescue Errno::ECONNRESET, Errno::ETIMEDOUT => e
|
86
|
-
Buildkite::TestCollector.logger.error("#{e}")
|
87
|
-
if @socket
|
88
|
-
Buildkite::TestCollector.logger.error("attempting disconnected flow")
|
89
|
-
@session.disconnected(self)
|
90
|
-
disconnect
|
91
|
-
end
|
92
|
-
rescue IOError
|
93
|
-
# This is fine to ignore
|
94
|
-
Buildkite::TestCollector.logger.error("IOError")
|
95
|
-
rescue IndexError
|
96
|
-
# I don't like that we're doing this but I think it's the best of the options
|
97
|
-
#
|
98
|
-
# This relates to this issue https://github.com/ruby/openssl/issues/452
|
99
|
-
# A fix for it has been released but the repercussions of overriding
|
100
|
-
# the OpenSSL version in the stdlib seem worse than catching this error here.
|
101
|
-
Buildkite::TestCollector.logger.error("IndexError")
|
102
|
-
if @socket
|
103
|
-
Buildkite::TestCollector.logger.error("attempting disconnected flow")
|
104
|
-
@session.disconnected(self)
|
105
|
-
disconnect
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def transmit(data, type: :text)
|
111
|
-
# this line prevents us from calling disconnect twice
|
112
|
-
return if @socket.nil?
|
113
|
-
|
114
|
-
raw_data = data.to_json
|
115
|
-
frame = WebSocket::Frame::Outgoing::Client.new(data: raw_data, type: :text, version: @version)
|
116
|
-
@socket.write(frame.to_s)
|
117
|
-
rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError => e
|
118
|
-
return unless @socket
|
119
|
-
return if type == :close
|
120
|
-
Buildkite::TestCollector.logger.error("got #{e}, attempting disconnected flow")
|
121
|
-
@session.disconnected(self)
|
122
|
-
disconnect
|
123
|
-
rescue IndexError
|
124
|
-
# I don't like that we're doing this but I think it's the best of the options
|
125
|
-
#
|
126
|
-
# This relates to this issue https://github.com/ruby/openssl/issues/452
|
127
|
-
# A fix for it has been released but the repercussions of overriding
|
128
|
-
# the OpenSSL version in the stdlib seem worse than catching this error here.
|
129
|
-
Buildkite::TestCollector.logger.error("IndexError")
|
130
|
-
if @socket
|
131
|
-
Buildkite::TestCollector.logger.error("attempting disconnected flow")
|
132
|
-
@session.disconnected(self)
|
133
|
-
disconnect
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def close
|
138
|
-
Buildkite::TestCollector.logger.debug("socket close")
|
139
|
-
transmit(nil, type: :close)
|
140
|
-
disconnect
|
141
|
-
end
|
142
|
-
|
143
|
-
private
|
144
|
-
|
145
|
-
def disconnect
|
146
|
-
Buildkite::TestCollector.logger.debug("socket disconnect")
|
147
|
-
socket = @socket
|
148
|
-
@socket = nil
|
149
|
-
socket&.close
|
150
|
-
@read_thread&.join unless @read_thread == Thread.current
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|