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