rspec-mergify 0.2.0-aarch64-linux → 0.3.0-aarch64-linux
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/README.md +17 -0
- data/lib/mergify/rspec/3.1/mergify_ci.so +0 -0
- data/lib/mergify/rspec/3.2/mergify_ci.so +0 -0
- data/lib/mergify/rspec/3.3/mergify_ci.so +0 -0
- data/lib/mergify/rspec/3.4/mergify_ci.so +0 -0
- data/lib/mergify/rspec/4.0/mergify_ci.so +0 -0
- data/lib/mergify/rspec/ci_insights.rb +46 -75
- data/lib/mergify/rspec/flaky_detection.rb +12 -9
- data/lib/mergify/rspec/formatter.rb +34 -43
- data/lib/mergify/rspec/native.rb +31 -10
- data/lib/mergify/rspec/resources/rspec.rb +9 -5
- data/lib/mergify/rspec/trace.rb +145 -0
- data/lib/mergify/rspec/version.rb +1 -1
- metadata +3 -31
- data/lib/mergify/rspec/synchronous_batch_span_processor.rb +0 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36fb92630fed9bfe3a9a6c8c0f4d3994e44176a40667ae2dd6bec8c9a70479b4
|
|
4
|
+
data.tar.gz: 2dc9b9adf8e0b48800f1be33f75289e21cb5a26b962255d5d8aaacaef6a95588
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 453494cb72c72f3b27d881720c499a95a41875f75f0d05256c9d497ac3e28f69f2ecdb529ae1a75578d834d3ddb4e16e0e5be2b70c00d8560f67fbdb04495d89
|
|
7
|
+
data.tar.gz: f7f1455b98ad01e4ae659c6e4cf7a8ddb01da1fc84782554aece4513ad11d8c52e3deaa273d069a85ed67e7d7e85f1f455c0951530ac8d94b70661e3c063283e
|
data/README.md
CHANGED
|
@@ -18,6 +18,23 @@ gem 'rspec-mergify'
|
|
|
18
18
|
|
|
19
19
|
Then run `bundle install`.
|
|
20
20
|
|
|
21
|
+
### Supported platforms
|
|
22
|
+
|
|
23
|
+
The gem ships precompiled, so nothing is built on your machine. Each platform gem carries an extension for Ruby 3.1 through 4.0:
|
|
24
|
+
|
|
25
|
+
| Platform | Requirement |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Linux x86_64 / aarch64 (glibc) | **glibc 2.30 or newer** — Debian 11+, Ubuntu 20.04+, RHEL 9+, Amazon Linux 2023 |
|
|
28
|
+
| Linux x86_64 / aarch64 (musl) | Alpine; on Ruby 3.1 also `apk add libgcc` |
|
|
29
|
+
| macOS arm64 / x86_64 | — |
|
|
30
|
+
| Windows x64 (UCRT) | — |
|
|
31
|
+
|
|
32
|
+
Anywhere else, and on glibc older than 2.30 — RHEL, Rocky, AlmaLinux and Oracle Linux 8, Amazon Linux 2, CentOS 7 — the extension cannot load. The gem still installs and your suite still runs, but nothing is reported to Mergify. Pin the last pure-Ruby release on those systems:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem 'rspec-mergify', '0.1.4'
|
|
36
|
+
```
|
|
37
|
+
|
|
21
38
|
## Configuration
|
|
22
39
|
|
|
23
40
|
Set the `MERGIFY_TOKEN` environment variable with your Mergify API token.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
|
-
|
|
4
|
+
require_relative 'trace'
|
|
5
5
|
require_relative 'utils'
|
|
6
6
|
require_relative 'native'
|
|
7
|
-
require_relative 'synchronous_batch_span_processor'
|
|
8
7
|
require_relative 'resources/rspec'
|
|
9
8
|
|
|
10
9
|
module Mergify
|
|
11
10
|
module RSpec
|
|
12
|
-
# Central orchestrator for Mergify Test Insights:
|
|
13
|
-
#
|
|
14
|
-
# rubocop:disable-next Metrics/ClassLength
|
|
11
|
+
# Central orchestrator for Mergify Test Insights: records the run's spans,
|
|
12
|
+
# uploads them, and coordinates flaky detection and quarantine.
|
|
15
13
|
class CIInsights
|
|
16
14
|
attr_reader :token, :repo_name, :api_url, :test_run_id,
|
|
17
|
-
:
|
|
15
|
+
:recorder,
|
|
18
16
|
:branch_name,
|
|
19
17
|
:flaky_detector, :flaky_detector_error_message, :quarantined_tests
|
|
20
18
|
|
|
@@ -24,9 +22,8 @@ module Mergify
|
|
|
24
22
|
@repo_name = Native.detect_repository_name
|
|
25
23
|
@api_url = ENV.fetch('MERGIFY_API_URL', 'https://api.mergify.com')
|
|
26
24
|
@test_run_id = SecureRandom.hex(8)
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
@exporter = nil
|
|
25
|
+
@recorder = nil
|
|
26
|
+
@uploads = false
|
|
30
27
|
@branch_name = nil
|
|
31
28
|
@flaky_detector = nil
|
|
32
29
|
@flaky_detector_error_message = nil
|
|
@@ -35,6 +32,21 @@ module Mergify
|
|
|
35
32
|
setup_tracing if Utils.in_ci?
|
|
36
33
|
end
|
|
37
34
|
|
|
35
|
+
# Send the run, once, at the end. Failing to report a run is worth
|
|
36
|
+
# saying out loud but never worth failing a suite that just passed,
|
|
37
|
+
# so this answers with a message instead of raising.
|
|
38
|
+
def flush
|
|
39
|
+
return nil unless @recorder && @uploads
|
|
40
|
+
return nil if @recorder.finished_spans.empty?
|
|
41
|
+
|
|
42
|
+
owner, repo = Utils.split_full_repo_name(@repo_name)
|
|
43
|
+
client = Native::Client.new(@api_url, @token, owner, repo, Mergify::RSpec::VERSION)
|
|
44
|
+
client.upload_trace(@recorder.resource_attributes, @recorder.finished_spans.map(&:to_h))
|
|
45
|
+
nil
|
|
46
|
+
rescue Native::ApiError, Utils::InvalidRepositoryFullNameError => e
|
|
47
|
+
e.message
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def mark_test_as_quarantined_if_needed(example_id) # rubocop:disable Naming/PredicateMethod
|
|
39
51
|
return false unless @quarantined_tests&.include?(example_id)
|
|
40
52
|
|
|
@@ -44,30 +56,25 @@ module Mergify
|
|
|
44
56
|
|
|
45
57
|
private
|
|
46
58
|
|
|
59
|
+
# Recording is unconditional; whether the run is *uploaded* is what the
|
|
60
|
+
# token and repository decide. Debug and test runs keep their spans and
|
|
61
|
+
# send nothing, which is what they always did -- the difference is that
|
|
62
|
+
# the collector is the same object either way instead of two processors.
|
|
47
63
|
def setup_tracing
|
|
48
|
-
processor, exp = build_processor
|
|
49
|
-
return unless processor
|
|
50
|
-
|
|
51
|
-
@exporter = exp
|
|
52
64
|
resource = build_resource
|
|
53
|
-
@
|
|
54
|
-
|
|
55
|
-
@
|
|
56
|
-
|
|
65
|
+
@recorder = Trace::Recorder.new(resource_attributes: resource,
|
|
66
|
+
traceparent: ENV.fetch('MERGIFY_TRACEPARENT', nil))
|
|
67
|
+
@uploads = uploadable?
|
|
68
|
+
# Only a pull request has a base branch, and that is what puts flaky
|
|
69
|
+
# detection in 'new' mode. GitHub Actions still sets GITHUB_BASE_REF on
|
|
70
|
+
# every other event, to an empty string, so empty counts as absent.
|
|
71
|
+
base_branch_name = resource['vcs.ref.base.name']
|
|
72
|
+
@base_branch_name = base_branch_name unless base_branch_name.to_s.empty?
|
|
73
|
+
@branch_name = @base_branch_name || resource['vcs.ref.head.name']
|
|
57
74
|
load_flaky_detector
|
|
58
75
|
load_quarantine
|
|
59
76
|
end
|
|
60
77
|
|
|
61
|
-
def build_processor
|
|
62
|
-
if debug_mode? || test_mode?
|
|
63
|
-
build_in_memory_processor
|
|
64
|
-
elsif @token && @repo_name
|
|
65
|
-
build_otlp_processor
|
|
66
|
-
else
|
|
67
|
-
[nil, nil]
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
78
|
def debug_mode?
|
|
72
79
|
ENV.key?('RSPEC_MERGIFY_DEBUG')
|
|
73
80
|
end
|
|
@@ -76,59 +83,23 @@ module Mergify
|
|
|
76
83
|
ENV['_RSPEC_MERGIFY_TEST'] == 'true'
|
|
77
84
|
end
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
# A run is uploaded when there is somewhere to upload it to and nothing
|
|
87
|
+
# asking us not to: debug and test runs record and keep.
|
|
88
|
+
def uploadable?
|
|
89
|
+
return false if debug_mode? || test_mode?
|
|
90
|
+
return false unless @token && @repo_name && Native.available?
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
owner, repo = Utils.split_full_repo_name(@repo_name)
|
|
87
|
-
endpoint = "#{@api_url}/v1/ci/#{owner}/repositories/#{repo}/traces"
|
|
88
|
-
exp = create_otlp_exporter(endpoint)
|
|
89
|
-
processor = SynchronousBatchSpanProcessor.new(exp)
|
|
90
|
-
[processor, exp]
|
|
92
|
+
true
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
# The cicd.* and vcs.* attributes come from the Rust core, which every
|
|
94
|
-
# Mergify test client shares, so a provider
|
|
95
|
-
# What stays here is what only Ruby knows: the test
|
|
96
|
-
# this run invented for itself.
|
|
96
|
+
# Mergify test client shares, so a CI provider added there reaches every
|
|
97
|
+
# client at once. What stays here is what only Ruby knows: the test
|
|
98
|
+
# framework and its language, and the id this run invented for itself.
|
|
97
99
|
def build_resource
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
OpenTelemetry::SDK::Resources::Resource.create('test.run.id' => @test_run_id)
|
|
102
|
-
]
|
|
103
|
-
resources.reduce(OpenTelemetry::SDK::Resources::Resource.create({})) do |merged, r|
|
|
104
|
-
merged.merge(r)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def extract_branch_name(resource)
|
|
109
|
-
attrs = resource.attribute_enumerator.to_h
|
|
110
|
-
@base_branch_name = attrs['vcs.ref.base.name']
|
|
111
|
-
@base_branch_name || attrs['vcs.ref.head.name']
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
115
|
-
def create_otlp_exporter(endpoint)
|
|
116
|
-
require 'opentelemetry-exporter-otlp'
|
|
117
|
-
original_env = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', nil)
|
|
118
|
-
ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = endpoint
|
|
119
|
-
begin
|
|
120
|
-
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
121
|
-
endpoint: endpoint,
|
|
122
|
-
headers: { 'Authorization' => "Bearer #{@token}" },
|
|
123
|
-
compression: 'gzip'
|
|
124
|
-
)
|
|
125
|
-
ensure
|
|
126
|
-
if original_env
|
|
127
|
-
ENV['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = original_env
|
|
128
|
-
else
|
|
129
|
-
ENV.delete('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT')
|
|
130
|
-
end
|
|
131
|
-
end
|
|
100
|
+
Native.detect_attributes
|
|
101
|
+
.merge(Resources::RSpec.detect)
|
|
102
|
+
.merge('test.run.id' => @test_run_id)
|
|
132
103
|
end
|
|
133
104
|
|
|
134
105
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -126,8 +126,7 @@ module Mergify
|
|
|
126
126
|
def set_test_deadline(test_id, timeout: nil)
|
|
127
127
|
return unless @metrics.key?(test_id)
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
per_test_budget = remaining_budget / remaining_tests
|
|
129
|
+
per_test_budget = next_test_share
|
|
131
130
|
|
|
132
131
|
allocated =
|
|
133
132
|
if timeout
|
|
@@ -198,17 +197,21 @@ module Mergify
|
|
|
198
197
|
context
|
|
199
198
|
end
|
|
200
199
|
|
|
201
|
-
def remaining_budget
|
|
202
|
-
used = budget_used
|
|
203
|
-
[@budget - used, 0.0].max
|
|
204
|
-
end
|
|
205
|
-
|
|
206
200
|
def budget_used
|
|
207
201
|
@metrics.sum { |_, m| m.total_duration }
|
|
208
202
|
end
|
|
209
203
|
|
|
210
|
-
|
|
211
|
-
|
|
204
|
+
# What is left of the budget, divided over the tests still waiting for a
|
|
205
|
+
# share of it. The engine works in milliseconds; RSpec durations are
|
|
206
|
+
# seconds.
|
|
207
|
+
def next_test_share
|
|
208
|
+
Native::Budget.dynamic_share_ms(
|
|
209
|
+
@budget * 1000, budget_used * 1000, @tests_to_process.size, processed_tests_count
|
|
210
|
+
) / 1000.0
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def processed_tests_count
|
|
214
|
+
@tests_to_process.count { |id| @metrics.key?(id) && !@metrics[id].deadline.nil? }
|
|
212
215
|
end
|
|
213
216
|
end
|
|
214
217
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rspec/core/formatters/base_formatter'
|
|
4
|
-
|
|
4
|
+
require_relative 'trace'
|
|
5
|
+
|
|
6
|
+
require 'mergify/rspec/native'
|
|
5
7
|
|
|
6
8
|
module Mergify
|
|
7
9
|
module RSpec
|
|
8
|
-
# RSpec formatter that
|
|
10
|
+
# RSpec formatter that records spans for Mergify Test Insights and
|
|
9
11
|
# prints a terminal report. It is purely observational and does not modify
|
|
10
12
|
# test execution.
|
|
11
13
|
# rubocop:disable-next Metrics/ClassLength
|
|
@@ -17,18 +19,14 @@ module Mergify
|
|
|
17
19
|
:example_pending,
|
|
18
20
|
:stop
|
|
19
21
|
|
|
20
|
-
# rubocop:disable-next Metrics/MethodLength
|
|
21
22
|
def start(notification)
|
|
22
23
|
super
|
|
23
24
|
|
|
24
25
|
@ci_insights = Mergify::RSpec.ci_insights
|
|
25
|
-
return unless @ci_insights&.
|
|
26
|
-
|
|
27
|
-
extract_distributed_trace_context
|
|
26
|
+
return unless @ci_insights&.recorder
|
|
28
27
|
|
|
29
|
-
@session_span = @ci_insights.
|
|
28
|
+
@session_span = @ci_insights.recorder.start_span(
|
|
30
29
|
'rspec session start',
|
|
31
|
-
with_parent: @parent_context,
|
|
32
30
|
attributes: { 'test.scope' => 'session' }
|
|
33
31
|
)
|
|
34
32
|
@has_error = false
|
|
@@ -36,15 +34,14 @@ module Mergify
|
|
|
36
34
|
end
|
|
37
35
|
|
|
38
36
|
def example_started(notification)
|
|
39
|
-
return unless @ci_insights&.
|
|
37
|
+
return unless @ci_insights&.recorder && @session_span
|
|
40
38
|
|
|
41
39
|
example = notification.example
|
|
42
|
-
parent_context = OpenTelemetry::Trace.context_with_span(@session_span)
|
|
43
40
|
quarantined = @ci_insights.mark_test_as_quarantined_if_needed(example.id)
|
|
44
41
|
|
|
45
|
-
span = @ci_insights.
|
|
42
|
+
span = @ci_insights.recorder.start_span(
|
|
46
43
|
example.id,
|
|
47
|
-
|
|
44
|
+
parent: @session_span,
|
|
48
45
|
attributes: build_example_attributes(example, quarantined)
|
|
49
46
|
)
|
|
50
47
|
@example_spans[example.id] = span
|
|
@@ -67,10 +64,10 @@ module Mergify
|
|
|
67
64
|
set_error_attributes(span, result.exception)
|
|
68
65
|
@has_error = true
|
|
69
66
|
else
|
|
70
|
-
span.
|
|
67
|
+
span.ok!
|
|
71
68
|
end
|
|
72
69
|
|
|
73
|
-
span
|
|
70
|
+
@ci_insights.recorder.record(span)
|
|
74
71
|
end
|
|
75
72
|
|
|
76
73
|
def example_pending(notification)
|
|
@@ -81,7 +78,7 @@ module Mergify
|
|
|
81
78
|
return unless span
|
|
82
79
|
|
|
83
80
|
span.set_attribute('test.case.result.status', 'skipped')
|
|
84
|
-
span
|
|
81
|
+
@ci_insights.recorder.record(span)
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
def stop(_notification)
|
|
@@ -92,14 +89,6 @@ module Mergify
|
|
|
92
89
|
|
|
93
90
|
private
|
|
94
91
|
|
|
95
|
-
def extract_distributed_trace_context
|
|
96
|
-
traceparent = ENV.fetch('MERGIFY_TRACEPARENT', nil)
|
|
97
|
-
@parent_context = if traceparent
|
|
98
|
-
propagator = OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator.new
|
|
99
|
-
propagator.extract({ 'traceparent' => traceparent })
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
92
|
def build_example_attributes(example, quarantined)
|
|
104
93
|
{
|
|
105
94
|
'test.scope' => 'case',
|
|
@@ -133,18 +122,18 @@ module Mergify
|
|
|
133
122
|
span.set_attribute('exception.type', exception.class.to_s)
|
|
134
123
|
span.set_attribute('exception.message', exception.message)
|
|
135
124
|
span.set_attribute('exception.stacktrace', exception.backtrace&.join("\n") || '')
|
|
136
|
-
span.
|
|
125
|
+
span.error!(exception.message)
|
|
137
126
|
end
|
|
138
127
|
|
|
139
128
|
def finish_session_span
|
|
140
129
|
return unless @session_span
|
|
141
130
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@session_span
|
|
131
|
+
if @has_error
|
|
132
|
+
@session_span.error!('One or more tests failed')
|
|
133
|
+
else
|
|
134
|
+
@session_span.ok!
|
|
135
|
+
end
|
|
136
|
+
@ci_insights.recorder.record(@session_span)
|
|
148
137
|
end
|
|
149
138
|
|
|
150
139
|
# rubocop:disable-next Metrics/MethodLength
|
|
@@ -167,12 +156,21 @@ module Mergify
|
|
|
167
156
|
def print_configuration_warnings
|
|
168
157
|
output.puts 'WARNING: MERGIFY_TOKEN is not set. Traces will not be sent to Mergify.' unless @ci_insights.token
|
|
169
158
|
|
|
159
|
+
return print_native_warning unless Mergify::RSpec::Native.available?
|
|
170
160
|
return if @ci_insights.repo_name
|
|
171
161
|
|
|
172
162
|
output.puts 'WARNING: Could not detect repository name. ' \
|
|
173
163
|
'Please set GITHUB_REPOSITORY or configure a git remote.'
|
|
174
164
|
end
|
|
175
165
|
|
|
166
|
+
# The extension is what detects CI, so when it did not load there is no
|
|
167
|
+
# repository name either, and blaming that would send people hunting for a
|
|
168
|
+
# git remote that is fine. Say what actually happened instead.
|
|
169
|
+
def print_native_warning
|
|
170
|
+
output.puts 'WARNING: the Mergify native extension could not be loaded, so this run reports nothing: ' \
|
|
171
|
+
"#{Mergify::RSpec::Native.load_error&.message}"
|
|
172
|
+
end
|
|
173
|
+
|
|
176
174
|
def print_flaky_report
|
|
177
175
|
return unless @ci_insights.flaky_detector.respond_to?(:make_report)
|
|
178
176
|
|
|
@@ -187,20 +185,13 @@ module Mergify
|
|
|
187
185
|
output.puts report if report
|
|
188
186
|
end
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
# One upload, at the end. There is nothing to shut down any more: the
|
|
189
|
+
# recorder holds spans in memory and the client owns the connection.
|
|
190
|
+
def flush_and_shutdown
|
|
191
|
+
return unless @ci_insights&.recorder
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
rescue StandardError => e
|
|
196
|
-
print_export_error(e)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
begin
|
|
200
|
-
@ci_insights.tracer_provider.shutdown
|
|
201
|
-
rescue StandardError => e
|
|
202
|
-
output.puts "Error while shutting down the tracer: #{e.message}"
|
|
203
|
-
end
|
|
193
|
+
error = @ci_insights.flush
|
|
194
|
+
print_export_error(StandardError.new(error)) if error
|
|
204
195
|
end
|
|
205
196
|
|
|
206
197
|
def print_export_error(error)
|
data/lib/mergify/rspec/native.rb
CHANGED
|
@@ -27,21 +27,42 @@ module Mergify
|
|
|
27
27
|
def available?
|
|
28
28
|
load_error.nil?
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
# Which of the two failed requires actually explains the absence.
|
|
32
|
+
#
|
|
33
|
+
# A precompiled gem ships lib/mergify/rspec/<ruby>/mergify_ci.<dlext>,
|
|
34
|
+
# so when one is packed for this Ruby that require is the real attempt:
|
|
35
|
+
# it fails with something like "version `GLIBC_2.29' not found", and the
|
|
36
|
+
# fallback that follows only ever adds "cannot load such file", which
|
|
37
|
+
# sends the reader looking for the wrong thing. With no extension for
|
|
38
|
+
# this Ruby -- the source gem, or a platform we publish no gem for --
|
|
39
|
+
# it is the other way round.
|
|
40
|
+
# Require a file next to this one, returning the LoadError instead of
|
|
41
|
+
# raising it. Two attempts read better as values than as nested rescues.
|
|
42
|
+
def attempt_require(path)
|
|
43
|
+
require_relative path
|
|
44
|
+
nil
|
|
45
|
+
rescue LoadError => e
|
|
46
|
+
e
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_failure_reason(versioned, fallback)
|
|
50
|
+
packed = Dir.glob(File.join(__dir__, RUBY_VERSION.to_f.to_s, 'mergify_ci.*')).any?
|
|
51
|
+
packed ? versioned : fallback
|
|
52
|
+
end
|
|
30
53
|
end
|
|
31
54
|
end
|
|
32
55
|
end
|
|
33
56
|
end
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Mergify::RSpec::Native.load_error = e
|
|
44
|
-
end
|
|
58
|
+
# Precompiled gems ship lib/mergify/rspec/<ruby>/mergify_ci.<dlext>; the source
|
|
59
|
+
# gem and any local `rake compile` put one beside this file instead.
|
|
60
|
+
versioned_error = Mergify::RSpec::Native.attempt_require("#{RUBY_VERSION.to_f}/mergify_ci")
|
|
61
|
+
fallback_error = versioned_error && Mergify::RSpec::Native.attempt_require('mergify_ci')
|
|
62
|
+
|
|
63
|
+
if fallback_error
|
|
64
|
+
Mergify::RSpec::Native.load_error =
|
|
65
|
+
Mergify::RSpec::Native.load_failure_reason(versioned_error, fallback_error)
|
|
45
66
|
end
|
|
46
67
|
|
|
47
68
|
unless Mergify::RSpec::Native.available?
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
3
|
require 'rspec/core/version'
|
|
5
4
|
|
|
6
5
|
module Mergify
|
|
7
6
|
module RSpec
|
|
8
7
|
module Resources
|
|
9
|
-
#
|
|
8
|
+
# The resource attributes only Ruby knows: which framework ran the suite,
|
|
9
|
+
# and in which language.
|
|
10
10
|
module RSpec
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
13
|
def detect
|
|
14
|
-
|
|
14
|
+
{
|
|
15
15
|
'test.framework' => 'rspec',
|
|
16
|
-
'test.framework.version' => ::RSpec::Core::Version::STRING
|
|
17
|
-
|
|
16
|
+
'test.framework.version' => ::RSpec::Core::Version::STRING,
|
|
17
|
+
# Mergify takes a test's language from here when the span does not
|
|
18
|
+
# name one. The OpenTelemetry SDK sets this on its default resource,
|
|
19
|
+
# but this gem never used that resource, so nothing had ever sent it.
|
|
20
|
+
'telemetry.sdk.language' => 'ruby'
|
|
21
|
+
}
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Mergify
|
|
6
|
+
module RSpec
|
|
7
|
+
# The little of OpenTelemetry this plugin actually used.
|
|
8
|
+
#
|
|
9
|
+
# A test run needs identifiers, a start and an end, some attributes and a
|
|
10
|
+
# status -- and the shared Rust client does the rest: the wire format, the
|
|
11
|
+
# compression, the retries, the size limit. The SDK brought samplers,
|
|
12
|
+
# context propagation machinery, batch processors and an exporter registry
|
|
13
|
+
# to do the part that fits here, and its dependency tree landed in every
|
|
14
|
+
# consumer's bundle. So the spans are assembled directly.
|
|
15
|
+
module Trace
|
|
16
|
+
# W3C traceparent: version-traceid-spanid-flags, all lowercase hex.
|
|
17
|
+
TRACEPARENT = /\A00-(?<trace_id>[0-9a-f]{32})-(?<span_id>[0-9a-f]{16})-[0-9a-f]{2}\z/
|
|
18
|
+
|
|
19
|
+
# A span being recorded, and once finished, the record itself.
|
|
20
|
+
class Span
|
|
21
|
+
attr_reader :name, :trace_id, :span_id, :parent_span_id, :attributes
|
|
22
|
+
attr_accessor :status, :status_message
|
|
23
|
+
|
|
24
|
+
def initialize(name:, trace_id:, span_id:, parent_span_id: nil, attributes: {})
|
|
25
|
+
@name = name
|
|
26
|
+
@trace_id = trace_id
|
|
27
|
+
@span_id = span_id
|
|
28
|
+
@parent_span_id = parent_span_id
|
|
29
|
+
@attributes = attributes.transform_keys(&:to_s)
|
|
30
|
+
@status = 'unset'
|
|
31
|
+
@status_message = nil
|
|
32
|
+
@start_unix_nano = Trace.now_unix_nano
|
|
33
|
+
@end_unix_nano = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Ids travel as bytes and are read as hex -- in a traceparent, in a
|
|
37
|
+
# backend URL, in a log line.
|
|
38
|
+
def hex_trace_id
|
|
39
|
+
@trace_id.unpack1('H*')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hex_span_id
|
|
43
|
+
@span_id.unpack1('H*')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def set_attribute(key, value)
|
|
47
|
+
@attributes[key.to_s] = value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def error!(message)
|
|
51
|
+
@status = 'error'
|
|
52
|
+
@status_message = message
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ok!
|
|
56
|
+
@status = 'ok'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def finish
|
|
60
|
+
@end_unix_nano ||= Trace.now_unix_nano
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The shape the binding accepts, which is the wire format's own.
|
|
65
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
66
|
+
def to_h
|
|
67
|
+
{
|
|
68
|
+
'name' => @name,
|
|
69
|
+
'trace_id' => @trace_id,
|
|
70
|
+
'span_id' => @span_id,
|
|
71
|
+
'parent_span_id' => @parent_span_id,
|
|
72
|
+
'start_unix_nano' => @start_unix_nano,
|
|
73
|
+
'end_unix_nano' => @end_unix_nano || Trace.now_unix_nano,
|
|
74
|
+
'attributes' => @attributes,
|
|
75
|
+
'status' => @status,
|
|
76
|
+
'status_message' => @status_message
|
|
77
|
+
}.compact
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Collects a run's spans and hands them to the client in one upload.
|
|
82
|
+
#
|
|
83
|
+
# Deliberately not streaming: a suite's spans are worth one request at the
|
|
84
|
+
# end, and exporting mid-run would put HTTP in the middle of the thing
|
|
85
|
+
# being timed. That was already why the gem replaced the SDK's batch
|
|
86
|
+
# processor with its own.
|
|
87
|
+
class Recorder
|
|
88
|
+
attr_reader :resource_attributes, :finished_spans, :trace_id
|
|
89
|
+
|
|
90
|
+
# `traceparent` is the W3C header a caller can hand down to put this
|
|
91
|
+
# run inside a trace it already started; the session span then hangs off
|
|
92
|
+
# that caller's span rather than starting a trace of its own.
|
|
93
|
+
def initialize(resource_attributes: {}, traceparent: nil)
|
|
94
|
+
@resource_attributes = resource_attributes.transform_keys(&:to_s)
|
|
95
|
+
inherited = Trace.parse_traceparent(traceparent)
|
|
96
|
+
@trace_id = inherited ? inherited.first : Trace.generate_trace_id
|
|
97
|
+
@root_parent_span_id = inherited&.last
|
|
98
|
+
@finished_spans = []
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def start_span(name, parent: nil, attributes: {})
|
|
102
|
+
Span.new(
|
|
103
|
+
name: name,
|
|
104
|
+
trace_id: @trace_id,
|
|
105
|
+
span_id: Trace.generate_span_id,
|
|
106
|
+
parent_span_id: parent&.span_id || @root_parent_span_id,
|
|
107
|
+
attributes: attributes
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def record(span)
|
|
112
|
+
@finished_spans << span.finish
|
|
113
|
+
span
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def clear
|
|
117
|
+
@finished_spans = []
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class << self
|
|
122
|
+
def now_unix_nano
|
|
123
|
+
(Time.now.to_r * 1_000_000_000).to_i
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def generate_trace_id
|
|
127
|
+
SecureRandom.bytes(16)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def generate_span_id
|
|
131
|
+
SecureRandom.bytes(8)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The trace this run belongs to, when a parent handed one down.
|
|
135
|
+
# Returns [trace_id, parent_span_id] as raw bytes, or nil.
|
|
136
|
+
def parse_traceparent(header)
|
|
137
|
+
match = TRACEPARENT.match(header.to_s)
|
|
138
|
+
return nil unless match
|
|
139
|
+
|
|
140
|
+
[[match[:trace_id]].pack('H*'), [match[:span_id]].pack('H*')]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
metadata
CHANGED
|
@@ -1,43 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-mergify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Mergify
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: opentelemetry-exporter-otlp
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.29'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0.29'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: opentelemetry-sdk
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - "~>"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '1.4'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '1.4'
|
|
41
13
|
- !ruby/object:Gem::Dependency
|
|
42
14
|
name: rspec-core
|
|
43
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,7 +47,7 @@ files:
|
|
|
75
47
|
- lib/mergify/rspec/native.rb
|
|
76
48
|
- lib/mergify/rspec/quarantine.rb
|
|
77
49
|
- lib/mergify/rspec/resources/rspec.rb
|
|
78
|
-
- lib/mergify/rspec/
|
|
50
|
+
- lib/mergify/rspec/trace.rb
|
|
79
51
|
- lib/mergify/rspec/utils.rb
|
|
80
52
|
- lib/mergify/rspec/version.rb
|
|
81
53
|
- lib/rspec_mergify.rb
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'opentelemetry-sdk'
|
|
4
|
-
|
|
5
|
-
module Mergify
|
|
6
|
-
module RSpec
|
|
7
|
-
class ExportError < StandardError; end
|
|
8
|
-
|
|
9
|
-
# A span processor that queues spans in memory and exports them all in one
|
|
10
|
-
# batch when force_flush is called. This avoids HTTP requests during test
|
|
11
|
-
# execution.
|
|
12
|
-
class SynchronousBatchSpanProcessor < OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor
|
|
13
|
-
def initialize(exporter)
|
|
14
|
-
super
|
|
15
|
-
@queue = []
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def on_finish(span)
|
|
19
|
-
return unless span.context.trace_flags.sampled?
|
|
20
|
-
|
|
21
|
-
@queue << span.to_span_data
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def force_flush(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
25
|
-
spans = @queue.dup
|
|
26
|
-
@queue.clear
|
|
27
|
-
result = @span_exporter.export(spans)
|
|
28
|
-
raise ExportError, 'Failed to export traces' unless result == OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
29
|
-
|
|
30
|
-
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|