airbrake-ruby 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/airbrake-ruby.rb +197 -43
- data/lib/airbrake-ruby/config.rb +43 -11
- data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
- data/lib/airbrake-ruby/filter_chain.rb +32 -50
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +9 -1
- data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
- data/lib/airbrake-ruby/hash_keyable.rb +37 -0
- data/lib/airbrake-ruby/ignorable.rb +44 -0
- data/lib/airbrake-ruby/notice.rb +2 -22
- data/lib/airbrake-ruby/{notifier.rb → notice_notifier.rb} +66 -46
- data/lib/airbrake-ruby/performance_notifier.rb +161 -0
- data/lib/airbrake-ruby/stat.rb +56 -0
- data/lib/airbrake-ruby/tdigest.rb +393 -0
- data/lib/airbrake-ruby/time_truncate.rb +17 -0
- data/lib/airbrake-ruby/version.rb +1 -1
- data/spec/airbrake_spec.rb +57 -13
- data/spec/async_sender_spec.rb +0 -2
- data/spec/backtrace_spec.rb +0 -2
- data/spec/code_hunk_spec.rb +0 -2
- data/spec/config/validator_spec.rb +0 -2
- data/spec/config_spec.rb +16 -4
- data/spec/deploy_notifier_spec.rb +41 -0
- data/spec/file_cache.rb +0 -2
- data/spec/filter_chain_spec.rb +1 -7
- data/spec/filters/context_filter_spec.rb +0 -2
- data/spec/filters/dependency_filter_spec.rb +0 -2
- data/spec/filters/exception_attributes_filter_spec.rb +0 -2
- data/spec/filters/gem_root_filter_spec.rb +0 -2
- data/spec/filters/git_last_checkout_filter_spec.rb +0 -2
- data/spec/filters/git_repository_filter.rb +0 -2
- data/spec/filters/git_revision_filter_spec.rb +0 -2
- data/spec/filters/keys_blacklist_spec.rb +0 -2
- data/spec/filters/keys_whitelist_spec.rb +0 -2
- data/spec/filters/root_directory_filter_spec.rb +0 -2
- data/spec/filters/sql_filter_spec.rb +219 -0
- data/spec/filters/system_exit_filter_spec.rb +0 -2
- data/spec/filters/thread_filter_spec.rb +0 -2
- data/spec/ignorable_spec.rb +14 -0
- data/spec/nested_exception_spec.rb +0 -2
- data/spec/{notifier_spec.rb → notice_notifier_spec.rb} +24 -114
- data/spec/{notifier_spec → notice_notifier_spec}/options_spec.rb +40 -39
- data/spec/notice_spec.rb +2 -4
- data/spec/performance_notifier_spec.rb +287 -0
- data/spec/promise_spec.rb +0 -2
- data/spec/response_spec.rb +0 -2
- data/spec/stat_spec.rb +35 -0
- data/spec/sync_sender_spec.rb +0 -2
- data/spec/tdigest_spec.rb +230 -0
- data/spec/time_truncate_spec.rb +13 -0
- data/spec/truncator_spec.rb +0 -2
- metadata +34 -15
- data/lib/airbrake-ruby/route_sender.rb +0 -175
- data/spec/route_sender_spec.rb +0 -130
data/spec/notice_spec.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
1
|
RSpec.describe Airbrake::Notice do
|
4
2
|
let(:notice) do
|
5
3
|
described_class.new(Airbrake::Config.new, AirbrakeTestError.new, bingo: '1')
|
@@ -261,7 +259,7 @@ RSpec.describe Airbrake::Notice do
|
|
261
259
|
it "raises error if notice is ignored" do
|
262
260
|
notice.ignore!
|
263
261
|
expect { notice[:params] }.
|
264
|
-
to raise_error(Airbrake::Error, 'cannot access ignored
|
262
|
+
to raise_error(Airbrake::Error, 'cannot access ignored Airbrake::Notice')
|
265
263
|
end
|
266
264
|
end
|
267
265
|
|
@@ -275,7 +273,7 @@ RSpec.describe Airbrake::Notice do
|
|
275
273
|
it "raises error if notice is ignored" do
|
276
274
|
notice.ignore!
|
277
275
|
expect { notice[:params] = {} }.
|
278
|
-
to raise_error(Airbrake::Error, 'cannot access ignored
|
276
|
+
to raise_error(Airbrake::Error, 'cannot access ignored Airbrake::Notice')
|
279
277
|
end
|
280
278
|
|
281
279
|
it "raises error when trying to assign unrecognized key" do
|
@@ -0,0 +1,287 @@
|
|
1
|
+
RSpec.describe Airbrake::PerformanceNotifier do
|
2
|
+
let(:routes) { 'https://api.airbrake.io/api/v5/projects/1/routes-stats' }
|
3
|
+
let(:queries) { 'https://api.airbrake.io/api/v5/projects/1/queries-stats' }
|
4
|
+
|
5
|
+
let(:config) do
|
6
|
+
Airbrake::Config.new(
|
7
|
+
project_id: 1,
|
8
|
+
project_key: 'banana',
|
9
|
+
performance_stats: true,
|
10
|
+
performance_stats_flush_period: 0
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
subject { described_class.new(config) }
|
15
|
+
|
16
|
+
before do
|
17
|
+
stub_request(:put, routes).to_return(status: 200, body: '')
|
18
|
+
stub_request(:put, queries).to_return(status: 200, body: '')
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#notify" do
|
22
|
+
it "rounds time to the floor minute" do
|
23
|
+
subject.notify(
|
24
|
+
Airbrake::Request.new(
|
25
|
+
method: 'GET',
|
26
|
+
route: '/foo',
|
27
|
+
status_code: 200,
|
28
|
+
start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
|
29
|
+
)
|
30
|
+
)
|
31
|
+
expect(
|
32
|
+
a_request(:put, routes).with(body: /"time":"2018-01-01T00:00:00\+00:00"/)
|
33
|
+
).to have_been_made
|
34
|
+
end
|
35
|
+
|
36
|
+
it "increments routes with the same key" do
|
37
|
+
subject.notify(
|
38
|
+
Airbrake::Request.new(
|
39
|
+
method: 'GET',
|
40
|
+
route: '/foo',
|
41
|
+
status_code: 200,
|
42
|
+
start_time: Time.new(2018, 1, 1, 0, 0, 20, 0)
|
43
|
+
)
|
44
|
+
)
|
45
|
+
subject.notify(
|
46
|
+
Airbrake::Request.new(
|
47
|
+
method: 'GET',
|
48
|
+
route: '/foo',
|
49
|
+
status_code: 200,
|
50
|
+
start_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
|
51
|
+
)
|
52
|
+
)
|
53
|
+
expect(
|
54
|
+
a_request(:put, routes).with(body: /"count":2/)
|
55
|
+
).to have_been_made
|
56
|
+
end
|
57
|
+
|
58
|
+
it "groups routes by time" do
|
59
|
+
subject.notify(
|
60
|
+
Airbrake::Request.new(
|
61
|
+
method: 'GET',
|
62
|
+
route: '/foo',
|
63
|
+
status_code: 200,
|
64
|
+
start_time: Time.new(2018, 1, 1, 0, 0, 49, 0),
|
65
|
+
end_time: Time.new(2018, 1, 1, 0, 0, 50, 0)
|
66
|
+
)
|
67
|
+
)
|
68
|
+
subject.notify(
|
69
|
+
Airbrake::Request.new(
|
70
|
+
method: 'GET',
|
71
|
+
route: '/foo',
|
72
|
+
status_code: 200,
|
73
|
+
start_time: Time.new(2018, 1, 1, 0, 1, 49, 0),
|
74
|
+
end_time: Time.new(2018, 1, 1, 0, 1, 55, 0)
|
75
|
+
)
|
76
|
+
)
|
77
|
+
expect(
|
78
|
+
a_request(:put, routes).with(
|
79
|
+
body: %r|\A
|
80
|
+
{"routes":\[
|
81
|
+
{"method":"GET","route":"/foo","statusCode":200,
|
82
|
+
"time":"2018-01-01T00:00:00\+00:00","count":1,"sum":1000.0,
|
83
|
+
"sumsq":1000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUR6AAAB"},
|
84
|
+
{"method":"GET","route":"/foo","statusCode":200,
|
85
|
+
"time":"2018-01-01T00:01:00\+00:00","count":1,"sum":6000.0,
|
86
|
+
"sumsq":36000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUW7gAAB"}\]}
|
87
|
+
\z|x
|
88
|
+
)
|
89
|
+
).to have_been_made
|
90
|
+
end
|
91
|
+
|
92
|
+
it "groups routes by route key" do
|
93
|
+
subject.notify(
|
94
|
+
Airbrake::Request.new(
|
95
|
+
method: 'GET',
|
96
|
+
route: '/foo',
|
97
|
+
status_code: 200,
|
98
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
|
99
|
+
end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
|
100
|
+
)
|
101
|
+
)
|
102
|
+
subject.notify(
|
103
|
+
Airbrake::Request.new(
|
104
|
+
method: 'POST',
|
105
|
+
route: '/foo',
|
106
|
+
status_code: 200,
|
107
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0),
|
108
|
+
end_time: Time.new(2018, 1, 1, 0, 50, 0, 0)
|
109
|
+
)
|
110
|
+
)
|
111
|
+
expect(
|
112
|
+
a_request(:put, routes).with(
|
113
|
+
body: %r|\A
|
114
|
+
{"routes":\[
|
115
|
+
{"method":"GET","route":"/foo","statusCode":200,
|
116
|
+
"time":"2018-01-01T00:49:00\+00:00","count":1,"sum":60000.0,
|
117
|
+
"sumsq":3600000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"},
|
118
|
+
{"method":"POST","route":"/foo","statusCode":200,
|
119
|
+
"time":"2018-01-01T00:49:00\+00:00","count":1,"sum":60000.0,
|
120
|
+
"sumsq":3600000000.0,"tdigest":"AAAAAkA0AAAAAAAAAAAAAUdqYAAB"}\]}
|
121
|
+
\z|x
|
122
|
+
)
|
123
|
+
).to have_been_made
|
124
|
+
end
|
125
|
+
|
126
|
+
it "returns a promise" do
|
127
|
+
promise = subject.notify(
|
128
|
+
Airbrake::Request.new(
|
129
|
+
method: 'GET',
|
130
|
+
route: '/foo',
|
131
|
+
status_code: 200,
|
132
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
133
|
+
)
|
134
|
+
)
|
135
|
+
expect(promise).to be_an(Airbrake::Promise)
|
136
|
+
expect(promise.value).to eq('' => nil)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "doesn't send route stats when performance stats are disabled" do
|
140
|
+
notifier = described_class.new(
|
141
|
+
Airbrake::Config.new(
|
142
|
+
project_id: 1, project_key: '2', performance_stats: false
|
143
|
+
)
|
144
|
+
)
|
145
|
+
promise = notifier.notify(
|
146
|
+
Airbrake::Request.new(
|
147
|
+
method: 'GET', route: '/foo', status_code: 200, start_time: Time.new
|
148
|
+
)
|
149
|
+
)
|
150
|
+
expect(a_request(:put, routes)).not_to have_been_made
|
151
|
+
expect(promise.value).to eq(
|
152
|
+
'error' => "The Performance Stats feature is disabled"
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "doesn't send route stats when current environment is ignored" do
|
157
|
+
notifier = described_class.new(
|
158
|
+
Airbrake::Config.new(
|
159
|
+
project_id: 1, project_key: '2', performance_stats: true,
|
160
|
+
environment: 'test', ignore_environments: %w[test]
|
161
|
+
)
|
162
|
+
)
|
163
|
+
promise = notifier.notify(
|
164
|
+
Airbrake::Request.new(
|
165
|
+
method: 'GET', route: '/foo', status_code: 200, start_time: Time.new
|
166
|
+
)
|
167
|
+
)
|
168
|
+
expect(a_request(:put, routes)).not_to have_been_made
|
169
|
+
expect(promise.value).to eq('error' => "The 'test' environment is ignored")
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "payload grouping" do
|
173
|
+
let(:flush_period) { 0.5 }
|
174
|
+
|
175
|
+
let(:config) do
|
176
|
+
Airbrake::Config.new(
|
177
|
+
project_id: 1,
|
178
|
+
project_key: 'banana',
|
179
|
+
performance_stats: true,
|
180
|
+
performance_stats_flush_period: flush_period
|
181
|
+
)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "groups payload by performance name and sends it separately" do
|
185
|
+
subject.notify(
|
186
|
+
Airbrake::Request.new(
|
187
|
+
method: 'GET',
|
188
|
+
route: '/foo',
|
189
|
+
status_code: 200,
|
190
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
191
|
+
)
|
192
|
+
)
|
193
|
+
|
194
|
+
subject.notify(
|
195
|
+
Airbrake::Query.new(
|
196
|
+
method: 'POST',
|
197
|
+
route: '/foo',
|
198
|
+
query: 'SELECT * FROM things',
|
199
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
200
|
+
)
|
201
|
+
)
|
202
|
+
|
203
|
+
sleep(flush_period + 0.5)
|
204
|
+
|
205
|
+
expect(a_request(:put, routes)).to have_been_made
|
206
|
+
expect(a_request(:put, queries)).to have_been_made
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context "when an ignore filter was defined" do
|
211
|
+
before { subject.add_filter(&:ignore!) }
|
212
|
+
|
213
|
+
it "doesn't notify airbrake of requests" do
|
214
|
+
subject.notify(
|
215
|
+
Airbrake::Request.new(
|
216
|
+
method: 'GET',
|
217
|
+
route: '/foo',
|
218
|
+
status_code: 200,
|
219
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
220
|
+
)
|
221
|
+
)
|
222
|
+
expect(a_request(:put, routes)).not_to have_been_made
|
223
|
+
end
|
224
|
+
|
225
|
+
it "doesn't notify airbrake of queries" do
|
226
|
+
subject.notify(
|
227
|
+
Airbrake::Query.new(
|
228
|
+
method: 'POST',
|
229
|
+
route: '/foo',
|
230
|
+
query: 'SELECT * FROM things',
|
231
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
232
|
+
)
|
233
|
+
)
|
234
|
+
expect(a_request(:put, queries)).not_to have_been_made
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
context "when a filter that modifies payload was defined" do
|
239
|
+
before do
|
240
|
+
subject.add_filter do |resource|
|
241
|
+
resource.route = '[Filtered]'
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
it "notifies airbrake with modified payload" do
|
246
|
+
subject.notify(
|
247
|
+
Airbrake::Query.new(
|
248
|
+
method: 'POST',
|
249
|
+
route: '/foo',
|
250
|
+
query: 'SELECT * FROM things',
|
251
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
252
|
+
)
|
253
|
+
)
|
254
|
+
expect(
|
255
|
+
a_request(:put, queries).with(
|
256
|
+
body: /\A{"queries":\[{"method":"POST","route":"\[Filtered\]"/
|
257
|
+
)
|
258
|
+
).to have_been_made
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe "#delete_filter" do
|
264
|
+
let(:filter) do
|
265
|
+
Class.new do
|
266
|
+
def call(resource)
|
267
|
+
resource.ignore!
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
before { subject.add_filter(filter.new) }
|
273
|
+
|
274
|
+
it "deletes a filter" do
|
275
|
+
subject.delete_filter(filter)
|
276
|
+
subject.notify(
|
277
|
+
Airbrake::Request.new(
|
278
|
+
method: 'POST',
|
279
|
+
route: '/foo',
|
280
|
+
status_code: 200,
|
281
|
+
start_time: Time.new(2018, 1, 1, 0, 49, 0, 0)
|
282
|
+
)
|
283
|
+
)
|
284
|
+
expect(a_request(:put, routes)).to have_been_made
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
data/spec/promise_spec.rb
CHANGED
data/spec/response_spec.rb
CHANGED
data/spec/stat_spec.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
RSpec.describe Airbrake::Stat do
|
2
|
+
describe "#to_h" do
|
3
|
+
it "converts to a hash" do
|
4
|
+
expect(subject.to_h).to eq(
|
5
|
+
'count' => 0,
|
6
|
+
'sum' => 0.0,
|
7
|
+
'sumsq' => 0.0,
|
8
|
+
'tdigest' => 'AAAAAkA0AAAAAAAAAAAAAA=='
|
9
|
+
)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#increment" do
|
14
|
+
let(:start_time) { Time.new(2018, 1, 1, 0, 0, 20, 0) }
|
15
|
+
let(:end_time) { Time.new(2018, 1, 1, 0, 0, 21, 0) }
|
16
|
+
|
17
|
+
before { subject.increment(start_time, end_time) }
|
18
|
+
|
19
|
+
it "increments count" do
|
20
|
+
expect(subject.count).to eq(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "updates sum" do
|
24
|
+
expect(subject.sum).to eq(1000)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "updates sumsq" do
|
28
|
+
expect(subject.sumsq).to eq(1000000)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "updates tdigest" do
|
32
|
+
expect(subject.tdigest.size).to eq(1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/spec/sync_sender_spec.rb
CHANGED
@@ -0,0 +1,230 @@
|
|
1
|
+
RSpec.describe Airbrake::TDigest do
|
2
|
+
describe "byte serialization" do
|
3
|
+
it "loads serialized data" do
|
4
|
+
subject.push(60, 100)
|
5
|
+
10.times { subject.push(rand * 100) }
|
6
|
+
bytes = subject.as_bytes
|
7
|
+
new_tdigest = described_class.from_bytes(bytes)
|
8
|
+
expect(new_tdigest.percentile(0.9)).to eq(subject.percentile(0.9))
|
9
|
+
expect(new_tdigest.as_bytes).to eq(bytes)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "handles zero size" do
|
13
|
+
bytes = subject.as_bytes
|
14
|
+
expect(described_class.from_bytes(bytes).size).to be_zero
|
15
|
+
end
|
16
|
+
|
17
|
+
it "preserves compression" do
|
18
|
+
td = described_class.new(0.001)
|
19
|
+
bytes = td.as_bytes
|
20
|
+
new_tdigest = described_class.from_bytes(bytes)
|
21
|
+
expect(new_tdigest.compression).to eq(td.compression)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "small byte serialization" do
|
26
|
+
it "loads serialized data" do
|
27
|
+
10.times { subject.push(10) }
|
28
|
+
bytes = subject.as_small_bytes
|
29
|
+
new_tdigest = described_class.from_bytes(bytes)
|
30
|
+
# Expect some rounding error due to compression
|
31
|
+
expect(new_tdigest.percentile(0.9).round(5)).to eq(
|
32
|
+
subject.percentile(0.9).round(5)
|
33
|
+
)
|
34
|
+
expect(new_tdigest.as_small_bytes).to eq(bytes)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "handles zero size" do
|
38
|
+
bytes = subject.as_small_bytes
|
39
|
+
expect(described_class.from_bytes(bytes).size).to be_zero
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "JSON serialization" do
|
44
|
+
it "loads serialized data" do
|
45
|
+
subject.push(60, 100)
|
46
|
+
json = subject.as_json
|
47
|
+
new_tdigest = described_class.from_json(json)
|
48
|
+
expect(new_tdigest.percentile(0.9)).to eq(subject.percentile(0.9))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#percentile" do
|
53
|
+
it "returns nil if empty" do
|
54
|
+
expect(subject.percentile(0.90)).to be_nil # This should not crash
|
55
|
+
end
|
56
|
+
|
57
|
+
it "raises ArgumentError of input not between 0 and 1" do
|
58
|
+
expect { subject.percentile(1.1) }.to raise_error(ArgumentError)
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "with only single value" do
|
62
|
+
it "returns the value" do
|
63
|
+
subject.push(60, 100)
|
64
|
+
expect(subject.percentile(0.90)).to eq(60)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "returns 0 for all percentiles when only 0 present" do
|
68
|
+
subject.push(0)
|
69
|
+
expect(subject.percentile([0.0, 0.5, 1.0])).to eq([0, 0, 0])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "with alot of uniformly distributed points" do
|
74
|
+
it "has minimal error" do
|
75
|
+
seed = srand(1234) # Makes the values a proper fixture
|
76
|
+
N = 100_000
|
77
|
+
maxerr = 0
|
78
|
+
values = Array.new(N).map { rand }
|
79
|
+
srand(seed)
|
80
|
+
|
81
|
+
subject.push(values)
|
82
|
+
subject.compress!
|
83
|
+
|
84
|
+
0.step(1, 0.1).each do |i|
|
85
|
+
q = subject.percentile(i)
|
86
|
+
maxerr = [maxerr, (i - q).abs].max
|
87
|
+
end
|
88
|
+
|
89
|
+
expect(maxerr).to be < 0.01
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#push" do
|
95
|
+
it "calls _cumulate so won't crash because of uninitialized mean_cumn" do
|
96
|
+
subject.push(
|
97
|
+
[
|
98
|
+
125000000.0,
|
99
|
+
104166666.66666666,
|
100
|
+
135416666.66666666,
|
101
|
+
104166666.66666666,
|
102
|
+
104166666.66666666,
|
103
|
+
93750000.0,
|
104
|
+
125000000.0,
|
105
|
+
62500000.0,
|
106
|
+
114583333.33333333,
|
107
|
+
156250000.0,
|
108
|
+
124909090.90909092,
|
109
|
+
104090909.0909091,
|
110
|
+
135318181.81818184,
|
111
|
+
104090909.0909091,
|
112
|
+
104090909.0909091,
|
113
|
+
93681818.18181819,
|
114
|
+
124909090.90909092,
|
115
|
+
62454545.45454546,
|
116
|
+
114500000.00000001,
|
117
|
+
156136363.63636366,
|
118
|
+
123567567.56756756,
|
119
|
+
102972972.97297296,
|
120
|
+
133864864.86486486,
|
121
|
+
102972972.97297296,
|
122
|
+
102972972.97297296,
|
123
|
+
92675675.67567568,
|
124
|
+
123567567.56756756,
|
125
|
+
61783783.78378378,
|
126
|
+
113270270.27027026,
|
127
|
+
154459459.45945945,
|
128
|
+
123829787.23404256,
|
129
|
+
103191489.36170213
|
130
|
+
]
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "does not blow up if data comes in sorted" do
|
135
|
+
subject.push(0..10_000)
|
136
|
+
expect(subject.centroids.size).to be < 5_000
|
137
|
+
subject.compress!
|
138
|
+
expect(subject.centroids.size).to be < 1_000
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#size" do
|
143
|
+
it "reports the number of observations" do
|
144
|
+
n = 10_000
|
145
|
+
n.times { subject.push(rand) }
|
146
|
+
subject.compress!
|
147
|
+
expect(subject.size).to eq(n)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "#+" do
|
152
|
+
it "works with empty tdigests" do
|
153
|
+
other = described_class.new(0.001, 50, 1.2)
|
154
|
+
expect((subject + other).centroids.size).to eq(0)
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "adding two tdigests" do
|
158
|
+
before do
|
159
|
+
@other = described_class.new(0.001, 50, 1.2)
|
160
|
+
[subject, @other].each do |td|
|
161
|
+
td.push(60, 100)
|
162
|
+
10.times { td.push(rand * 100) }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it "has the parameters of the left argument (the calling tdigest)" do
|
167
|
+
new_tdigest = subject + @other
|
168
|
+
expect(new_tdigest.instance_variable_get(:@delta)).to eq(
|
169
|
+
subject.instance_variable_get(:@delta)
|
170
|
+
)
|
171
|
+
expect(new_tdigest.instance_variable_get(:@k)).to eq(
|
172
|
+
subject.instance_variable_get(:@k)
|
173
|
+
)
|
174
|
+
expect(new_tdigest.instance_variable_get(:@cx)).to eq(
|
175
|
+
subject.instance_variable_get(:@cx)
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "returns a tdigest with less than or equal centroids" do
|
180
|
+
new_tdigest = subject + @other
|
181
|
+
expect(new_tdigest.centroids.size).
|
182
|
+
to be <= subject.centroids.size + @other.centroids.size
|
183
|
+
end
|
184
|
+
|
185
|
+
it "has the size of the two digests combined" do
|
186
|
+
new_tdigest = subject + @other
|
187
|
+
expect(new_tdigest.size).to eq(subject.size + @other.size)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "#merge!" do
|
193
|
+
it "works with empty tdigests" do
|
194
|
+
other = described_class.new(0.001, 50, 1.2)
|
195
|
+
subject.merge!(other)
|
196
|
+
expect(subject.centroids.size).to be_zero
|
197
|
+
end
|
198
|
+
|
199
|
+
describe "with populated tdigests" do
|
200
|
+
before do
|
201
|
+
@other = described_class.new(0.001, 50, 1.2)
|
202
|
+
[subject, @other].each do |td|
|
203
|
+
td.push(60, 100)
|
204
|
+
10.times { td.push(rand * 100) }
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "has the parameters of the calling tdigest" do
|
209
|
+
vars = %i[@delta @k @cx]
|
210
|
+
expected = Hash[vars.map { |v| [v, subject.instance_variable_get(v)] }]
|
211
|
+
subject.merge!(@other)
|
212
|
+
vars.each do |v|
|
213
|
+
expect(subject.instance_variable_get(v)).to eq(expected[v])
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
it "returns a tdigest with less than or equal centroids" do
|
218
|
+
combined_size = subject.centroids.size + @other.centroids.size
|
219
|
+
subject.merge!(@other)
|
220
|
+
expect(subject.centroids.size).to be <= combined_size
|
221
|
+
end
|
222
|
+
|
223
|
+
it "has the size of the two digests combined" do
|
224
|
+
combined_size = subject.size + @other.size
|
225
|
+
subject.merge!(@other)
|
226
|
+
expect(subject.size).to eq(combined_size)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|