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.
- data/lib/vmc/cli.rb +62 -24
- data/lib/vmc/cli/app/push.rb +23 -2
- data/lib/vmc/cli/app/push/create.rb +14 -7
- data/lib/vmc/cli/app/push/interactions.rb +10 -3
- data/lib/vmc/cli/app/push/sync.rb +4 -2
- data/lib/vmc/cli/app/stats.rb +1 -1
- data/lib/vmc/cli/route/map.rb +22 -16
- data/lib/vmc/cli/service/create.rb +19 -4
- data/lib/vmc/cli/start/info.rb +4 -0
- data/lib/vmc/cli/start/login.rb +4 -0
- data/lib/vmc/cli/start/logout.rb +4 -0
- data/lib/vmc/cli/user/base.rb +1 -1
- data/lib/vmc/test_support/command_helper.rb +7 -13
- data/lib/vmc/version.rb +1 -1
- data/spec/assets/specker_runner/specker_runner_input.rb +6 -0
- data/spec/assets/specker_runner/specker_runner_pause.rb +5 -0
- data/spec/console_app_specker/console_app_specker_matchers_spec.rb +152 -0
- data/spec/console_app_specker/specker_runner_spec.rb +157 -0
- data/spec/features/new_user_flow_spec.rb +83 -45
- data/spec/spec_helper.rb +16 -0
- data/spec/support/console_app_specker_matchers.rb +75 -0
- data/spec/support/specker_runner.rb +123 -0
- data/spec/vmc/cli/app/push/create_spec.rb +82 -21
- data/spec/vmc/cli/app/push_spec.rb +49 -4
- data/spec/vmc/cli/app/stats_spec.rb +1 -1
- data/spec/vmc/cli/route/map_spec.rb +38 -42
- data/spec/vmc/cli/start/info_spec.rb +21 -1
- data/spec/vmc/cli/start/login_spec.rb +36 -10
- data/spec/vmc/cli/start/logout_spec.rb +63 -0
- data/spec/vmc/cli/user/passwd_spec.rb +1 -1
- data/spec/vmc/cli/user/register_spec.rb +1 -1
- data/spec/vmc/cli_spec.rb +243 -33
- metadata +68 -36
data/spec/spec_helper.rb
CHANGED
@@ -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
|
-
|
32
|
+
command = Mothership.commands[:push]
|
33
|
+
create = VMC::App::Push.new(command)
|
33
34
|
create.path = "some-path"
|
34
|
-
create.input = Mothership::Inputs.new(
|
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(:
|
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
|
-
|
311
|
-
|
338
|
+
stub_ask("Domain", anything) { domains.first }
|
339
|
+
|
340
|
+
stub(create).invoke
|
341
|
+
|
342
|
+
subject
|
312
343
|
end
|
313
344
|
|
314
|
-
|
345
|
+
it "asks for a domain with 'none' as an option" do
|
346
|
+
stub_ask("Subdomain", anything) { hosts.first }
|
315
347
|
|
316
|
-
|
317
|
-
|
318
|
-
expect(options[:
|
319
|
-
|
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
|
-
|
354
|
+
stub(create).invoke
|
324
355
|
|
325
356
|
subject
|
326
357
|
end
|
327
358
|
|
328
|
-
|
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
|
-
|
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('
|
397
|
+
mock_ask('Subdomain', anything) { "foo" }
|
398
|
+
mock_ask('Domain', anything) { domains.first }
|
341
399
|
|
342
|
-
mock(create).invoke(:map,
|
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('
|
409
|
+
mock_ask('Subdomain', anything) { hosts.first }
|
410
|
+
mock_ask('Domain', anything) { domains.first }
|
351
411
|
|
352
|
-
stub(create).invoke
|
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('
|
421
|
+
stub_ask('Subdomain', anything) { hosts.first }
|
422
|
+
stub_ask('Domain', anything) { domains.first }
|
362
423
|
|
363
|
-
stub(create).invoke
|
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(:
|
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
|
-
:
|
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, :
|
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
|