heroku_hatchet 6.0.0 → 7.1.3
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/.circleci/config.yml +2 -0
- data/CHANGELOG.md +36 -1
- data/README.md +774 -174
- data/bin/hatchet +13 -6
- data/hatchet.gemspec +1 -2
- data/hatchet.json +2 -1
- data/hatchet.lock +2 -0
- data/lib/hatchet.rb +2 -3
- data/lib/hatchet/api_rate_limit.rb +6 -17
- data/lib/hatchet/app.rb +150 -41
- data/lib/hatchet/config.rb +1 -1
- data/lib/hatchet/git_app.rb +29 -2
- data/lib/hatchet/reaper.rb +159 -56
- data/lib/hatchet/reaper/app_age.rb +49 -0
- data/lib/hatchet/reaper/reaper_throttle.rb +55 -0
- data/lib/hatchet/shell_throttle.rb +71 -0
- data/lib/hatchet/test_run.rb +2 -1
- data/lib/hatchet/version.rb +1 -1
- data/spec/hatchet/allow_failure_git_spec.rb +42 -2
- data/spec/hatchet/app_spec.rb +145 -6
- data/spec/hatchet/ci_spec.rb +10 -1
- data/spec/hatchet/git_spec.rb +9 -3
- data/spec/hatchet/lock_spec.rb +63 -1
- data/spec/unit/reaper_spec.rb +169 -0
- data/spec/unit/shell_throttle.rb +28 -0
- metadata +16 -23
data/lib/hatchet/test_run.rb
CHANGED
@@ -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
|
-
|
227
|
+
connection.request(options)
|
227
228
|
end
|
228
229
|
end
|
229
230
|
end
|
data/lib/hatchet/version.rb
CHANGED
@@ -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 {
|
51
|
+
expect {
|
52
|
+
Hatchet::GitApp.new("no_lockfile").deploy {}
|
53
|
+
}.to(raise_error(Hatchet::App::FailedDeploy))
|
14
54
|
end
|
15
55
|
end
|
data/spec/hatchet/app_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
141
|
+
|
73
142
|
app.run("ls erpderp", heroku: ({ "exit-code" => (Hatchet::App::SkipDefaultOption) }))
|
74
143
|
expect((0 == $?.exitstatus)).to be_truthy
|
75
|
-
|
144
|
+
|
76
145
|
app.run("ls erpderp", heroku: ({ "no-tty" => nil }))
|
77
146
|
expect((0 != $?.exitstatus)).to be_truthy
|
78
|
-
|
147
|
+
|
79
148
|
expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "HELLO=ohai;NAME=world" }))).to match(/ohai world/)
|
80
|
-
|
149
|
+
|
81
150
|
expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "" }))).to_not match(/ohai world/)
|
82
|
-
|
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
|
data/spec/hatchet/ci_spec.rb
CHANGED
@@ -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")
|
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
|
data/spec/hatchet/git_spec.rb
CHANGED
@@ -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("
|
6
|
-
expect(app.
|
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
|
data/spec/hatchet/lock_spec.rb
CHANGED
@@ -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
|