vmc 0.5.0.beta.12 → 0.5.0.rc1

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.
@@ -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