vmc 0.5.0.beta.12 → 0.5.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,7 @@ require "cfoundry"
5
5
  require "cfoundry/test_support"
6
6
  require "vmc"
7
7
  require "vmc/test_support"
8
+ require "webmock"
8
9
 
9
10
  Dir[File.expand_path('../support/**/*.rb', __FILE__)].each do |file|
10
11
  require file
@@ -12,12 +13,21 @@ end
12
13
 
13
14
  RSpec.configure do |c|
14
15
  c.include Fake::FakeMethods
16
+ c.include V1Fake::FakeMethods
15
17
  c.mock_with :rr
16
18
 
19
+ if RUBY_VERSION =~ /^1\.8\.\d/
20
+ c.filter_run_excluding :ruby19 => true
21
+ end
22
+
17
23
  c.include VMC::TestSupport::FakeHomeDir
18
24
  c.include VMC::TestSupport::CommandHelper
19
25
  c.include VMC::TestSupport::InteractHelper
20
26
 
27
+ c.before(:all) do
28
+ WebMock.disable_net_connect!
29
+ end
30
+
21
31
  c.before do
22
32
  VMC::CLI.send(:class_variable_set, :@@client, nil)
23
33
  end
@@ -55,3 +65,9 @@ def stub_output(cli)
55
65
  stub(Interact::Progress::Dots).start!
56
66
  stub(Interact::Progress::Dots).stop!
57
67
  end
68
+
69
+ def run(command)
70
+ SpeckerRunner.new(command) do |runner|
71
+ yield runner
72
+ end
73
+ end
@@ -0,0 +1,75 @@
1
+ module ConsoleAppSpeckerMatchers
2
+ class InvalidInputError < StandardError; end
3
+
4
+ class ExpectOutputMatcher
5
+ attr_reader :timeout
6
+
7
+ def initialize(expected_output, timeout = 30)
8
+ @expected_output = expected_output
9
+ @timeout = timeout
10
+ end
11
+
12
+ def matches?(runner)
13
+ raise InvalidInputError unless runner.is_a?(SpeckerRunner)
14
+ expected = runner.expect(@expected_output, @timeout)
15
+ @full_output = runner.output
16
+ !!expected
17
+ end
18
+
19
+ def failure_message
20
+ "expected '#{@expected_output}' to be printed, but it wasn't. full output:\n#@full_output"
21
+ end
22
+
23
+ def negative_failure_message
24
+ "expected '#{@expected_output}' to not be printed, but it was. full output:\n#@full_output"
25
+ end
26
+ end
27
+
28
+
29
+ class ExitCodeMatcher
30
+ def initialize(expected_code)
31
+ @expected_code = expected_code
32
+ end
33
+
34
+ def matches?(runner)
35
+ raise InvalidInputError unless runner.is_a?(SpeckerRunner)
36
+
37
+ begin
38
+ Timeout.timeout(5) do
39
+ @actual_code = runner.exit_code
40
+ end
41
+
42
+ @actual_code == @expected_code
43
+ rescue Timeout::Error
44
+ @timed_out = true
45
+ false
46
+ end
47
+ end
48
+
49
+ def failure_message
50
+ if @timed_out
51
+ "expected process to exit with status #@expected_code, but it did not exit within 5 seconds"
52
+ else
53
+ "expected process to exit with status #{@expected_code}, but it exited with status #{@actual_code}"
54
+ end
55
+ end
56
+
57
+ def negative_failure_message
58
+ if @timed_out
59
+ "expected process to exit with status #@expected_code, but it did not exit within 5 seconds"
60
+ else
61
+ "expected process to not exit with status #{@expected_code}, but it did"
62
+ end
63
+ end
64
+ end
65
+
66
+ def say(expected_output, timeout = 30)
67
+ ExpectOutputMatcher.new(expected_output, timeout)
68
+ end
69
+
70
+ def have_exited_with(expected_code)
71
+ ExitCodeMatcher.new(expected_code)
72
+ end
73
+
74
+ alias :exit_with :have_exited_with
75
+ end
@@ -0,0 +1,123 @@
1
+ require "expect"
2
+ require "pty"
3
+
4
+ class SpeckerRunner
5
+ attr_reader :output
6
+
7
+ def initialize(*args)
8
+ @output = ""
9
+
10
+ @stdout, slave = PTY.open
11
+ system("stty raw", :in => slave)
12
+ read, @stdin = IO.pipe
13
+
14
+ @pid = spawn(*(args.push(:in => read, :out => slave, :err => slave)))
15
+
16
+ yield self
17
+ end
18
+
19
+ def expect(matcher, timeout = 30)
20
+ case matcher
21
+ when Hash
22
+ expect_branches(matcher, timeout)
23
+ else
24
+ tracking_expect(matcher, timeout)
25
+ end
26
+ end
27
+
28
+ def send_keys(text_to_send)
29
+ @stdin.puts(text_to_send)
30
+ end
31
+
32
+ def exit_code
33
+ return @status if @status
34
+
35
+ status = nil
36
+ Timeout.timeout(5) do
37
+ _, status = Process.waitpid2(@pid)
38
+ end
39
+
40
+ @status = numeric_exit_code(status)
41
+ end
42
+
43
+ alias_method :wait_for_exit, :exit_code
44
+
45
+ def exited?
46
+ !running?
47
+ end
48
+
49
+ def running?
50
+ !!Process.getpgid(@pid)
51
+ end
52
+
53
+ private
54
+
55
+ def expect_branches(branches, timeout)
56
+ branch_names = /#{branches.keys.collect { |k| Regexp.quote(k) }.join("|")}/
57
+ expected = @stdout.expect(branch_names, timeout)
58
+ return unless expected
59
+
60
+ data = expected.first.match(/(#{branch_names})$/)
61
+ matched = data[1]
62
+ branches[matched].call
63
+ end
64
+
65
+ def numeric_exit_code(status)
66
+ status.exitstatus
67
+ rescue NoMethodError
68
+ status
69
+ end
70
+
71
+ def tracking_expect(pattern, timeout)
72
+ buffer = ''
73
+
74
+ case pattern
75
+ when String
76
+ pattern = Regexp.new(Regexp.quote(pattern))
77
+ when Regexp
78
+ else
79
+ raise TypeError, "unsupported pattern class: #{pattern.class}"
80
+ end
81
+
82
+ result = nil
83
+ position = 0
84
+ @unused ||= ""
85
+
86
+ while true
87
+ if !@unused.empty?
88
+ c = @unused.slice!(0).chr
89
+ elsif !IO.select([@stdout], nil, nil, timeout) || @stdout.eof?
90
+ @unused = buffer
91
+ break
92
+ else
93
+ c = @stdout.getc.chr
94
+ end
95
+
96
+ # wear your flip flops
97
+ unless (c == "\e") .. (c == "m")
98
+ if c == "\b"
99
+ if position > 0 && buffer[position - 1] && buffer[position - 1].chr != "\n"
100
+ position -= 1
101
+ end
102
+ else
103
+ if buffer.size > position
104
+ buffer[position] = c
105
+ else
106
+ buffer << c
107
+ end
108
+
109
+ position += 1
110
+ end
111
+ end
112
+
113
+ if matches = pattern.match(buffer)
114
+ result = [buffer, *matches.to_a[1..-1]]
115
+ break
116
+ end
117
+ end
118
+
119
+ @output << buffer
120
+
121
+ result
122
+ end
123
+ end
@@ -29,9 +29,10 @@ describe VMC::App::Create do
29
29
  end
30
30
 
31
31
  let(:create) do
32
- create = VMC::App::Push.new
32
+ command = Mothership.commands[:push]
33
+ create = VMC::App::Push.new(command)
33
34
  create.path = "some-path"
34
- create.input = Mothership::Inputs.new(Mothership.commands[:push], create, inputs, given, global)
35
+ create.input = Mothership::Inputs.new(command, create, inputs, given, global)
35
36
  create.extend VMC::App::PushInteractions
36
37
  create
37
38
  end
@@ -301,33 +302,89 @@ describe VMC::App::Create do
301
302
  expect(app.send(key)).to eq val
302
303
  end
303
304
  end
305
+
306
+ context "with an invalid buildpack" do
307
+ before do
308
+ stub(app).create! do
309
+ raise CFoundry::MessageParseError.new(
310
+ "Request invalid due to parse error: Field: buildpack, Error: Value git@github.com:cloudfoundry/heroku-buildpack-ruby.git doesn't match regexp String /GIT_URL_REGEX/",
311
+ 1001)
312
+ end
313
+ end
314
+
315
+ it "fails and prints a pretty message" do
316
+ stub(create).line(anything)
317
+ expect { subject }.to raise_error(
318
+ VMC::UserError, "Buildpack must be a public git repository URI.")
319
+ end
320
+ end
304
321
  end
305
322
 
306
323
  describe '#map_url' do
307
- let(:app) { fake(:app) }
308
- let(:url_choices) { %W(#{app.name}.foo-cloud.com) }
324
+ let(:app) { fake(:app, :space => space) }
325
+ let(:space) { fake(:space, :domains => domains) }
326
+ let(:domains) { [fake(:domain, :name => "foo.com")] }
327
+ let(:hosts) { [app.name] }
328
+
329
+ subject { create.map_route(app) }
330
+
331
+ it "asks for a subdomain with 'none' as an option" do
332
+ mock_ask('Subdomain', anything) do |_, options|
333
+ expect(options[:choices]).to eq(hosts + %w(none))
334
+ expect(options[:default]).to eq hosts.first
335
+ hosts.first
336
+ end
309
337
 
310
- before do
311
- stub(create).url_choices { url_choices }
338
+ stub_ask("Domain", anything) { domains.first }
339
+
340
+ stub(create).invoke
341
+
342
+ subject
312
343
  end
313
344
 
314
- subject { create.map_url(app) }
345
+ it "asks for a domain with 'none' as an option" do
346
+ stub_ask("Subdomain", anything) { hosts.first }
315
347
 
316
- it "maps a url" do
317
- mock_ask('URL', anything) do |_, options|
318
- expect(options[:choices]).to eq(url_choices + %w(none))
319
- expect(options[:default]).to eq url_choices.first
320
- url_choices.first
348
+ mock_ask('Domain', anything) do |_, options|
349
+ expect(options[:choices]).to eq(domains + %w(none))
350
+ expect(options[:default]).to eq domains.first
351
+ domains.first
321
352
  end
322
353
 
323
- mock(create).invoke(:map, :app => app, :url => url_choices.first)
354
+ stub(create).invoke
324
355
 
325
356
  subject
326
357
  end
327
358
 
328
- context "when 'none' is given" do
359
+ it "maps the host and domain after both are given" do
360
+ stub_ask('Subdomain', anything) { hosts.first }
361
+ stub_ask('Domain', anything) { domains.first }
362
+
363
+ mock(create).invoke(:map,
364
+ :app => app, :host => hosts.first,
365
+ :domain => domains.first)
366
+
367
+ subject
368
+ end
369
+
370
+ context "when 'none' is given as the host" do
371
+ context "and a domain is provided afterwards" do
372
+ it "invokes 'map' with an empty host" do
373
+ mock_ask('Subdomain', anything) { "none" }
374
+ stub_ask('Domain', anything) { domains.first }
375
+
376
+ mock(create).invoke(:map,
377
+ :host => "", :domain => domains.first, :app => app)
378
+
379
+ subject
380
+ end
381
+ end
382
+ end
383
+
384
+ context "when 'none' is given as the domain" do
329
385
  it "does not perform any mapping" do
330
- mock_ask('URL', anything) { "none" }
386
+ stub_ask('Subdomain', anything) { "foo" }
387
+ mock_ask('Domain', anything) { "none" }
331
388
 
332
389
  dont_allow(create).invoke(:map, anything)
333
390
 
@@ -337,9 +394,11 @@ describe VMC::App::Create do
337
394
 
338
395
  context "when mapping fails" do
339
396
  before do
340
- mock_ask('URL', anything) { url_choices.first }
397
+ mock_ask('Subdomain', anything) { "foo" }
398
+ mock_ask('Domain', anything) { domains.first }
341
399
 
342
- mock(create).invoke(:map, :app => app, :url => url_choices.first) do
400
+ mock(create).invoke(:map,
401
+ :host => "foo", :domain => domains.first, :app => app) do
343
402
  raise CFoundry::RouteHostTaken.new("foo", 1234)
344
403
  end
345
404
  end
@@ -347,9 +406,10 @@ describe VMC::App::Create do
347
406
  it "asks again" do
348
407
  stub(create).line
349
408
 
350
- mock_ask('URL', anything) { url_choices.first }
409
+ mock_ask('Subdomain', anything) { hosts.first }
410
+ mock_ask('Domain', anything) { domains.first }
351
411
 
352
- stub(create).invoke(:map, :app => app, :url => url_choices.first)
412
+ stub(create).invoke
353
413
 
354
414
  subject
355
415
  end
@@ -358,9 +418,10 @@ describe VMC::App::Create do
358
418
  mock(create).line "foo"
359
419
  mock(create).line
360
420
 
361
- stub_ask('URL', anything) { url_choices.first }
421
+ stub_ask('Subdomain', anything) { hosts.first }
422
+ stub_ask('Domain', anything) { domains.first }
362
423
 
363
- stub(create).invoke(:map, :app => app, :url => url_choices.first)
424
+ stub(create).invoke
364
425
 
365
426
  subject
366
427
  end
@@ -284,20 +284,65 @@ describe VMC::App::Push do
284
284
  end
285
285
  end
286
286
  end
287
+
288
+ context "when buildpack is given" do
289
+ let(:old) { nil }
290
+ let(:app) { fake(:app, :buildpack => old) }
291
+ let(:inputs) { { :buildpack => new } }
292
+
293
+ context "and it's an invalid URL" do
294
+ let(:new) { "git@github.com:foo/bar.git" }
295
+
296
+ before do
297
+ stub(app).update! do
298
+ raise CFoundry::MessageParseError.new(
299
+ "Request invalid due to parse error: Field: buildpack, Error: Value git@github.com:cloudfoundry/heroku-buildpack-ruby.git doesn't match regexp String /GIT_URL_REGEX/",
300
+ 1001)
301
+ end
302
+ end
303
+
304
+ it "fails and prints a pretty message" do
305
+ stub(push).line(anything)
306
+ expect { subject }.to raise_error(
307
+ VMC::UserError, "Buildpack must be a public git repository URI.")
308
+ end
309
+ end
310
+
311
+ context "and it's a valid URL" do
312
+ let(:new) { "git://github.com/foo/bar.git" }
313
+
314
+ it "updates the app's buildpack" do
315
+ stub(push).line(anything)
316
+ mock(app).update!
317
+ expect { subject }.to change { app.buildpack }.from(old).to(new)
318
+ end
319
+
320
+ it "outputs the changed buildpack with single quotes" do
321
+ mock(push).line("Changes:")
322
+ mock(push).line("buildpack: '' -> '#{new}'")
323
+ stub(app).update!
324
+ subject
325
+ end
326
+
327
+ include_examples 'common tests for inputs', :buildpack
328
+ end
329
+ end
287
330
  end
288
331
 
289
332
  describe '#setup_new_app (integration spec!!)' do
290
333
  let(:app) { fake(:app, :guid => nil) }
291
334
  let(:framework) { fake(:framework) }
292
335
  let(:runtime) { fake(:runtime) }
293
- let(:url) { "https://www.foobar.com" }
336
+ let(:host) { "" }
337
+ let(:domain) { fake(:domain, :name => "example.com") }
294
338
  let(:inputs) do
295
339
  { :name => "some-app",
296
340
  :instances => 2,
297
341
  :framework => framework,
298
342
  :runtime => runtime,
299
343
  :memory => 1024,
300
- :url => url
344
+ :host => host,
345
+ :domain => domain
301
346
  }
302
347
  end
303
348
  let(:global) { {:quiet => true, :color => false, :force => true} }
@@ -316,9 +361,9 @@ describe VMC::App::Push do
316
361
  mock(app).upload(path)
317
362
  mock(push).filter(:create_app, app) { app }
318
363
  mock(push).filter(:push_app, app) { app }
319
- mock(push).invoke :map, :app => app, :url => url
364
+ mock(push).invoke :map, :app => app, :host => host, :domain => domain
320
365
  mock(push).invoke :start, :app => app
321
366
  subject
322
367
  end
323
368
  end
324
- end
369
+ end