chef 11.14.0.alpha.3-x86-mingw32 → 11.14.0.alpha.4-x86-mingw32
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/CONTRIBUTING.md +140 -99
- data/lib/chef/application.rb +80 -2
- data/lib/chef/application/apply.rb +1 -0
- data/lib/chef/application/client.rb +5 -0
- data/lib/chef/application/knife.rb +4 -0
- data/lib/chef/application/windows_service.rb +1 -0
- data/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb +6 -4
- data/lib/chef/config.rb +5 -3
- data/lib/chef/exceptions.rb +2 -0
- data/lib/chef/http/basic_client.rb +1 -0
- data/lib/chef/knife.rb +1 -0
- data/lib/chef/platform/provider_mapping.rb +7 -0
- data/lib/chef/provider/env/windows.rb +2 -0
- data/lib/chef/provider/group/usermod.rb +1 -1
- data/lib/chef/provider/mount/solaris.rb +233 -0
- data/lib/chef/provider/package/apt.rb +9 -0
- data/lib/chef/provider/package/windows.rb +3 -0
- data/lib/chef/providers.rb +1 -0
- data/lib/chef/resource/mount.rb +6 -1
- data/lib/chef/util/path_helper.rb +94 -0
- data/lib/chef/version.rb +1 -1
- data/spec/functional/application_spec.rb +58 -0
- data/spec/functional/resource/mount_spec.rb +14 -11
- data/spec/integration/client/client_spec.rb +11 -0
- data/spec/integration/knife/common_options_spec.rb +9 -0
- data/spec/unit/application_spec.rb +157 -0
- data/spec/unit/http/basic_client_spec.rb +42 -0
- data/spec/unit/provider/env/windows_spec.rb +67 -0
- data/spec/unit/provider/group/usermod_spec.rb +2 -1
- data/spec/unit/provider/mount/mount_spec.rb +3 -3
- data/spec/unit/provider/mount/solaris_spec.rb +646 -0
- data/spec/unit/provider/package/apt_spec.rb +5 -0
- data/spec/unit/provider/package/windows_spec.rb +6 -0
- data/spec/unit/resource_reporter_spec.rb +2 -2
- data/spec/unit/util/path_helper_spec.rb +136 -0
- metadata +23 -16
@@ -0,0 +1,94 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Bryan McLellan <btm@loftninjas.org>
|
3
|
+
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'chef/platform'
|
20
|
+
require 'chef/exceptions'
|
21
|
+
|
22
|
+
class Chef
|
23
|
+
class Util
|
24
|
+
class PathHelper
|
25
|
+
# Maximum characters in a standard Windows path (260 including drive letter and NUL)
|
26
|
+
WIN_MAX_PATH = 259
|
27
|
+
|
28
|
+
def self.validate_path(path)
|
29
|
+
if Chef::Platform.windows?
|
30
|
+
unless printable?(path)
|
31
|
+
msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings."
|
32
|
+
Chef::Log.error(msg)
|
33
|
+
raise Chef::Exceptions::ValidationFailed, msg
|
34
|
+
end
|
35
|
+
|
36
|
+
if windows_max_length_exceeded?(path)
|
37
|
+
Chef::Log.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'")
|
38
|
+
path.insert(0, "\\\\?\\")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
path
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.windows_max_length_exceeded?(path)
|
46
|
+
# Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API
|
47
|
+
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
|
48
|
+
unless path =~ /^\\\\?\\/
|
49
|
+
if path.length > WIN_MAX_PATH
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.printable?(string)
|
58
|
+
# returns true if string is free of non-printable characters (escape sequences)
|
59
|
+
# this returns false for whitespace escape sequences as well, e.g. \n\t
|
60
|
+
if string =~ /[^[:print:]]/
|
61
|
+
false
|
62
|
+
else
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Produces a comparable path.
|
68
|
+
def self.canonical_path(path, add_prefix=true)
|
69
|
+
# Rather than find an equivalent for File.absolute_path on 1.8.7, just bail out
|
70
|
+
raise NotImplementedError, "This feature is not supported on Ruby versions < 1.9" if RUBY_VERSION.to_f < 1.9
|
71
|
+
|
72
|
+
# First remove extra separators and resolve any relative paths
|
73
|
+
abs_path = File.absolute_path(path)
|
74
|
+
|
75
|
+
if Chef::Platform.windows?
|
76
|
+
# Add the \\?\ API prefix on Windows unless add_prefix is false
|
77
|
+
# Downcase on Windows where paths are still case-insensitive
|
78
|
+
abs_path.gsub!(::File::SEPARATOR, ::File::ALT_SEPARATOR)
|
79
|
+
if add_prefix && abs_path !~ /^\\\\?\\/
|
80
|
+
abs_path.insert(0, "\\\\?\\")
|
81
|
+
end
|
82
|
+
|
83
|
+
abs_path.downcase!
|
84
|
+
end
|
85
|
+
|
86
|
+
abs_path
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.paths_eql?(path1, path2)
|
90
|
+
canonical_path(path1) == canonical_path(path2)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/chef/version.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'spec_helper'
|
19
|
+
require 'chef/mixin/shell_out'
|
20
|
+
|
21
|
+
describe Chef::Application do
|
22
|
+
include Chef::Mixin::ShellOut
|
23
|
+
|
24
|
+
before do
|
25
|
+
@original_argv = ARGV.dup
|
26
|
+
ARGV.clear
|
27
|
+
@original_env = ENV.to_hash
|
28
|
+
ENV.clear
|
29
|
+
@app = Chef::Application.new
|
30
|
+
end
|
31
|
+
|
32
|
+
after do
|
33
|
+
ARGV.replace(@original_argv)
|
34
|
+
ENV.clear
|
35
|
+
ENV.update(@original_env)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "when proxy options are set in config" do
|
39
|
+
before do
|
40
|
+
Chef::Config[:http_proxy] = "http://proxy.example.org:8080"
|
41
|
+
Chef::Config[:https_proxy] = nil
|
42
|
+
Chef::Config[:ftp_proxy] = nil
|
43
|
+
Chef::Config[:no_proxy] = nil
|
44
|
+
|
45
|
+
@app.configure_proxy_environment_variables
|
46
|
+
end
|
47
|
+
|
48
|
+
it "saves built proxy to ENV which shell_out can use" do
|
49
|
+
so = if windows?
|
50
|
+
shell_out("echo $env:http_proxy")
|
51
|
+
else
|
52
|
+
shell_out("echo $http_proxy")
|
53
|
+
end
|
54
|
+
|
55
|
+
so.stdout.should == "http://proxy.example.org:8080\n"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -21,7 +21,7 @@ require 'chef/mixin/shell_out'
|
|
21
21
|
require 'tmpdir'
|
22
22
|
|
23
23
|
# run this test only for following platforms.
|
24
|
-
include_flag = !(['ubuntu', 'centos', 'aix'].include?(ohai[:platform]))
|
24
|
+
include_flag = !(['ubuntu', 'centos', 'aix', 'solaris2'].include?(ohai[:platform]))
|
25
25
|
|
26
26
|
describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
27
27
|
|
@@ -52,6 +52,9 @@ describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
|
52
52
|
end
|
53
53
|
fstype = "tmpfs"
|
54
54
|
shell_out!("mkfs -q #{device} 512")
|
55
|
+
when "solaris2"
|
56
|
+
device = "swap"
|
57
|
+
fstype = "tmpfs"
|
55
58
|
else
|
56
59
|
end
|
57
60
|
[device, fstype]
|
@@ -71,11 +74,10 @@ describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
|
71
74
|
end
|
72
75
|
|
73
76
|
# platform specific validations.
|
74
|
-
def
|
77
|
+
def mount_should_exist(mount_point, device, fstype = nil, options = nil)
|
75
78
|
validation_cmd = "mount | grep #{mount_point} | grep #{device} "
|
76
79
|
validation_cmd << " | grep #{fstype} " unless fstype.nil?
|
77
80
|
validation_cmd << " | grep #{options.join(',')} " unless options.nil? || options.empty?
|
78
|
-
puts "validation_cmd = #{validation_cmd}"
|
79
81
|
expect(shell_out(validation_cmd).exitstatus).to eq(0)
|
80
82
|
end
|
81
83
|
|
@@ -87,6 +89,8 @@ describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
|
87
89
|
case ohai[:platform]
|
88
90
|
when 'aix'
|
89
91
|
mount_config = "/etc/filesystems"
|
92
|
+
when 'solaris2'
|
93
|
+
mount_config = "/etc/vfstab"
|
90
94
|
else
|
91
95
|
mount_config = "/etc/fstab"
|
92
96
|
end
|
@@ -119,7 +123,7 @@ describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
|
119
123
|
provider
|
120
124
|
end
|
121
125
|
|
122
|
-
|
126
|
+
let(:current_resource) do
|
123
127
|
provider.load_current_resource
|
124
128
|
provider.current_resource
|
125
129
|
end
|
@@ -138,7 +142,6 @@ describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
|
138
142
|
end
|
139
143
|
end
|
140
144
|
end
|
141
|
-
|
142
145
|
end
|
143
146
|
|
144
147
|
after(:all) do
|
@@ -156,28 +159,28 @@ describe Chef::Resource::Mount, :requires_root, :external => include_flag do
|
|
156
159
|
current_resource.mounted.should be_false
|
157
160
|
new_resource.run_action(:mount)
|
158
161
|
new_resource.should be_updated
|
159
|
-
|
162
|
+
mount_should_exist(new_resource.mount_point, new_resource.device)
|
160
163
|
end
|
161
|
-
|
162
164
|
end
|
163
165
|
|
164
|
-
|
166
|
+
# don't run the remount tests on solaris2 (tmpfs does not support remount)
|
167
|
+
describe "when the filesystem should be remounted and the resource supports remounting", :external => ohai[:platform] == "solaris2" do
|
165
168
|
it "should remount the filesystem if it is mounted" do
|
166
169
|
new_resource.run_action(:mount)
|
167
|
-
|
170
|
+
mount_should_exist(new_resource.mount_point, new_resource.device)
|
168
171
|
|
169
172
|
new_resource.supports[:remount] = true
|
170
173
|
new_resource.options "rw,log=NULL" if ohai[:platform] == 'aix'
|
171
174
|
new_resource.run_action(:remount)
|
172
175
|
|
173
|
-
|
176
|
+
mount_should_exist(new_resource.mount_point, new_resource.device, nil, (ohai[:platform] == 'aix') ? new_resource.options : nil)
|
174
177
|
end
|
175
178
|
end
|
176
179
|
|
177
180
|
describe "when the target state is a unmounted filesystem" do
|
178
181
|
it "should umount the filesystem if it is mounted" do
|
179
182
|
new_resource.run_action(:mount)
|
180
|
-
|
183
|
+
mount_should_exist(new_resource.mount_point, new_resource.device)
|
181
184
|
|
182
185
|
new_resource.run_action(:umount)
|
183
186
|
mount_should_not_exists(new_resource.mount_point)
|
@@ -206,6 +206,17 @@ EOM
|
|
206
206
|
result.error!
|
207
207
|
end
|
208
208
|
|
209
|
+
it "should not print SSL warnings when running in local-mode" do
|
210
|
+
file 'config/client.rb', <<EOM
|
211
|
+
chef_server_url 'http://omg.com/blah'
|
212
|
+
cookbook_path "#{path_to('cookbooks')}"
|
213
|
+
EOM
|
214
|
+
|
215
|
+
result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default' --local-mode", :cwd => chef_dir)
|
216
|
+
result.stdout.should_not include("SSL validation of HTTPS requests is disabled.")
|
217
|
+
result.error!
|
218
|
+
end
|
219
|
+
|
209
220
|
it "should complete with success when passed -z and --chef-zero-port" do
|
210
221
|
file 'config/client.rb', <<EOM
|
211
222
|
chef_server_url 'http://omg.com/blah'
|
@@ -50,6 +50,15 @@ describe 'knife common options' do
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
context 'And chef_zero.host is 0.0.0.0' do
|
54
|
+
before(:each) { Chef::Config.chef_zero.host = '0.0.0.0' }
|
55
|
+
|
56
|
+
it 'knife raw /nodes/x should retrieve the role' do
|
57
|
+
knife('raw /nodes/x').should_succeed /"name": "x"/
|
58
|
+
Chef::Config.chef_server_url.should == 'http://0.0.0.0:8889'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
53
62
|
context 'and there is a private key' do
|
54
63
|
file 'mykey.pem', <<EOM
|
55
64
|
-----BEGIN RSA PRIVATE KEY-----
|
@@ -39,6 +39,7 @@ describe Chef::Application do
|
|
39
39
|
@app = Chef::Application.new
|
40
40
|
@app.stub(:configure_chef).and_return(true)
|
41
41
|
@app.stub(:configure_logging).and_return(true)
|
42
|
+
@app.stub(:configure_proxy_environment_variables).and_return(true)
|
42
43
|
end
|
43
44
|
|
44
45
|
it "should configure chef" do
|
@@ -51,6 +52,10 @@ describe Chef::Application do
|
|
51
52
|
@app.reconfigure
|
52
53
|
end
|
53
54
|
|
55
|
+
it "should configure environment variables" do
|
56
|
+
@app.should_receive(:configure_proxy_environment_variables).and_return(true)
|
57
|
+
@app.reconfigure
|
58
|
+
end
|
54
59
|
end
|
55
60
|
|
56
61
|
describe Chef::Application do
|
@@ -235,6 +240,158 @@ describe Chef::Application do
|
|
235
240
|
end
|
236
241
|
end
|
237
242
|
|
243
|
+
describe "when configuring environment variables" do
|
244
|
+
def configure_proxy_environment_variables_stubs
|
245
|
+
@app.stub(:configure_http_proxy).and_return(true)
|
246
|
+
@app.stub(:configure_https_proxy).and_return(true)
|
247
|
+
@app.stub(:configure_ftp_proxy).and_return(true)
|
248
|
+
@app.stub(:configure_no_proxy).and_return(true)
|
249
|
+
end
|
250
|
+
|
251
|
+
shared_examples_for "setting ENV['http_proxy']" do
|
252
|
+
before do
|
253
|
+
Chef::Config[:http_proxy] = http_proxy
|
254
|
+
end
|
255
|
+
|
256
|
+
it "should set ENV['http_proxy']" do
|
257
|
+
@app.configure_proxy_environment_variables
|
258
|
+
@env['http_proxy'].should == "http://#{address}:#{port}"
|
259
|
+
end
|
260
|
+
|
261
|
+
describe "when Chef::Config[:http_proxy_user] is set" do
|
262
|
+
before do
|
263
|
+
Chef::Config[:http_proxy_user] = "username"
|
264
|
+
end
|
265
|
+
|
266
|
+
it "should set ENV['http_proxy'] with the username" do
|
267
|
+
@app.configure_proxy_environment_variables
|
268
|
+
@env['http_proxy'].should == "http://username@#{address}:#{port}"
|
269
|
+
end
|
270
|
+
|
271
|
+
context "when :http_proxy_user contains '@' and/or ':'" do
|
272
|
+
before do
|
273
|
+
Chef::Config[:http_proxy_user] = "my:usern@me"
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should set ENV['http_proxy'] with the escaped username" do
|
277
|
+
@app.configure_proxy_environment_variables
|
278
|
+
@env['http_proxy'].should == "http://my%3Ausern%40me@#{address}:#{port}"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
describe "when Chef::Config[:http_proxy_pass] is set" do
|
283
|
+
before do
|
284
|
+
Chef::Config[:http_proxy_pass] = "password"
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should set ENV['http_proxy'] with the password" do
|
288
|
+
@app.configure_proxy_environment_variables
|
289
|
+
@env['http_proxy'].should == "http://username:password@#{address}:#{port}"
|
290
|
+
end
|
291
|
+
|
292
|
+
context "when :http_proxy_pass contains '@' and/or ':'" do
|
293
|
+
before do
|
294
|
+
Chef::Config[:http_proxy_pass] = ":P@ssword101"
|
295
|
+
end
|
296
|
+
|
297
|
+
it "should set ENV['http_proxy'] with the escaped password" do
|
298
|
+
@app.configure_proxy_environment_variables
|
299
|
+
@env['http_proxy'].should == "http://username:%3AP%40ssword101@#{address}:#{port}"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
describe "when Chef::Config[:http_proxy_pass] is set (but not Chef::Config[:http_proxy_user])" do
|
306
|
+
before do
|
307
|
+
Chef::Config[:http_proxy_user] = nil
|
308
|
+
Chef::Config[:http_proxy_pass] = "password"
|
309
|
+
end
|
310
|
+
|
311
|
+
it "should set ENV['http_proxy']" do
|
312
|
+
@app.configure_proxy_environment_variables
|
313
|
+
@env['http_proxy'].should == "http://#{address}:#{port}"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
describe "when configuring ENV['http_proxy']" do
|
319
|
+
before do
|
320
|
+
@env = {}
|
321
|
+
@app.stub(:env).and_return(@env)
|
322
|
+
|
323
|
+
@app.stub(:configure_https_proxy).and_return(true)
|
324
|
+
@app.stub(:configure_ftp_proxy).and_return(true)
|
325
|
+
@app.stub(:configure_no_proxy).and_return(true)
|
326
|
+
end
|
327
|
+
|
328
|
+
describe "when Chef::Config[:http_proxy] is not set" do
|
329
|
+
before do
|
330
|
+
Chef::Config[:http_proxy] = nil
|
331
|
+
end
|
332
|
+
|
333
|
+
it "should not set ENV['http_proxy']" do
|
334
|
+
@app.configure_proxy_environment_variables
|
335
|
+
@env.should == {}
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
describe "when Chef::Config[:http_proxy] is set" do
|
340
|
+
context "when given an FQDN" do
|
341
|
+
let(:address) { "proxy.example.org" }
|
342
|
+
let(:port) { 8080 }
|
343
|
+
let(:http_proxy) { "http://#{address}:#{port}" }
|
344
|
+
|
345
|
+
it_should_behave_like "setting ENV['http_proxy']"
|
346
|
+
end
|
347
|
+
|
348
|
+
context "when given an IP" do
|
349
|
+
let(:address) { "127.0.0.1" }
|
350
|
+
let(:port) { 22 }
|
351
|
+
let(:http_proxy) { "http://#{address}:#{port}" }
|
352
|
+
|
353
|
+
it_should_behave_like "setting ENV['http_proxy']"
|
354
|
+
end
|
355
|
+
|
356
|
+
context "when given an IPv6" do
|
357
|
+
let(:address) { "[2001:db8::1]" }
|
358
|
+
let(:port) { 80 }
|
359
|
+
let(:http_proxy) { "http://#{address}:#{port}" }
|
360
|
+
|
361
|
+
it_should_behave_like "setting ENV['http_proxy']"
|
362
|
+
end
|
363
|
+
|
364
|
+
context "when given without including http://" do
|
365
|
+
let(:address) { "proxy.example.org" }
|
366
|
+
let(:port) { 8181 }
|
367
|
+
let(:http_proxy) { "#{address}:#{port}" }
|
368
|
+
|
369
|
+
it_should_behave_like "setting ENV['http_proxy']"
|
370
|
+
end
|
371
|
+
|
372
|
+
context "when given the full proxy in :http_proxy only" do
|
373
|
+
before do
|
374
|
+
Chef::Config[:http_proxy] = "http://username:password@proxy.example.org:2222"
|
375
|
+
Chef::Config[:http_proxy_user] = nil
|
376
|
+
Chef::Config[:http_proxy_pass] = nil
|
377
|
+
end
|
378
|
+
|
379
|
+
it "should set ENV['http_proxy']" do
|
380
|
+
@app.configure_proxy_environment_variables
|
381
|
+
@env['http_proxy'].should == Chef::Config[:http_proxy]
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
context "when the config options aren't URI compliant" do
|
386
|
+
it "raises Chef::Exceptions::BadProxyURI" do
|
387
|
+
Chef::Config[:http_proxy] = "http://proxy.bad_example.org/:8080"
|
388
|
+
expect { @app.configure_proxy_environment_variables }.to raise_error(Chef::Exceptions::BadProxyURI)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
238
395
|
describe "class method: fatal!" do
|
239
396
|
before do
|
240
397
|
STDERR.stub(:puts).with("FATAL: blah").and_return(true)
|