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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +554 -0
  3. data/lib/airbrake-ruby/async_sender.rb +119 -0
  4. data/lib/airbrake-ruby/backtrace.rb +194 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +53 -0
  6. data/lib/airbrake-ruby/config.rb +238 -0
  7. data/lib/airbrake-ruby/config/validator.rb +63 -0
  8. data/lib/airbrake-ruby/deploy_notifier.rb +47 -0
  9. data/lib/airbrake-ruby/file_cache.rb +48 -0
  10. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  11. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +90 -0
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +42 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  18. data/lib/airbrake-ruby/filters/keys_blacklist.rb +50 -0
  19. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  20. data/lib/airbrake-ruby/filters/keys_whitelist.rb +49 -0
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +104 -0
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  25. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  26. data/lib/airbrake-ruby/ignorable.rb +44 -0
  27. data/lib/airbrake-ruby/nested_exception.rb +39 -0
  28. data/lib/airbrake-ruby/notice.rb +165 -0
  29. data/lib/airbrake-ruby/notice_notifier.rb +228 -0
  30. data/lib/airbrake-ruby/performance_notifier.rb +161 -0
  31. data/lib/airbrake-ruby/promise.rb +99 -0
  32. data/lib/airbrake-ruby/response.rb +71 -0
  33. data/lib/airbrake-ruby/stat.rb +56 -0
  34. data/lib/airbrake-ruby/sync_sender.rb +111 -0
  35. data/lib/airbrake-ruby/tdigest.rb +393 -0
  36. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  37. data/lib/airbrake-ruby/truncator.rb +115 -0
  38. data/lib/airbrake-ruby/version.rb +6 -0
  39. data/spec/airbrake_spec.rb +171 -0
  40. data/spec/async_sender_spec.rb +154 -0
  41. data/spec/backtrace_spec.rb +438 -0
  42. data/spec/code_hunk_spec.rb +118 -0
  43. data/spec/config/validator_spec.rb +189 -0
  44. data/spec/config_spec.rb +281 -0
  45. data/spec/deploy_notifier_spec.rb +41 -0
  46. data/spec/file_cache.rb +36 -0
  47. data/spec/filter_chain_spec.rb +83 -0
  48. data/spec/filters/context_filter_spec.rb +25 -0
  49. data/spec/filters/dependency_filter_spec.rb +14 -0
  50. data/spec/filters/exception_attributes_filter_spec.rb +63 -0
  51. data/spec/filters/gem_root_filter_spec.rb +44 -0
  52. data/spec/filters/git_last_checkout_filter_spec.rb +48 -0
  53. data/spec/filters/git_repository_filter.rb +53 -0
  54. data/spec/filters/git_revision_filter_spec.rb +126 -0
  55. data/spec/filters/keys_blacklist_spec.rb +236 -0
  56. data/spec/filters/keys_whitelist_spec.rb +205 -0
  57. data/spec/filters/root_directory_filter_spec.rb +42 -0
  58. data/spec/filters/sql_filter_spec.rb +219 -0
  59. data/spec/filters/system_exit_filter_spec.rb +14 -0
  60. data/spec/filters/thread_filter_spec.rb +279 -0
  61. data/spec/fixtures/notroot.txt +7 -0
  62. data/spec/fixtures/project_root/code.rb +221 -0
  63. data/spec/fixtures/project_root/empty_file.rb +0 -0
  64. data/spec/fixtures/project_root/long_line.txt +1 -0
  65. data/spec/fixtures/project_root/short_file.rb +3 -0
  66. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  67. data/spec/helpers.rb +9 -0
  68. data/spec/ignorable_spec.rb +14 -0
  69. data/spec/nested_exception_spec.rb +75 -0
  70. data/spec/notice_notifier_spec.rb +436 -0
  71. data/spec/notice_notifier_spec/options_spec.rb +266 -0
  72. data/spec/notice_spec.rb +297 -0
  73. data/spec/performance_notifier_spec.rb +287 -0
  74. data/spec/promise_spec.rb +165 -0
  75. data/spec/response_spec.rb +82 -0
  76. data/spec/spec_helper.rb +102 -0
  77. data/spec/stat_spec.rb +35 -0
  78. data/spec/sync_sender_spec.rb +140 -0
  79. data/spec/tdigest_spec.rb +230 -0
  80. data/spec/time_truncate_spec.rb +13 -0
  81. data/spec/truncator_spec.rb +238 -0
  82. 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
@@ -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