heroku_hatchet 6.0.0 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -218,12 +218,13 @@ module Hatchet
218
218
  "Content-Type" => "application/json"
219
219
  }.merge(options[:headers] || {})
220
220
  options[:body] = JSON.generate(options[:body]) if options[:body]
221
+ options[:expects] << 429 if options[:expects]
221
222
 
222
223
  Hatchet::RETRIES.times.retry do
223
224
  PlatformAPI.rate_throttle.call do
224
225
  connection = Excon.new("https://api.heroku.com")
225
226
 
226
- return connection.request(options)
227
+ connection.request(options)
227
228
  end
228
229
  end
229
230
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "6.0.0"
2
+ VERSION = "7.1.3"
3
3
  end
@@ -1,15 +1,55 @@
1
1
  require("spec_helper")
2
2
 
3
3
  describe "AllowFailureGitTest" do
4
+ describe "release failures" do
5
+ let(:release_fail_proc) {
6
+ Proc.new do
7
+ File.open("Procfile", "w+") do |f|
8
+ f.write <<~EOM
9
+ release: echo "failing on release" && exit 1
10
+ EOM
11
+ end
12
+ end
13
+ }
14
+
15
+ it "is marked as a failure if the release fails" do
16
+ app = Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc, retries: 2)
17
+ def app.retry_error_message(*args); @test_attempts_count ||= 0; @test_attempts_count += 1; "" end
18
+ def app.test_attempts_count; @test_attempts_count ; end
19
+
20
+ expect {
21
+ app.deploy {}
22
+ }.to raise_error { |error|
23
+ expect(error).to be_a(Hatchet::App::FailedReleaseError)
24
+ expect(error.message).to_not match("Everything up-to-date")
25
+ }
26
+
27
+ expect(app.test_attempts_count).to eq(2)
28
+ end
29
+
30
+ it "works when failure is allowed" do
31
+ Hatchet::GitApp.new("default_ruby", before_deploy: release_fail_proc, allow_failure: true, retries: 3).tap do |app|
32
+ def app.retry_error_message(*args); @test_attempts_count ||= 0; @test_attempts_count += 1; "" end
33
+ def app.test_attempts_count; @test_attempts_count ; end
34
+
35
+ app.deploy do
36
+ expect(app.output).to match("failing on release")
37
+ expect(app.test_attempts_count).to eq(nil)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
4
43
  it "allowed failure" do
5
44
  Hatchet::GitApp.new("no_lockfile", allow_failure: true).deploy do |app|
6
- puts app.output
7
45
  expect(app.deployed?).to be_falsey
8
46
  expect(app.output).to match("Gemfile.lock required")
9
47
  end
10
48
  end
11
49
 
12
50
  it "failure with no flag" do
13
- expect { Hatchet::GitApp.new("no_lockfile").deploy }.to(raise_error(Hatchet::App::FailedDeploy))
51
+ expect {
52
+ Hatchet::GitApp.new("no_lockfile").deploy {}
53
+ }.to(raise_error(Hatchet::App::FailedDeploy))
14
54
  end
15
55
  end
@@ -1,6 +1,42 @@
1
1
  require("spec_helper")
2
2
 
3
3
  describe "AppTest" do
4
+ it "rate throttles `git push` " do
5
+ app = Hatchet::GitApp.new("default_ruby")
6
+ def app.git_push_heroku_yall
7
+ @_git_push_heroku_yall_call_count ||= 0
8
+ @_git_push_heroku_yall_call_count += 1
9
+ if @_git_push_heroku_yall_call_count >= 2
10
+ "Success"
11
+ else
12
+ raise Hatchet::App::FailedDeployError.new(self, "message", output: "Your account reached the API rate limit Please wait a few minutes before making new requests")
13
+ end
14
+ end
15
+
16
+ def app.sleep_called?; @sleep_called; end
17
+
18
+ def app.what_is_git_push_heroku_yall_call_count; @_git_push_heroku_yall_call_count; end
19
+ app.push_without_retry!
20
+
21
+ expect(app.what_is_git_push_heroku_yall_call_count).to be(2)
22
+ end
23
+
24
+ it "calls reaper if cannot create an app" do
25
+ app = Hatchet::App.new("default_ruby", buildpacks: [:default])
26
+ def app.heroku_api_create_app(*args); raise StandardError.new("made you look"); end
27
+
28
+ reaper = app.reaper
29
+
30
+ def reaper.cycle(app_exception_message: ); @app_exception_message = app_exception_message; end
31
+ def reaper.recorded_app_exception_message; @app_exception_message; end
32
+
33
+ expect {
34
+ app.create_app
35
+ }.to raise_error("made you look")
36
+
37
+ expect(reaper.recorded_app_exception_message).to match("made you look")
38
+ end
39
+
4
40
  it "app with default" do
5
41
  app = Hatchet::App.new("default_ruby", buildpacks: [:default])
6
42
  expect(app.buildpacks.first).to match("https://github.com/heroku/heroku-buildpack-ruby")
@@ -13,6 +49,37 @@ describe "AppTest" do
13
49
  expect(app.platform_api.app.info(app.name)["build_stack"]["name"]).to eq(stack)
14
50
  end
15
51
 
52
+ it "marks itself 'finished' when done in block mode" do
53
+ app = Hatchet::Runner.new("default_ruby")
54
+
55
+ def app.push_with_retry!; nil; end
56
+ app.deploy do |app|
57
+ expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
58
+ end
59
+
60
+ # After the app is updated, there's no guarantee it will still exist
61
+ # so we cannot rely on an api call to determine maintenance mode
62
+ app_update_info = app.instance_variable_get(:"@app_update_info")
63
+ expect(app_update_info["name"]).to eq(app.name)
64
+ expect(app_update_info["maintenance"]).to be_truthy
65
+ end
66
+
67
+ it "marks itself 'finished' when done in non-block mode" do
68
+ app = Hatchet::Runner.new("default_ruby")
69
+
70
+ def app.push_with_retry!; nil; end
71
+ app.deploy
72
+ expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
73
+
74
+ app.teardown!
75
+
76
+ # After the app is updated, there's no guarantee it will still exist
77
+ # so we cannot rely on an api call to determine maintenance mode
78
+ app_update_info = app.instance_variable_get(:"@app_update_info")
79
+ expect(app_update_info["name"]).to eq(app.name)
80
+ expect(app_update_info["maintenance"]).to be_truthy
81
+ end
82
+
16
83
  it "before deploy" do
17
84
  @called = false
18
85
  @dir = false
@@ -65,23 +132,95 @@ describe "AppTest" do
65
132
  end
66
133
 
67
134
  it "run" do
68
- app = Hatchet::GitApp.new("default_ruby")
135
+ skip("Must set HATCHET_EXPENSIVE_MODE") unless ENV["HATCHET_EXPENSIVE_MODE"]
136
+
137
+ app = Hatchet::GitApp.new("default_ruby", run_multi: true)
69
138
  app.deploy do
70
139
  expect(app.run("ls -a Gemfile 'foo bar #baz'")).to match(/ls: cannot access 'foo bar #baz': No such file or directory\s+Gemfile/)
71
140
  expect((0 != $?.exitstatus)).to be_truthy
72
- sleep(4)
141
+
73
142
  app.run("ls erpderp", heroku: ({ "exit-code" => (Hatchet::App::SkipDefaultOption) }))
74
143
  expect((0 == $?.exitstatus)).to be_truthy
75
- sleep(4)
144
+
76
145
  app.run("ls erpderp", heroku: ({ "no-tty" => nil }))
77
146
  expect((0 != $?.exitstatus)).to be_truthy
78
- sleep(4)
147
+
79
148
  expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "HELLO=ohai;NAME=world" }))).to match(/ohai world/)
80
- sleep(4)
149
+
81
150
  expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "" }))).to_not match(/ohai world/)
82
- sleep(4)
151
+
83
152
  random_name = SecureRandom.hex
84
153
  expect(app.run("mkdir foo; touch foo/#{random_name}; ls foo/")).to match(/#{random_name}/)
85
154
  end
86
155
  end
156
+
157
+ class AtomicCount
158
+ attr_reader :value
159
+
160
+ def initialize(value)
161
+ @value = value
162
+ @mutex = Mutex.new
163
+ end
164
+
165
+ # In MRI the `+=` is not atomic, it is two seperate virtual machine
166
+ # instructions. To protect against race conditions, we can lock with a mutex
167
+ def add(val)
168
+ @mutex.synchronize do
169
+ @value += val
170
+ end
171
+ end
172
+ end
173
+
174
+ it "run multi" do
175
+ skip("Must set HATCHET_EXPENSIVE_MODE") unless ENV["HATCHET_EXPENSIVE_MODE"]
176
+
177
+ @run_count = AtomicCount.new(0)
178
+ app = Hatchet::GitApp.new("default_ruby", run_multi: true)
179
+ app.deploy do
180
+ app.run_multi("ls") { |out| expect(out).to include("Gemfile"); @run_count.add(1) }
181
+ app.run_multi("blerg -v") { |_, status| expect(status.success?).to be_falsey; @run_count.add(1) }
182
+ app.run_multi("ruby -v") do |out, status|
183
+ expect(out).to include("ruby")
184
+ expect(status.success?).to be_truthy
185
+
186
+ @run_count.add(1)
187
+ end
188
+
189
+ expect(app.platform_api.formation.list(app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
190
+ end
191
+
192
+ # After the deploy block exits `teardown!` is called
193
+ # this ensures all `run_multi` commands have exited and the dyno should be scaled down
194
+ expect(@run_count.value).to eq(3)
195
+ end
196
+
197
+ describe "running concurrent tests in different examples works" do
198
+ # This is not a great pattern if we're running tests via a parallel runner
199
+ #
200
+ # For example this will be guaranteed to be called, not just once, but at least once for every process
201
+ # that needs to run a test. In the best case it will only fire once, in the worst case it will fire N times
202
+ # if there are N tests. It is effectively the same as a `before(:each)`
203
+ #
204
+ # Documented here: https://github.com/grosser/parallel_split_test/pull/22/files
205
+ before(:all) do
206
+ skip("Must set HATCHET_EXPENSIVE_MODE") unless ENV["HATCHET_EXPENSIVE_MODE"]
207
+
208
+ @app = Hatchet::GitApp.new("default_ruby", run_multi: true)
209
+ @app.deploy
210
+ end
211
+
212
+ after(:all) do
213
+ @app.teardown! if @app
214
+ end
215
+
216
+ it "test one" do
217
+ expect(@app.run("ls")).to include("Gemfile")
218
+ expect(@app.platform_api.formation.list(@app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
219
+ end
220
+
221
+ it "test two" do
222
+ expect(@app.run("ruby -v")).to include("ruby")
223
+ expect(@app.platform_api.formation.list(@app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
224
+ end
225
+ end
87
226
  end
@@ -4,7 +4,8 @@ describe "CIFourTest" do
4
4
  it "error with bad app" do
5
5
  string = SecureRandom.hex
6
6
 
7
- Hatchet::GitApp.new("default_ruby").run_ci do |test_run|
7
+ app = Hatchet::GitApp.new("default_ruby")
8
+ app.run_ci do |test_run|
8
9
  expect(test_run.output).to_not match(string)
9
10
  expect(test_run.output).to match("Installing rake")
10
11
 
@@ -14,7 +15,15 @@ describe "CIFourTest" do
14
15
  expect(test_run.output).to match(string)
15
16
  expect(test_run.output).to match("Using rake")
16
17
  expect(test_run.output).to_not match("Installing rake")
18
+
19
+ expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
17
20
  end
21
+
22
+ # After the app is updated, there's no guarantee it will still exist
23
+ # so we cannot rely on an api call to determine maintenance mode
24
+ app_update_info = app.instance_variable_get(:"@app_update_info")
25
+ expect(app_update_info["name"]).to eq(app.name)
26
+ expect(app_update_info["maintenance"]).to be_truthy
18
27
  end
19
28
 
20
29
  it "error with bad app" do
@@ -1,9 +1,15 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe "GitAppTest" do
4
- it "can deploy git app" do
5
- Hatchet::GitApp.new("rails5_ruby_schema_format").deploy do |app|
6
- expect(app.run("ruby -v")).to match("2.6.6")
4
+ it "can deploy git app to the main branch" do
5
+ Hatchet::GitApp.new("lock_fail_main", allow_failure: true).deploy do |app|
6
+ expect(app.output).to match("INTENTIONAL ERROR")
7
7
  end
8
8
  end
9
+
10
+ it "returns the correct branch name on circle CI" do
11
+ skip("only runs on circle") unless ENV["CIRCLE_BRANCH"]
12
+
13
+ expect(Hatchet.git_branch).to eq(ENV["CIRCLE_BRANCH"])
14
+ end
9
15
  end
@@ -2,6 +2,10 @@ require "spec_helper"
2
2
  require 'yaml'
3
3
 
4
4
  describe "LockTest" do
5
+ before(:all) do
6
+ puts(`bundle exec hatchet lock`)
7
+ end
8
+
5
9
  it "app with failure can be locked to prior commit" do
6
10
  Hatchet::GitApp.new("lock_fail").deploy do |app|
7
11
  expect(app.deployed?).to be_truthy
@@ -9,11 +13,69 @@ describe "LockTest" do
9
13
  end
10
14
 
11
15
  it "app with failure can be locked to master" do
12
- puts(`bundle exec hatchet lock`)
13
16
  lock = YAML.load_file("hatchet.lock")
14
17
  name, branch = lock.select { |k, v| k.end_with?("lock_fail_master") }.first
15
18
 
16
19
  expect(name).to eq("repo_fixtures/repos/lock/lock_fail_master")
17
20
  expect(branch).to eq("master")
18
21
  end
22
+
23
+ it "app with failure can be locked to main" do
24
+ lock = YAML.load_file("hatchet.lock")
25
+ name, branch = lock.select { |k, v| k.end_with?("lock_fail_main") }.first
26
+
27
+ expect(name).to eq("repo_fixtures/repos/lock/lock_fail_main")
28
+ expect(branch).to eq("main")
29
+ end
30
+ end
31
+
32
+ describe "isolated lock tests" do
33
+ it "works when there's no hatchet.lock" do
34
+ Dir.mktmpdir do |dir|
35
+ dir = Pathname.new(dir)
36
+
37
+ dir.join("hatchet.json").open("w+") do |f|
38
+ f.puts %Q{{ "foo": ["sharpstone/lock_fail_main_default_is_master"] }}
39
+ end
40
+
41
+ output = `cd #{dir} && hatchet lock 2>&1`
42
+
43
+ raise "Expected cmd `hatchet lock` to succeed, but it did not: #{output}" unless $?.success?
44
+ expect(output).to include("locking")
45
+
46
+ lockfile_contents = dir.join('hatchet.lock').read
47
+ expect(lockfile_contents).to include("repos/foo/lock_fail_main_default_is_master")
48
+ end
49
+ end
50
+
51
+ it "works when a project is locked to main but the default branch is master" do
52
+ Dir.mktmpdir do |dir|
53
+ dir = Pathname.new(dir)
54
+
55
+ dir.join("hatchet.json").open("w+") do |f|
56
+ f.puts %Q{{ "foo": ["sharpstone/lock_fail_main_default_is_master"] }}
57
+ end
58
+
59
+ dir.join("hatchet.lock").open("w+") do |f|
60
+ f.puts <<~EOM
61
+ ---
62
+ - - "./repos/foo/lock_fail_main_default_is_master"
63
+ - main
64
+ EOM
65
+ end
66
+
67
+ output = `cd #{dir} && hatchet install 2>&1`
68
+
69
+ raise "Expected cmd `hatchet install` to succeed, but it did not:\n#{output}" unless $?.success?
70
+ expect(output).to include("Installing")
71
+
72
+ lockfile_contents = dir.join('hatchet.lock').read
73
+ contents = YAML.safe_load(lockfile_contents).to_h
74
+ expect(contents).to eq({"./repos/foo/lock_fail_main_default_is_master" => "main"})
75
+
76
+ contents.each do |repo_dir, commit_or_branch|
77
+ expect(`cd #{dir.join(repo_dir)} && git describe --contains --all HEAD`).to match("main")
78
+ end
79
+ end
80
+ end
19
81
  end
@@ -0,0 +1,169 @@
1
+ require "spec_helper"
2
+
3
+ describe "Reaper" do
4
+ it "destroy all" do
5
+ reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
6
+
7
+ def reaper.get_heroku_apps
8
+ @mock_apps ||= [
9
+ {"name" => "hatchet-t-unfinished", "id" => 2, "maintenance" => false, "created_at" => Time.now.to_s},
10
+ {"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}
11
+ ]
12
+ end
13
+ def reaper.destroy_with_log(*args); @destroy_with_log_count ||= 0; @destroy_with_log_count += 1; end
14
+
15
+ reaper.destroy_all
16
+
17
+ expect(reaper.instance_variable_get("@destroy_with_log_count")).to eq(1)
18
+ end
19
+
20
+ describe "cycle" do
21
+ it "does not delete anything if under the limit" do
22
+ reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
23
+
24
+ def reaper.get_heroku_apps
25
+ @called_get_heroku_apps = true
26
+
27
+ @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}]
28
+ end
29
+ def reaper.check_get_heroku_apps_called; @called_get_heroku_apps ; end
30
+ def reaper.reap_once; raise "should not be called"; end
31
+
32
+ reaper.cycle
33
+
34
+ expect(reaper.check_get_heroku_apps_called).to be_truthy
35
+ end
36
+
37
+ it "deletes a maintenance mode app on error" do
38
+ reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
39
+
40
+ def reaper.get_heroku_apps
41
+ @mock_apps ||= [
42
+ {"name" => "hatchet-t-unfinished", "id" => 2, "maintenance" => false, "created_at" => Time.now.to_s},
43
+ {"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}
44
+ ]
45
+ end
46
+ def reaper.destroy_with_log(name: , id: )
47
+ @reaper_destroy_called_with = {"name" => name, "id" => id}
48
+ end
49
+ def reaper.destroy_called_with; @reaper_destroy_called_with; end
50
+
51
+ reaper.cycle(app_exception_message: true)
52
+
53
+ expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
54
+ end
55
+
56
+ it "deletes maintenance mode app when over limit" do
57
+ reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 0, io: StringIO.new)
58
+
59
+ def reaper.get_heroku_apps
60
+ @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}]
61
+ end
62
+ def reaper.destroy_with_log(name: , id: )
63
+ @reaper_destroy_called_with = {"name" => name, "id" => id}
64
+ end
65
+ def reaper.destroy_called_with; @reaper_destroy_called_with; end
66
+
67
+ reaper.cycle
68
+
69
+ expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
70
+ end
71
+
72
+ it "deletes an old app that is past TLL" do
73
+ reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 0, io: StringIO.new)
74
+
75
+ def reaper.get_heroku_apps
76
+ two_days_ago = DateTime.now.new_offset(0) - 2
77
+ @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => false, "created_at" => two_days_ago.to_s }]
78
+ end
79
+ def reaper.destroy_with_log(name: , id: )
80
+ @reaper_destroy_called_with = {"name" => name, "id" => id}
81
+ end
82
+ def reaper.destroy_called_with; @reaper_destroy_called_with; end
83
+
84
+ reaper.cycle
85
+
86
+ expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
87
+ end
88
+
89
+ it "sleeps, refreshes app list, and tries again when an old app is not past TTL" do
90
+ warning = StringIO.new
91
+ reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, initial_sleep: 0, io: warning)
92
+
93
+ def reaper.get_heroku_apps
94
+ now = DateTime.now.new_offset(0)
95
+ @mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => false, "created_at" => now.to_s }]
96
+ end
97
+ def reaper.destroy_with_log(name: , id: )
98
+ @reaper_destroy_called_with = {"name" => name, "id" => id}
99
+ end
100
+ def reaper.destroy_called_with; @reaper_destroy_called_with; end
101
+ def reaper.sleep(val)
102
+ @_slept_for = val
103
+ end
104
+
105
+ def reaper.get_slept_for_val; @_slept_for; end
106
+
107
+ reaper.cycle(app_exception_message: true)
108
+
109
+ expect(reaper.get_slept_for_val).to eq(0)
110
+ expect(reaper.destroy_called_with).to eq(nil)
111
+
112
+ expect(warning.string).to match("WARNING")
113
+ expect(warning.string).to match("total_app_count: 1, hatchet_app_count: 1/#{Hatchet::Reaper::HATCHET_APP_LIMIT}, finished: 0, unfinished: 1")
114
+ end
115
+ end
116
+
117
+ describe "app age" do
118
+ it "calculates young apps" do
119
+ time_now = DateTime.parse("2020-07-28T14:40:00Z")
120
+ age = Hatchet::Reaper::AppAge.new(created_at: time_now, time_now: time_now, ttl_minutes: 1)
121
+ expect(age.in_minutes).to eq(0.0)
122
+ expect(age.too_young_to_die?).to be_truthy
123
+ expect(age.can_delete?).to be_falsey
124
+ expect(age.sleep_for_ttl).to eq(60)
125
+ end
126
+
127
+ it "calculates old apps" do
128
+ time_now = DateTime.parse("2020-07-28T14:40:00Z")
129
+ created_at = time_now - 2
130
+ age = Hatchet::Reaper::AppAge.new(created_at: created_at, time_now: time_now, ttl_minutes: 1)
131
+ expect(age.in_minutes).to eq(2880.0)
132
+ expect(age.too_young_to_die?).to be_falsey
133
+ expect(age.can_delete?).to be_truthy
134
+ expect(age.sleep_for_ttl).to eq(0)
135
+ end
136
+ end
137
+
138
+ describe "reaper throttle" do
139
+ it "increments and decrements based on min_sleep" do
140
+ reaper_throttle = Hatchet::Reaper::ReaperThrottle.new(initial_sleep: 2)
141
+ reaper_throttle.call(max_sleep: 5) do |sleep_for|
142
+ expect(sleep_for).to eq(2)
143
+ end
144
+ reaper_throttle.call(max_sleep: 5) do |sleep_for|
145
+ expect(sleep_for).to eq(4)
146
+ end
147
+ reaper_throttle.call(max_sleep: 5) do |sleep_for|
148
+ expect(sleep_for).to eq(5)
149
+ end
150
+ # The throttle is now reset since it hit the min_sleep value
151
+
152
+ reaper_throttle.call(max_sleep: 5) do |sleep_for|
153
+ expect(sleep_for).to eq(2)
154
+ end
155
+ end
156
+ end
157
+
158
+ it "over limit" do
159
+ reaper = Hatchet::Reaper.new(api_rate_limit: -> (){}, io: StringIO.new)
160
+ def reaper.hatchet_app_count; Hatchet::Reaper::HATCHET_APP_LIMIT + 1; end
161
+
162
+ expect(reaper.over_limit?).to be_truthy
163
+
164
+ reaper = Hatchet::Reaper.new(api_rate_limit: -> (){}, io: StringIO.new)
165
+ def reaper.hatchet_app_count; Hatchet::Reaper::HATCHET_APP_LIMIT - 1; end
166
+
167
+ expect(reaper.over_limit?).to be_falsey
168
+ end
169
+ end