airbrake-ruby 3.2.2-java
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 +7 -0
- data/lib/airbrake-ruby.rb +554 -0
- data/lib/airbrake-ruby/async_sender.rb +119 -0
- data/lib/airbrake-ruby/backtrace.rb +194 -0
- data/lib/airbrake-ruby/code_hunk.rb +53 -0
- data/lib/airbrake-ruby/config.rb +238 -0
- data/lib/airbrake-ruby/config/validator.rb +63 -0
- data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
- data/lib/airbrake-ruby/file_cache.rb +48 -0
- data/lib/airbrake-ruby/filter_chain.rb +95 -0
- data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
- data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
- data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
- data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
- data/lib/airbrake-ruby/hash_keyable.rb +37 -0
- data/lib/airbrake-ruby/ignorable.rb +44 -0
- data/lib/airbrake-ruby/nested_exception.rb +39 -0
- data/lib/airbrake-ruby/notice.rb +165 -0
- data/lib/airbrake-ruby/notice_notifier.rb +228 -0
- data/lib/airbrake-ruby/performance_notifier.rb +161 -0
- data/lib/airbrake-ruby/promise.rb +99 -0
- data/lib/airbrake-ruby/response.rb +71 -0
- data/lib/airbrake-ruby/stat.rb +56 -0
- data/lib/airbrake-ruby/sync_sender.rb +111 -0
- data/lib/airbrake-ruby/tdigest.rb +393 -0
- data/lib/airbrake-ruby/time_truncate.rb +17 -0
- data/lib/airbrake-ruby/truncator.rb +115 -0
- data/lib/airbrake-ruby/version.rb +6 -0
- data/spec/airbrake_spec.rb +171 -0
- data/spec/async_sender_spec.rb +154 -0
- data/spec/backtrace_spec.rb +438 -0
- data/spec/code_hunk_spec.rb +118 -0
- data/spec/config/validator_spec.rb +189 -0
- data/spec/config_spec.rb +281 -0
- data/spec/deploy_notifier_spec.rb +41 -0
- data/spec/file_cache.rb +36 -0
- data/spec/filter_chain_spec.rb +83 -0
- data/spec/filters/context_filter_spec.rb +25 -0
- data/spec/filters/dependency_filter_spec.rb +14 -0
- data/spec/filters/exception_attributes_filter_spec.rb +63 -0
- data/spec/filters/gem_root_filter_spec.rb +44 -0
- data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
- data/spec/filters/git_repository_filter.rb +53 -0
- data/spec/filters/git_revision_filter_spec.rb +126 -0
- data/spec/filters/keys_blacklist_spec.rb +236 -0
- data/spec/filters/keys_whitelist_spec.rb +205 -0
- data/spec/filters/root_directory_filter_spec.rb +42 -0
- data/spec/filters/sql_filter_spec.rb +219 -0
- data/spec/filters/system_exit_filter_spec.rb +14 -0
- data/spec/filters/thread_filter_spec.rb +279 -0
- data/spec/fixtures/notroot.txt +7 -0
- data/spec/fixtures/project_root/code.rb +221 -0
- data/spec/fixtures/project_root/empty_file.rb +0 -0
- data/spec/fixtures/project_root/long_line.txt +1 -0
- data/spec/fixtures/project_root/short_file.rb +3 -0
- data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
- data/spec/helpers.rb +9 -0
- data/spec/ignorable_spec.rb +14 -0
- data/spec/nested_exception_spec.rb +75 -0
- data/spec/notice_notifier_spec.rb +436 -0
- data/spec/notice_notifier_spec/options_spec.rb +266 -0
- data/spec/notice_spec.rb +297 -0
- data/spec/performance_notifier_spec.rb +287 -0
- data/spec/promise_spec.rb +165 -0
- data/spec/response_spec.rb +82 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/stat_spec.rb +35 -0
- data/spec/sync_sender_spec.rb +140 -0
- data/spec/tdigest_spec.rb +230 -0
- data/spec/time_truncate_spec.rb +13 -0
- data/spec/truncator_spec.rb +238 -0
- metadata +278 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
RSpec.describe Airbrake::Response do
|
2
|
+
describe ".parse" do
|
3
|
+
let(:out) { StringIO.new }
|
4
|
+
let(:logger) { Logger.new(out) }
|
5
|
+
|
6
|
+
[200, 201, 204].each do |code|
|
7
|
+
context "when response code is #{code}" do
|
8
|
+
it "logs response body" do
|
9
|
+
described_class.parse(OpenStruct.new(code: code, body: '{}'), logger)
|
10
|
+
expect(out.string).to match(/Airbrake: Airbrake::Response \(#{code}\): {}/)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
[400, 401, 403, 420].each do |code|
|
16
|
+
context "when response code is #{code}" do
|
17
|
+
it "logs response message" do
|
18
|
+
described_class.parse(
|
19
|
+
OpenStruct.new(code: code, body: '{"message":"foo"}'), logger
|
20
|
+
)
|
21
|
+
expect(out.string).to match(/Airbrake: foo/)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when response code is 429" do
|
27
|
+
let(:response) { OpenStruct.new(code: 429, body: '{"message":"rate limited"}') }
|
28
|
+
it "logs response message" do
|
29
|
+
described_class.parse(response, logger)
|
30
|
+
expect(out.string).to match(/Airbrake: rate limited/)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns an error response" do
|
34
|
+
time = Time.now
|
35
|
+
allow(Time).to receive(:now).and_return(time)
|
36
|
+
|
37
|
+
resp = described_class.parse(response, logger)
|
38
|
+
expect(resp).to include(
|
39
|
+
'error' => '**Airbrake: rate limited',
|
40
|
+
'rate_limit_reset' => time
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when response code is unhandled" do
|
46
|
+
let(:response) { OpenStruct.new(code: 500, body: 'foo') }
|
47
|
+
|
48
|
+
it "logs response body" do
|
49
|
+
described_class.parse(response, logger)
|
50
|
+
expect(out.string).to match(/Airbrake: unexpected code \(500\)\. Body: foo/)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns an error response" do
|
54
|
+
resp = described_class.parse(response, logger)
|
55
|
+
expect(resp).to eq('error' => 'foo')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "truncates body" do
|
59
|
+
response.body *= 1000
|
60
|
+
resp = described_class.parse(response, logger)
|
61
|
+
expect(resp).to eq('error' => ('foo' * 33) + 'fo...')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "when response body can't be parsed as JSON" do
|
66
|
+
let(:response) { OpenStruct.new(code: 201, body: 'foo') }
|
67
|
+
|
68
|
+
it "logs response body" do
|
69
|
+
described_class.parse(response, logger)
|
70
|
+
expect(out.string).to match(
|
71
|
+
/Airbrake: error while parsing body \(.*unexpected token.*\)\. Body: foo/
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns an error message" do
|
76
|
+
expect(described_class.parse(response, logger)['error']).to match(
|
77
|
+
/\A#<JSON::ParserError.+>/
|
78
|
+
)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'airbrake-ruby'
|
2
|
+
|
3
|
+
require 'webmock'
|
4
|
+
require 'webmock/rspec'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
require 'pathname'
|
8
|
+
require 'webrick'
|
9
|
+
require 'English'
|
10
|
+
require 'base64'
|
11
|
+
require 'pp'
|
12
|
+
|
13
|
+
require 'helpers'
|
14
|
+
|
15
|
+
RSpec.configure do |c|
|
16
|
+
c.order = 'random'
|
17
|
+
c.color = true
|
18
|
+
c.disable_monkey_patching!
|
19
|
+
c.include Helpers
|
20
|
+
end
|
21
|
+
|
22
|
+
Thread.abort_on_exception = true
|
23
|
+
|
24
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
25
|
+
|
26
|
+
class AirbrakeTestError < RuntimeError
|
27
|
+
attr_reader :backtrace
|
28
|
+
|
29
|
+
def initialize(*)
|
30
|
+
super
|
31
|
+
# rubocop:disable Metrics/LineLength
|
32
|
+
@backtrace = [
|
33
|
+
"/home/kyrylo/code/airbrake/ruby/spec/spec_helper.rb:23:in `<top (required)>'",
|
34
|
+
"/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'",
|
35
|
+
"/opt/rubies/ruby-2.2.2/lib/ruby/2.2.0/rubygems/core_ext/kernel_require.rb:54:in `require'",
|
36
|
+
"/home/kyrylo/code/airbrake/ruby/spec/airbrake_spec.rb:1:in `<top (required)>'",
|
37
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1327:in `load'",
|
38
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1327:in `block in load_spec_files'",
|
39
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1325:in `each'",
|
40
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1325:in `load_spec_files'",
|
41
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:102:in `setup'",
|
42
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:88:in `run'",
|
43
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:73:in `run'",
|
44
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:41:in `invoke'",
|
45
|
+
"/home/kyrylo/.gem/ruby/2.2.2/gems/rspec-core-3.3.2/exe/rspec:4:in `<main>'"
|
46
|
+
]
|
47
|
+
# rubocop:enable Metrics/LineLength
|
48
|
+
end
|
49
|
+
|
50
|
+
# rubocop:disable Naming/AccessorMethodName
|
51
|
+
def set_backtrace(backtrace)
|
52
|
+
@backtrace = backtrace
|
53
|
+
end
|
54
|
+
# rubocop:enable Naming/AccessorMethodName
|
55
|
+
|
56
|
+
def message
|
57
|
+
'App crashed!'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class JavaAirbrakeTestError < AirbrakeTestError
|
62
|
+
def initialize(*)
|
63
|
+
super
|
64
|
+
# rubocop:disable Metrics/LineLength
|
65
|
+
@backtrace = [
|
66
|
+
"org.jruby.java.invokers.InstanceMethodInvoker.call(InstanceMethodInvoker.java:26)",
|
67
|
+
"org.jruby.ir.interpreter.Interpreter.INTERPRET_EVAL(Interpreter.java:126)",
|
68
|
+
"org.jruby.RubyKernel$INVOKER$s$0$3$eval19.call(RubyKernel$INVOKER$s$0$3$eval19.gen)",
|
69
|
+
"org.jruby.RubyKernel$INVOKER$s$0$0$loop.call(RubyKernel$INVOKER$s$0$0$loop.gen)",
|
70
|
+
"org.jruby.runtime.IRBlockBody.doYield(IRBlockBody.java:139)",
|
71
|
+
"org.jruby.RubyKernel$INVOKER$s$rbCatch19.call(RubyKernel$INVOKER$s$rbCatch19.gen)",
|
72
|
+
"opt.rubies.jruby_minus_9_dot_0_dot_0_dot_0.bin.irb.invokeOther4:start(/opt/rubies/jruby-9.0.0.0/bin/irb)",
|
73
|
+
"opt.rubies.jruby_minus_9_dot_0_dot_0_dot_0.bin.irb.RUBY$script(/opt/rubies/jruby-9.0.0.0/bin/irb:13)",
|
74
|
+
"org.jruby.ir.Compiler$1.load(Compiler.java:111)",
|
75
|
+
"org.jruby.Main.run(Main.java:225)",
|
76
|
+
"org.jruby.Main.main(Main.java:197)"
|
77
|
+
]
|
78
|
+
# rubocop:enable Metrics/LineLength
|
79
|
+
end
|
80
|
+
|
81
|
+
def is_a?(*)
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Ruby21Error < RuntimeError
|
87
|
+
attr_accessor :cause
|
88
|
+
|
89
|
+
def self.raise_error(msg)
|
90
|
+
ex = new(msg)
|
91
|
+
ex.cause = $ERROR_INFO
|
92
|
+
|
93
|
+
raise ex
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
puts <<BANNER
|
98
|
+
#{'#' * 80}
|
99
|
+
# RUBY_VERSION: #{RUBY_VERSION}
|
100
|
+
# RUBY_ENGINE: #{RUBY_ENGINE}
|
101
|
+
#{'#' * 80}
|
102
|
+
BANNER
|
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
|
@@ -0,0 +1,140 @@
|
|
1
|
+
RSpec.describe Airbrake::SyncSender do
|
2
|
+
describe "#build_https" do
|
3
|
+
it "overrides Net::HTTP's open_timeout and read_timeout if timeout is specified" do
|
4
|
+
config = Airbrake::Config.new(timeout: 10)
|
5
|
+
sender = described_class.new(config)
|
6
|
+
https = sender.__send__(:build_https, config.endpoint)
|
7
|
+
expect(https.open_timeout).to eq(10)
|
8
|
+
expect(https.read_timeout).to eq(10)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#send" do
|
13
|
+
let(:promise) { Airbrake::Promise.new }
|
14
|
+
let(:stdout) { StringIO.new }
|
15
|
+
|
16
|
+
let(:config) do
|
17
|
+
Airbrake::Config.new(
|
18
|
+
project_id: 1,
|
19
|
+
project_key: 'banana',
|
20
|
+
logger: Logger.new(stdout)
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:sender) { described_class.new(config) }
|
25
|
+
let(:notice) { Airbrake::Notice.new(config, AirbrakeTestError.new) }
|
26
|
+
let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
|
27
|
+
|
28
|
+
before { stub_request(:post, endpoint).to_return(body: '{}') }
|
29
|
+
|
30
|
+
it "sets the Content-Type header to JSON" do
|
31
|
+
sender.send({}, promise)
|
32
|
+
expect(
|
33
|
+
a_request(:post, endpoint).with(
|
34
|
+
headers: { 'Content-Type' => 'application/json' }
|
35
|
+
)
|
36
|
+
).to have_been_made.once
|
37
|
+
end
|
38
|
+
|
39
|
+
it "sets the User-Agent header to the notifier slug" do
|
40
|
+
sender.send({}, promise)
|
41
|
+
expect(
|
42
|
+
a_request(:post, endpoint).with(
|
43
|
+
headers: {
|
44
|
+
'User-Agent' => %r{airbrake-ruby/\d+\.\d+\.\d+ Ruby/\d+\.\d+\.\d+}
|
45
|
+
}
|
46
|
+
)
|
47
|
+
).to have_been_made.once
|
48
|
+
end
|
49
|
+
|
50
|
+
it "sets the Authorization header to the project key" do
|
51
|
+
sender.send({}, promise)
|
52
|
+
expect(
|
53
|
+
a_request(:post, endpoint).with(
|
54
|
+
headers: { 'Authorization' => 'Bearer banana' }
|
55
|
+
)
|
56
|
+
).to have_been_made.once
|
57
|
+
end
|
58
|
+
|
59
|
+
it "catches exceptions raised while sending" do
|
60
|
+
https = double("foo")
|
61
|
+
allow(sender).to receive(:build_https).and_return(https)
|
62
|
+
allow(https).to receive(:request).and_raise(StandardError.new('foo'))
|
63
|
+
expect(sender.send({}, promise)).to be_an(Airbrake::Promise)
|
64
|
+
expect(promise.value).to eq('error' => '**Airbrake: HTTP error: foo')
|
65
|
+
expect(stdout.string).to match(/ERROR -- : .+ HTTP error: foo/)
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when request body is nil" do
|
69
|
+
it "doesn't send data" do
|
70
|
+
expect_any_instance_of(Airbrake::Truncator).
|
71
|
+
to receive(:reduce_max_size).and_return(0)
|
72
|
+
|
73
|
+
encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
|
74
|
+
bad_string = Base64.decode64(encoded)
|
75
|
+
|
76
|
+
ex = AirbrakeTestError.new
|
77
|
+
backtrace = []
|
78
|
+
10.times { backtrace << "bin/rails:3:in `<#{bad_string}>'" }
|
79
|
+
ex.set_backtrace(backtrace)
|
80
|
+
|
81
|
+
notice = Airbrake::Notice.new(config, ex)
|
82
|
+
|
83
|
+
expect(sender.send(notice, promise)).to be_an(Airbrake::Promise)
|
84
|
+
expect(promise.value).
|
85
|
+
to match('error' => '**Airbrake: data was not sent because of missing body')
|
86
|
+
expect(stdout.string).to match(/ERROR -- : .+ data was not sent/)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when IP is rate limited" do
|
91
|
+
let(:endpoint) { %r{https://api.airbrake.io/api/v3/projects/1/notices} }
|
92
|
+
|
93
|
+
before do
|
94
|
+
stub_request(:post, endpoint).to_return(
|
95
|
+
status: 429,
|
96
|
+
body: '{"message":"IP is rate limited"}',
|
97
|
+
headers: { 'X-RateLimit-Delay' => '1' }
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns error" do
|
102
|
+
p1 = Airbrake::Promise.new
|
103
|
+
sender.send({}, p1)
|
104
|
+
expect(p1.value).to match('error' => '**Airbrake: IP is rate limited')
|
105
|
+
|
106
|
+
p2 = Airbrake::Promise.new
|
107
|
+
sender.send({}, p2)
|
108
|
+
expect(p2.value).to match('error' => '**Airbrake: IP is rate limited')
|
109
|
+
|
110
|
+
# Wait for X-RateLimit-Delay and then make a new request to make sure p2
|
111
|
+
# was ignored (no request made for it).
|
112
|
+
sleep 1
|
113
|
+
|
114
|
+
p3 = Airbrake::Promise.new
|
115
|
+
sender.send({}, p3)
|
116
|
+
expect(p3.value).to match('error' => '**Airbrake: IP is rate limited')
|
117
|
+
|
118
|
+
expect(a_request(:post, endpoint)).to have_been_made.twice
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "when the provided method is :put" do
|
123
|
+
before { stub_request(:put, endpoint).to_return(status: 200, body: '') }
|
124
|
+
|
125
|
+
it "PUTs the request" do
|
126
|
+
sender = described_class.new(config, :put)
|
127
|
+
sender.send({}, promise)
|
128
|
+
expect(a_request(:put, endpoint)).to have_been_made
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when the provided method is :post" do
|
133
|
+
it "POSTs the request" do
|
134
|
+
sender = described_class.new(config, :post)
|
135
|
+
sender.send({}, promise)
|
136
|
+
expect(a_request(:post, endpoint)).to have_been_made
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -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
|