rory-deploy 1.8.4.1
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/.gitignore +10 -0
- data/CONTRIBUTORS.md +77 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.md +574 -0
- data/Rakefile +15 -0
- data/bin/rory +80 -0
- data/bin/rory-gen-config +79 -0
- data/lib/capistrano_dsl.rb +91 -0
- data/lib/centurion.rb +9 -0
- data/lib/centurion/deploy.rb +139 -0
- data/lib/centurion/deploy_dsl.rb +180 -0
- data/lib/centurion/docker_registry.rb +89 -0
- data/lib/centurion/docker_server.rb +79 -0
- data/lib/centurion/docker_server_group.rb +33 -0
- data/lib/centurion/docker_via_api.rb +166 -0
- data/lib/centurion/docker_via_cli.rb +81 -0
- data/lib/centurion/dogestry.rb +92 -0
- data/lib/centurion/logging.rb +28 -0
- data/lib/centurion/service.rb +218 -0
- data/lib/centurion/shell.rb +46 -0
- data/lib/centurion/version.rb +3 -0
- data/lib/core_ext/numeric_bytes.rb +94 -0
- data/lib/tasks/centurion.rake +15 -0
- data/lib/tasks/deploy.rake +250 -0
- data/lib/tasks/info.rake +24 -0
- data/lib/tasks/list.rake +56 -0
- data/rory-deploy.gemspec +33 -0
- data/spec/capistrano_dsl_spec.rb +67 -0
- data/spec/deploy_dsl_spec.rb +184 -0
- data/spec/deploy_spec.rb +212 -0
- data/spec/docker_registry_spec.rb +105 -0
- data/spec/docker_server_group_spec.rb +31 -0
- data/spec/docker_server_spec.rb +92 -0
- data/spec/docker_via_api_spec.rb +246 -0
- data/spec/docker_via_cli_spec.rb +91 -0
- data/spec/dogestry_spec.rb +73 -0
- data/spec/logging_spec.rb +41 -0
- data/spec/service_spec.rb +288 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/matchers/capistrano_dsl_matchers.rb +13 -0
- data/spec/support/matchers/exit_code_matches.rb +38 -0
- metadata +214 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'centurion/dogestry'
|
3
|
+
|
4
|
+
describe Centurion::Dogestry do
|
5
|
+
let(:dogestry_options) {
|
6
|
+
{
|
7
|
+
aws_access_key_id: "abc",
|
8
|
+
aws_secret_key: "xyz",
|
9
|
+
s3_bucket: "s3-registry-test"
|
10
|
+
}
|
11
|
+
}
|
12
|
+
let(:registry) { Centurion::Dogestry.new(dogestry_options) }
|
13
|
+
let(:repo) { 'google/golang' }
|
14
|
+
let(:pull_hosts) {
|
15
|
+
[
|
16
|
+
'tcp://host-1:2375',
|
17
|
+
'tcp://host-2:2375'
|
18
|
+
]
|
19
|
+
}
|
20
|
+
let(:flags) { "-pullhosts #{pull_hosts.join(',')}"}
|
21
|
+
|
22
|
+
describe '#aws_access_key_id' do
|
23
|
+
it 'returns correct value' do
|
24
|
+
expect(registry.aws_access_key_id).to eq(dogestry_options[:aws_access_key_id])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#aws_secret_key' do
|
29
|
+
it 'returns correct value' do
|
30
|
+
expect(registry.aws_secret_key).to eq(dogestry_options[:aws_secret_key])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#s3_bucket' do
|
35
|
+
it 'returns correct value' do
|
36
|
+
expect(registry.s3_bucket).to eq(dogestry_options[:s3_bucket])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#s3_region' do
|
41
|
+
it 'returns correct default value' do
|
42
|
+
expect(registry.s3_region).to eq("us-east-1")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#s3_url' do
|
47
|
+
it 'returns correct value' do
|
48
|
+
expect(registry.s3_url).to eq("s3://#{registry.s3_bucket}/?region=#{registry.s3_region}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#exec_command' do
|
53
|
+
it 'returns correct value' do
|
54
|
+
expect(registry.exec_command('pull', repo)).to start_with('dogestry')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#pull' do
|
59
|
+
it 'returns correct value' do
|
60
|
+
if registry.which('dogestry')
|
61
|
+
expect(registry).to receive(:echo).with("dogestry #{flags} pull #{registry.s3_url} #{repo}")
|
62
|
+
registry.pull(repo, pull_hosts)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#which' do
|
68
|
+
it 'finds dogestry command line' do
|
69
|
+
allow(File).to receive(:executable?).and_return(true)
|
70
|
+
expect(registry.which('dogestry')).to_not be_nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'centurion/logging'
|
3
|
+
|
4
|
+
class TestLogging
|
5
|
+
extend Centurion::Logging
|
6
|
+
def self.logger
|
7
|
+
log
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe Centurion::Logging do
|
12
|
+
let(:message) { %w{ something something_else } }
|
13
|
+
|
14
|
+
context '#info' do
|
15
|
+
it 'passes through to Logger' do
|
16
|
+
expect(TestLogging.logger).to receive(:info).with(message.join(' '))
|
17
|
+
TestLogging.info(*message)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context '#warn' do
|
22
|
+
it 'passes through to Logger' do
|
23
|
+
expect(TestLogging.logger).to receive(:warn).with(message.join(' '))
|
24
|
+
TestLogging.warn(*message)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context '#debug' do
|
29
|
+
it 'passes through to Logger' do
|
30
|
+
expect(TestLogging.logger).to receive(:debug).with(message.join(' '))
|
31
|
+
TestLogging.debug(*message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context '#error' do
|
36
|
+
it 'passes through to Logger' do
|
37
|
+
expect(TestLogging.logger).to receive(:error).with(message.join(' '))
|
38
|
+
TestLogging.error(*message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'centurion/service'
|
3
|
+
|
4
|
+
describe Centurion::Service do
|
5
|
+
|
6
|
+
let(:service) { Centurion::Service.new(:redis) }
|
7
|
+
let(:hostname) { 'shakespeare' }
|
8
|
+
let(:image) { 'redis' }
|
9
|
+
|
10
|
+
it 'creates a service from the environment' do
|
11
|
+
extend Capistrano::DSL
|
12
|
+
set_current_environment(:test)
|
13
|
+
set(:name, 'mycontainer')
|
14
|
+
set(:image, image)
|
15
|
+
set(:hostname, hostname)
|
16
|
+
set(:binds, [ Centurion::Service::Volume.new('/foo', '/foo/bar') ])
|
17
|
+
set(:port_bindings, [ Centurion::Service::PortBinding.new(12340, 80, 'tcp') ])
|
18
|
+
|
19
|
+
svc = Centurion::Service.from_env
|
20
|
+
expect(svc.name).to eq('mycontainer')
|
21
|
+
expect(svc.image).to eq(image)
|
22
|
+
expect(svc.dns).to be_nil
|
23
|
+
expect(svc.volumes.size).to eq(1)
|
24
|
+
expect(svc.volumes.first.host_volume).to eq('/foo')
|
25
|
+
expect(svc.port_bindings.size).to eq(1)
|
26
|
+
expect(svc.port_bindings.first.container_port).to eq(80)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'starts with a command' do
|
30
|
+
service.command = ['redis-server']
|
31
|
+
expect(service.command).to eq(['redis-server'])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'has memory bounds' do
|
35
|
+
service.memory = 1024
|
36
|
+
expect(service.memory).to eq(1024)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'rejects non-numeric memory bounds' do
|
40
|
+
expect(-> { service.memory = 'all' }).to raise_error
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'has cpu shares bounds' do
|
44
|
+
service.cpu_shares = 512
|
45
|
+
expect(service.cpu_shares).to eq(512)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'rejects non-numeric cpu shares' do
|
49
|
+
expect(-> { service.cpu_shares = 'all' }).to raise_error
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'has a custom dns association' do
|
53
|
+
service.dns = 'redis.example.com'
|
54
|
+
expect(service.dns).to eq('redis.example.com')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'boots from a docker image' do
|
58
|
+
service.image = 'registry.hub.docker.com/library/redis'
|
59
|
+
expect(service.image).to eq('registry.hub.docker.com/library/redis')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'has env vars' do
|
63
|
+
service.add_env_vars(SLAVE_OF: '127.0.0.1')
|
64
|
+
service.add_env_vars(USE_AOF: '1')
|
65
|
+
expect(service.env_vars).to eq(SLAVE_OF: '127.0.0.1', USE_AOF: '1')
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'has volume bindings' do
|
69
|
+
service.add_volume('/volumes/redis/data', '/data')
|
70
|
+
service.add_volume('/volumes/redis/config', '/config')
|
71
|
+
expect(service.volumes).to eq([Centurion::Service::Volume.new('/volumes/redis/data', '/data'),
|
72
|
+
Centurion::Service::Volume.new('/volumes/redis/config', '/config')])
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'has port mappings' do
|
76
|
+
service.add_port_bindings(8000, 6379, 'tcp', '127.0.0.1')
|
77
|
+
service.add_port_bindings(18000, 16379, 'tcp', '127.0.0.1')
|
78
|
+
expect(service.port_bindings).to eq([Centurion::Service::PortBinding.new(8000, 6379, 'tcp', '127.0.0.1'),
|
79
|
+
Centurion::Service::PortBinding.new(18000, 16379, 'tcp', '127.0.0.1')])
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'builds a list of public ports for the service' do
|
83
|
+
service.add_port_bindings(8000, 6379, 'tcp', '127.0.0.1')
|
84
|
+
service.add_port_bindings(18000, 16379, 'tcp', '127.0.0.1')
|
85
|
+
expect(service.public_ports).to eq([8000, 18000])
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'building a container configuration' do
|
89
|
+
service = Centurion::Service.new(:redis)
|
90
|
+
service.image = 'http://registry.hub.docker.com/library/redis'
|
91
|
+
service.command = ['redis-server', '--appendonly', 'yes']
|
92
|
+
service.memory = 1024
|
93
|
+
service.cpu_shares = 512
|
94
|
+
service.add_env_vars(SLAVE_OF: '127.0.0.2')
|
95
|
+
service.add_port_bindings(8000, 6379, 'tcp', '10.0.0.1')
|
96
|
+
service.network_mode = 'host'
|
97
|
+
service.add_volume('/volumes/redis.8000', '/data')
|
98
|
+
|
99
|
+
it 'builds a valid docker container configuration' do
|
100
|
+
expect(service.build_config('example.com')).to eq({
|
101
|
+
'Image' => 'http://registry.hub.docker.com/library/redis',
|
102
|
+
'Cmd' => ['redis-server', '--appendonly', 'yes'],
|
103
|
+
'Memory' => 1024,
|
104
|
+
'CpuShares' => 512,
|
105
|
+
'ExposedPorts' => {'6379/tcp' => {}},
|
106
|
+
'Env' => ['SLAVE_OF=127.0.0.2'],
|
107
|
+
'Volumes' => {'/data' => {}},
|
108
|
+
|
109
|
+
# TODO: Ignoring this for now because Docker 1.6
|
110
|
+
# https://github.com/newrelic/centurion/issues/117
|
111
|
+
# 'VolumesFrom' => 'parent'
|
112
|
+
})
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'overrides the default hostname when passed a block' do
|
116
|
+
expect(service.build_config('example.com') { |s| "host.#{s}" }).to eq({
|
117
|
+
'Image' => 'http://registry.hub.docker.com/library/redis',
|
118
|
+
'Hostname' => 'host.example.com',
|
119
|
+
'Cmd' => ['redis-server', '--appendonly', 'yes'],
|
120
|
+
'Memory' => 1024,
|
121
|
+
'CpuShares' => 512,
|
122
|
+
'ExposedPorts' => {'6379/tcp' => {}},
|
123
|
+
'Env' => ['SLAVE_OF=127.0.0.2'],
|
124
|
+
'Volumes' => {'/data' => {}},
|
125
|
+
|
126
|
+
# TODO: Ignoring this for now because Docker 1.6
|
127
|
+
# https://github.com/newrelic/centurion/issues/117
|
128
|
+
# 'VolumesFrom' => 'parent'
|
129
|
+
})
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'interpolates hostname into env variables' do
|
134
|
+
allow(Socket).to receive(:getaddrinfo).and_return([["AF_INET", 0, "93.184.216.34", "93.184.216.34", 2, 1, 6]])
|
135
|
+
service = Centurion::Service.new(:redis)
|
136
|
+
service.add_env_vars(HOST: '%DOCKER_HOSTNAME%')
|
137
|
+
|
138
|
+
expect(service.build_config('example.com')['Env']).to eq(['HOST=example.com'])
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'interpolates host ip into env variables' do
|
142
|
+
allow(Socket).to receive(:getaddrinfo).and_return([["AF_INET", 0, "93.184.216.34", "93.184.216.34", 2, 1, 6]])
|
143
|
+
service = Centurion::Service.new(:redis)
|
144
|
+
service.add_env_vars(HOST: '%DOCKER_HOST_IP%')
|
145
|
+
|
146
|
+
expect(service.build_config('example.com')['Env']).to eq(['HOST=93.184.216.34'])
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'does not blow up on non-string values' do
|
150
|
+
expect { service.add_env_vars(SOMETHING: true) }.not_to raise_error
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'builds a valid docker host configuration' do
|
154
|
+
service = Centurion::Service.new(:redis)
|
155
|
+
service.dns = 'example.com'
|
156
|
+
service.add_port_bindings(8000, 6379)
|
157
|
+
service.cap_adds = ['IPC_BIND', 'NET_RAW']
|
158
|
+
service.cap_drops = ['DAC_OVERRIDE']
|
159
|
+
service.add_volume('/volumes/redis.8000', '/data')
|
160
|
+
|
161
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('on-failure', 10))).to eq({
|
162
|
+
'Binds' => ['/volumes/redis.8000:/data'],
|
163
|
+
'CapAdd' => ['IPC_BIND', 'NET_RAW'],
|
164
|
+
'CapDrop' => ['DAC_OVERRIDE'],
|
165
|
+
'PortBindings' => {
|
166
|
+
'6379/tcp' => [{'HostPort' => '8000'}]
|
167
|
+
},
|
168
|
+
'NetworkMode' => 'bridge',
|
169
|
+
'Dns' => 'example.com',
|
170
|
+
'RestartPolicy' => {
|
171
|
+
'Name' => 'on-failure',
|
172
|
+
'MaximumRetryCount' => 10
|
173
|
+
}
|
174
|
+
})
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'ignores garbage restart policy' do
|
178
|
+
service = Centurion::Service.new(:redis)
|
179
|
+
|
180
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('garbage'))).to eq({
|
181
|
+
'Binds' => [],
|
182
|
+
'CapAdd' => [],
|
183
|
+
'CapDrop' => [],
|
184
|
+
'PortBindings' => {},
|
185
|
+
'NetworkMode' => 'bridge',
|
186
|
+
'RestartPolicy' => {
|
187
|
+
'Name' => 'on-failure',
|
188
|
+
'MaximumRetryCount' => 10
|
189
|
+
}
|
190
|
+
})
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'accepts "no" restart policy' do
|
194
|
+
service = Centurion::Service.new(:redis)
|
195
|
+
|
196
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('no'))).to eq({
|
197
|
+
'Binds' => [],
|
198
|
+
'CapAdd' => [],
|
199
|
+
'CapDrop' => [],
|
200
|
+
'PortBindings' => {},
|
201
|
+
'NetworkMode' => 'bridge',
|
202
|
+
'RestartPolicy' => {
|
203
|
+
'Name' => 'no',
|
204
|
+
}
|
205
|
+
})
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'accepts "always" restart policy' do
|
209
|
+
service = Centurion::Service.new(:redis)
|
210
|
+
|
211
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('always'))).to eq({
|
212
|
+
'Binds' => [],
|
213
|
+
'CapAdd' => [],
|
214
|
+
'CapDrop' => [],
|
215
|
+
'PortBindings' => {},
|
216
|
+
'NetworkMode' => 'bridge',
|
217
|
+
'RestartPolicy' => {
|
218
|
+
'Name' => 'always',
|
219
|
+
}
|
220
|
+
})
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'accepts "on-failure" restart policy with retry count' do
|
224
|
+
service = Centurion::Service.new(:redis)
|
225
|
+
|
226
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('on-failure', 50))).to eq({
|
227
|
+
'Binds' => [],
|
228
|
+
'CapAdd' => [],
|
229
|
+
'CapDrop' => [],
|
230
|
+
'NetworkMode' => 'bridge',
|
231
|
+
'PortBindings' => {},
|
232
|
+
'RestartPolicy' => {
|
233
|
+
'Name' => 'on-failure',
|
234
|
+
'MaximumRetryCount' => 50
|
235
|
+
}
|
236
|
+
})
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'builds docker configuration for volume binds' do
|
240
|
+
service.add_volume('/volumes/redis/data', '/data')
|
241
|
+
expect(service.volume_binds_config).to eq(['/volumes/redis/data:/data'])
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'builds docker configuration for port bindings' do
|
245
|
+
service.add_port_bindings(8000, 6379, 'tcp', '127.0.0.1')
|
246
|
+
expect(service.port_bindings_config).to eq({
|
247
|
+
'6379/tcp' => [{'HostPort' => '8000', 'HostIp' => '127.0.0.1'}]
|
248
|
+
})
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'builds docker configuration for port bindings without host ip' do
|
252
|
+
service.add_port_bindings(8000, 6379, 'tcp')
|
253
|
+
expect(service.port_bindings_config).to eq({
|
254
|
+
'6379/tcp' => [{'HostPort' => '8000'}]
|
255
|
+
})
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'builds docker configuration for container-linked networking' do
|
259
|
+
service.network_mode = 'container:a2e8937b'
|
260
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('on-failure', 50))).to eq({
|
261
|
+
'Binds' => [],
|
262
|
+
'CapAdd' => [],
|
263
|
+
'CapDrop' => [],
|
264
|
+
'NetworkMode' => 'container:a2e8937b',
|
265
|
+
'PortBindings' => {},
|
266
|
+
'RestartPolicy' => {
|
267
|
+
'Name' => 'on-failure',
|
268
|
+
'MaximumRetryCount' => 50
|
269
|
+
}
|
270
|
+
})
|
271
|
+
end
|
272
|
+
|
273
|
+
it 'builds docker configuration for host networking' do
|
274
|
+
service.network_mode = 'host'
|
275
|
+
expect(service.build_host_config(Centurion::Service::RestartPolicy.new('on-failure', 50))).to eq({
|
276
|
+
'Binds' => [],
|
277
|
+
'CapAdd' => [],
|
278
|
+
'CapDrop' => [],
|
279
|
+
'NetworkMode' => 'host',
|
280
|
+
'PortBindings' => {},
|
281
|
+
'RestartPolicy' => {
|
282
|
+
'Name' => 'on-failure',
|
283
|
+
'MaximumRetryCount' => 50
|
284
|
+
}
|
285
|
+
})
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
RSpec::Matchers.define :have_key_and_value do |expected_key, expected_value|
|
2
|
+
match do |actual|
|
3
|
+
actual.env[actual.current_environment].has_key?(expected_key.to_sym) && (actual.fetch(expected_key.to_sym) == expected_value)
|
4
|
+
end
|
5
|
+
|
6
|
+
failure_message do |actual|
|
7
|
+
"expected that #{actual.env[actual.current_environment].keys.inspect} would include #{expected_key.inspect} with value #{expected_value.inspect}"
|
8
|
+
end
|
9
|
+
|
10
|
+
failure_message_when_negated do |actual|
|
11
|
+
"expected that #{actual.env[actual.current_environment].keys.join(', ')} would not include #{expected_key.inspect} with value #{expected_value.inspect}"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# https://gist.github.com/mmasashi/58bd7e2668836a387856
|
2
|
+
RSpec::Matchers.define :terminate do |code|
|
3
|
+
actual = nil
|
4
|
+
|
5
|
+
def supports_block_expectations?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
match do |block|
|
10
|
+
begin
|
11
|
+
block.call
|
12
|
+
rescue SystemExit => e
|
13
|
+
actual = e.status
|
14
|
+
end
|
15
|
+
actual and actual == status_code
|
16
|
+
end
|
17
|
+
|
18
|
+
chain :with_code do |status_code|
|
19
|
+
@status_code = status_code
|
20
|
+
end
|
21
|
+
|
22
|
+
failure_message do |block|
|
23
|
+
"expected block to call exit(#{status_code}) but exit" +
|
24
|
+
(actual.nil? ? " not called" : "(#{actual}) was called")
|
25
|
+
end
|
26
|
+
|
27
|
+
failure_message_when_negated do |block|
|
28
|
+
"expected block not to call exit(#{status_code})"
|
29
|
+
end
|
30
|
+
|
31
|
+
description do
|
32
|
+
"expect block to call exit(#{status_code})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def status_code
|
36
|
+
@status_code ||= 0
|
37
|
+
end
|
38
|
+
end
|