buildkite-test_collector 1.5.0 → 2.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|