cf 0.1.5 → 0.6.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.
Files changed (140) hide show
  1. data/LICENSE +1277 -30
  2. data/Rakefile +12 -1
  3. data/bin/cf +0 -3
  4. data/lib/cf.rb +6 -0
  5. data/lib/cf/cli.rb +389 -190
  6. data/lib/cf/cli/app/app.rb +45 -0
  7. data/lib/cf/cli/app/apps.rb +99 -0
  8. data/lib/cf/cli/app/base.rb +90 -0
  9. data/lib/cf/cli/app/crashes.rb +42 -0
  10. data/lib/cf/cli/app/delete.rb +95 -0
  11. data/lib/cf/cli/app/deprecated.rb +11 -0
  12. data/lib/cf/cli/app/env.rb +78 -0
  13. data/lib/cf/cli/app/files.rb +137 -0
  14. data/lib/cf/cli/app/health.rb +26 -0
  15. data/lib/cf/cli/app/instances.rb +53 -0
  16. data/lib/cf/cli/app/logs.rb +76 -0
  17. data/lib/cf/cli/app/push.rb +105 -0
  18. data/lib/cf/cli/app/push/create.rb +149 -0
  19. data/lib/cf/cli/app/push/interactions.rb +94 -0
  20. data/lib/cf/cli/app/push/sync.rb +64 -0
  21. data/lib/cf/cli/app/rename.rb +35 -0
  22. data/lib/cf/cli/app/restart.rb +20 -0
  23. data/lib/cf/cli/app/scale.rb +69 -0
  24. data/lib/cf/cli/app/start.rb +143 -0
  25. data/lib/cf/cli/app/stats.rb +67 -0
  26. data/lib/cf/cli/app/stop.rb +27 -0
  27. data/lib/cf/cli/domain/base.rb +8 -0
  28. data/lib/cf/cli/domain/domains.rb +40 -0
  29. data/lib/cf/cli/domain/map.rb +55 -0
  30. data/lib/cf/cli/domain/unmap.rb +56 -0
  31. data/lib/cf/cli/help.rb +15 -0
  32. data/lib/cf/cli/interactive.rb +105 -0
  33. data/lib/cf/cli/organization/base.rb +12 -0
  34. data/lib/cf/cli/organization/create.rb +32 -0
  35. data/lib/cf/cli/organization/delete.rb +73 -0
  36. data/lib/cf/cli/organization/org.rb +45 -0
  37. data/lib/cf/cli/organization/orgs.rb +35 -0
  38. data/lib/cf/cli/organization/rename.rb +36 -0
  39. data/lib/cf/cli/route/base.rb +8 -0
  40. data/lib/cf/cli/route/map.rb +70 -0
  41. data/lib/cf/cli/route/routes.rb +26 -0
  42. data/lib/cf/cli/route/unmap.rb +62 -0
  43. data/lib/cf/cli/service/base.rb +8 -0
  44. data/lib/cf/cli/service/bind.rb +44 -0
  45. data/lib/cf/cli/service/create.rb +107 -0
  46. data/lib/cf/cli/service/delete.rb +82 -0
  47. data/lib/cf/cli/service/rename.rb +35 -0
  48. data/lib/cf/cli/service/service.rb +40 -0
  49. data/lib/cf/cli/service/services.rb +99 -0
  50. data/lib/cf/cli/service/unbind.rb +38 -0
  51. data/lib/cf/cli/space/base.rb +19 -0
  52. data/lib/cf/cli/space/create.rb +63 -0
  53. data/lib/cf/cli/space/delete.rb +95 -0
  54. data/lib/cf/cli/space/rename.rb +39 -0
  55. data/lib/cf/cli/space/space.rb +64 -0
  56. data/lib/cf/cli/space/spaces.rb +55 -0
  57. data/lib/cf/cli/space/switch.rb +16 -0
  58. data/lib/cf/cli/start/base.rb +93 -0
  59. data/lib/cf/cli/start/colors.rb +13 -0
  60. data/lib/cf/cli/start/info.rb +124 -0
  61. data/lib/cf/cli/start/login.rb +94 -0
  62. data/lib/cf/cli/start/logout.rb +17 -0
  63. data/lib/cf/cli/start/target.rb +69 -0
  64. data/lib/cf/cli/start/target_interactions.rb +37 -0
  65. data/lib/cf/cli/start/targets.rb +16 -0
  66. data/lib/cf/cli/user/base.rb +29 -0
  67. data/lib/cf/cli/user/create.rb +39 -0
  68. data/lib/cf/cli/user/passwd.rb +43 -0
  69. data/lib/cf/cli/user/register.rb +42 -0
  70. data/lib/cf/cli/user/users.rb +32 -0
  71. data/lib/cf/constants.rb +10 -7
  72. data/lib/cf/detect.rb +113 -48
  73. data/lib/cf/errors.rb +17 -0
  74. data/lib/cf/plugin.rb +28 -12
  75. data/lib/cf/spacing.rb +89 -0
  76. data/lib/cf/spec_helper.rb +1 -0
  77. data/lib/cf/test_support.rb +6 -0
  78. data/lib/cf/version.rb +1 -1
  79. data/spec/assets/hello-sinatra/Gemfile +3 -0
  80. data/spec/assets/hello-sinatra/Gemfile.lock +17 -0
  81. data/spec/assets/hello-sinatra/config.ru +3 -0
  82. data/spec/assets/hello-sinatra/fat-cat-makes-app-larger.png +0 -0
  83. data/spec/assets/hello-sinatra/main.rb +6 -0
  84. data/spec/assets/specker_runner/specker_runner_input.rb +6 -0
  85. data/spec/assets/specker_runner/specker_runner_pause.rb +5 -0
  86. data/spec/cf/cli/app/base_spec.rb +17 -0
  87. data/spec/cf/cli/app/delete_spec.rb +188 -0
  88. data/spec/cf/cli/app/instances_spec.rb +65 -0
  89. data/spec/cf/cli/app/push/create_spec.rb +661 -0
  90. data/spec/cf/cli/app/push_spec.rb +369 -0
  91. data/spec/cf/cli/app/rename_spec.rb +104 -0
  92. data/spec/cf/cli/app/scale_spec.rb +75 -0
  93. data/spec/cf/cli/app/start_spec.rb +208 -0
  94. data/spec/cf/cli/app/stats_spec.rb +68 -0
  95. data/spec/cf/cli/domain/map_spec.rb +130 -0
  96. data/spec/cf/cli/domain/unmap_spec.rb +69 -0
  97. data/spec/cf/cli/organization/orgs_spec.rb +108 -0
  98. data/spec/cf/cli/organization/rename_spec.rb +113 -0
  99. data/spec/cf/cli/route/map_spec.rb +121 -0
  100. data/spec/cf/cli/route/unmap_spec.rb +155 -0
  101. data/spec/cf/cli/service/bind_spec.rb +25 -0
  102. data/spec/cf/cli/service/delete_spec.rb +22 -0
  103. data/spec/cf/cli/service/rename_spec.rb +105 -0
  104. data/spec/cf/cli/service/service_spec.rb +23 -0
  105. data/spec/cf/cli/service/unbind_spec.rb +25 -0
  106. data/spec/cf/cli/space/create_spec.rb +93 -0
  107. data/spec/cf/cli/space/rename_spec.rb +102 -0
  108. data/spec/cf/cli/space/spaces_spec.rb +104 -0
  109. data/spec/cf/cli/space/switch_space_spec.rb +55 -0
  110. data/spec/cf/cli/start/info_spec.rb +160 -0
  111. data/spec/cf/cli/start/login_spec.rb +142 -0
  112. data/spec/cf/cli/start/logout_spec.rb +50 -0
  113. data/spec/cf/cli/start/target_spec.rb +123 -0
  114. data/spec/cf/cli/user/create_spec.rb +54 -0
  115. data/spec/cf/cli/user/passwd_spec.rb +102 -0
  116. data/spec/cf/cli/user/register_spec.rb +140 -0
  117. data/spec/cf/cli_spec.rb +442 -0
  118. data/spec/cf/detect_spec.rb +54 -0
  119. data/spec/console_app_specker/console_app_specker_matchers_spec.rb +173 -0
  120. data/spec/console_app_specker/specker_runner_spec.rb +167 -0
  121. data/spec/features/account_lifecycle_spec.rb +85 -0
  122. data/spec/features/login_spec.rb +66 -0
  123. data/spec/features/push_flow_spec.rb +125 -0
  124. data/spec/features/switching_targets_spec.rb +32 -0
  125. data/spec/spec_helper.rb +72 -0
  126. data/spec/support/command_helper.rb +81 -0
  127. data/spec/support/config_helper.rb +15 -0
  128. data/spec/support/console_app_specker_matchers.rb +86 -0
  129. data/spec/support/fake_home_dir.rb +55 -0
  130. data/spec/support/interact_helper.rb +29 -0
  131. data/spec/support/shared_examples/errors.rb +40 -0
  132. data/spec/support/shared_examples/input.rb +14 -0
  133. data/spec/support/specker_runner.rb +80 -0
  134. data/spec/support/tracking_expector.rb +71 -0
  135. metadata +427 -66
  136. data/lib/cf/cli/app.rb +0 -595
  137. data/lib/cf/cli/command.rb +0 -444
  138. data/lib/cf/cli/dots.rb +0 -133
  139. data/lib/cf/cli/service.rb +0 -112
  140. data/lib/cf/cli/user.rb +0 -71
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ if ENV['CF_V2_TEST_USER'] && ENV['CF_V2_TEST_PASSWORD'] && ENV['CF_V2_TEST_TARGET'] && ENV['CF_V2_OTHER_TEST_USER']
4
+ describe 'A user logs in and switches spaces, after a different user has logged in', :ruby19 => true do
5
+ include ConsoleAppSpeckerMatchers
6
+
7
+ let(:target) { ENV['CF_V2_TEST_TARGET'] }
8
+ let(:username) { ENV['CF_V2_TEST_USER'] }
9
+ let(:password) { ENV['CF_V2_TEST_PASSWORD'] }
10
+
11
+ let(:second_username) { ENV['CF_V2_OTHER_TEST_USER'] }
12
+ let(:second_organization) { ENV['CF_V2_OTHER_TEST_ORGANIZATION'] }
13
+ let(:second_space) { ENV['CF_V2_OTHER_TEST_SPACE'] }
14
+ let(:second_password) { ENV['CF_V2_OTHER_TEST_PASSWORD'] || ENV['CF_V2_TEST_PASSWORD'] }
15
+
16
+ before do
17
+ Interact::Progress::Dots.start!
18
+
19
+ run("#{cf_bin} target #{target}") do |runner|
20
+ expect(runner).to say "Setting target"
21
+ expect(runner).to say target
22
+ runner.wait_for_exit
23
+ end
24
+
25
+ run("#{cf_bin} logout") do |runner|
26
+ runner.wait_for_exit
27
+ end
28
+ end
29
+
30
+ after do
31
+ Interact::Progress::Dots.stop!
32
+ end
33
+
34
+ context "when a different user is already logged in" do
35
+ before do
36
+ run("#{cf_bin} login #{username} --password #{password}") do |runner|
37
+ expect(runner).to say "Authenticating... OK"
38
+ expect(runner).to say "Organization>"
39
+ runner.send_keys("pivotal")
40
+
41
+ expect(runner).to say "Switching to organization"
42
+ expect(runner).to say "OK"
43
+
44
+ expect(runner).to say "Space"
45
+ runner.send_keys("1")
46
+
47
+ expect(runner).to say "Switching to space"
48
+ expect(runner).to say "OK"
49
+
50
+ runner.wait_for_exit
51
+ end
52
+ end
53
+
54
+ it "can switch spaces on login" do
55
+ run("#{cf_bin} login #{second_username} --password #{second_password} --organization #{second_organization} --space #{second_space}") do |runner|
56
+ expect(runner).to say "Authenticating... OK"
57
+ expect(runner).to say "Switching to organization #{second_organization}... OK"
58
+ expect(runner).to say "Switching to space #{second_space}... OK"
59
+ runner.wait_for_exit
60
+ end
61
+ end
62
+ end
63
+ end
64
+ else
65
+ $stderr.puts 'Skipping v2 integration specs; please provide $CF_V2_TEST_TARGET, $CF_V2_TEST_USER, $CF_V2_TEST_PASSWORD, and $CF_V2_OTHER_TEST_USER'
66
+ end
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+ require "webmock/rspec"
3
+
4
+ if ENV['CF_V2_TEST_USER'] && ENV['CF_V2_TEST_PASSWORD'] && ENV['CF_V2_TEST_TARGET']
5
+ describe 'A new user tries to use CF against v2', :ruby19 => true do
6
+ include ConsoleAppSpeckerMatchers
7
+ include CF::Interactive
8
+
9
+ let(:target) { ENV['CF_V2_TEST_TARGET'] }
10
+ let(:username) { ENV['CF_V2_TEST_USER'] }
11
+ let(:password) { ENV['CF_V2_TEST_PASSWORD'] }
12
+
13
+ let(:app) do
14
+ fuzz = TRAVIS_BUILD_ID.to_s + Time.new.to_f.to_s.gsub(".", "_")
15
+ "hello-sinatra-#{fuzz}"
16
+ end
17
+
18
+ before do
19
+ FileUtils.rm_rf File.expand_path(CF::CONFIG_DIR)
20
+ WebMock.allow_net_connect!
21
+ Interact::Progress::Dots.start!
22
+ end
23
+
24
+ after do
25
+ cf %W(delete #{app} -f --no-script)
26
+ Interact::Progress::Dots.stop!
27
+ end
28
+
29
+ it 'pushes a simple sinatra app using defaults as much as possible' do
30
+ run("#{cf_bin} target http://#{target}") do |runner|
31
+ expect(runner).to say %r{Setting target to http://#{target}... OK}
32
+ end
33
+
34
+ run("#{cf_bin} login") do |runner|
35
+ expect(runner).to say %r{target: https?://#{target}}
36
+
37
+ expect(runner).to say "Email>"
38
+ runner.send_keys username
39
+
40
+ expect(runner).to say "Password>"
41
+ runner.send_keys password
42
+
43
+ expect(runner).to say "Authenticating... OK"
44
+
45
+ expect(runner).to say(
46
+ "Organization>" => proc {
47
+ runner.send_keys "1"
48
+ expect(runner).to say /Switching to organization .*\.\.\. OK/
49
+ },
50
+ "Switching to organization" => proc {}
51
+ )
52
+
53
+ expect(runner).to say(
54
+ "Space>" => proc {
55
+ runner.send_keys "1"
56
+ expect(runner).to say /Switching to space .*\.\.\. OK/
57
+ },
58
+ "Switching to space" => proc {}
59
+ )
60
+ end
61
+
62
+ run("#{cf_bin} app #{app}") do |runner|
63
+ expect(runner).to say "Unknown app '#{app}'."
64
+ end
65
+
66
+ Dir.chdir("#{SPEC_ROOT}/assets/hello-sinatra") do
67
+ run("#{cf_bin} push") do |runner|
68
+ expect(runner).to say "Name>"
69
+ runner.send_keys app
70
+
71
+ expect(runner).to say "Instances> 1"
72
+ runner.send_keys ""
73
+
74
+ expect(runner).to say "Custom startup command> "
75
+ runner.send_keys "bundle exec ruby main.rb -p $PORT"
76
+
77
+ expect(runner).to say "Memory Limit>"
78
+ runner.send_keys "64M"
79
+
80
+ expect(runner).to say "Creating #{app}... OK"
81
+
82
+ expect(runner).to say "Subdomain> #{app}"
83
+ runner.send_keys ""
84
+
85
+ expect(runner).to say "1:"
86
+ expect(runner).to say "Domain>"
87
+ runner.send_keys "1"
88
+
89
+ expect(runner).to say(/Creating route #{app}\..*\.\.\. OK/)
90
+ expect(runner).to say(/Binding #{app}\..* to #{app}\.\.\. OK/)
91
+
92
+ expect(runner).to say "Create services for application?> n"
93
+ runner.send_keys ""
94
+
95
+ # skip this
96
+ if runner.expect "Bind other services to application?> n", 1
97
+ runner.send_keys ""
98
+ end
99
+
100
+ expect(runner).to say "Save configuration?> n"
101
+ runner.send_keys ""
102
+
103
+ expect(runner).to say "Uploading #{app}... OK"
104
+ expect(runner).to say "Starting #{app}... OK"
105
+
106
+ expect(runner).to say /(Using|Installing) Ruby/i, 10
107
+ expect(runner).to say "Your bundle is complete!", 30
108
+
109
+ expect(runner).to say "Checking #{app}..."
110
+ expect(runner).to say "1/1 instances"
111
+ expect(runner).to say "OK", 30
112
+ end
113
+ end
114
+
115
+ run("#{cf_bin} delete #{app}") do |runner|
116
+ expect(runner).to say "Really delete #{app}?>"
117
+ runner.send_keys "y"
118
+
119
+ expect(runner).to say "Deleting #{app}... OK"
120
+ end
121
+ end
122
+ end
123
+ else
124
+ $stderr.puts 'Skipping v2 integration specs; please provide $CF_V2_TEST_TARGET, $CF_V2_TEST_USER, and $CF_V2_TEST_PASSWORD'
125
+ end
@@ -0,0 +1,32 @@
1
+ require "spec_helper"
2
+
3
+ if ENV['CF_V2_TEST_TARGET']
4
+ describe 'A new user tries to use CF against v2 production', :ruby19 => true do
5
+ include ConsoleAppSpeckerMatchers
6
+
7
+ let(:target) { ENV['CF_V2_TEST_TARGET'] }
8
+
9
+ before do
10
+ Interact::Progress::Dots.start!
11
+ end
12
+
13
+ after do
14
+ Interact::Progress::Dots.stop!
15
+ end
16
+
17
+ it "can switch targets, even if a target is invalid" do
18
+ run("#{cf_bin} target invalid-target") do |runner|
19
+ expect(runner).to say "Target refused"
20
+ runner.wait_for_exit
21
+ end
22
+
23
+ run("#{cf_bin} target #{target}") do |runner|
24
+ expect(runner).to say "Setting target"
25
+ expect(runner).to say target
26
+ runner.wait_for_exit
27
+ end
28
+ end
29
+ end
30
+ else
31
+ $stderr.puts 'Skipping v2 integration specs; please provide $CF_V2_TEST_TARGET'
32
+ end
@@ -0,0 +1,72 @@
1
+ SPEC_ROOT = File.dirname(__FILE__).freeze
2
+
3
+ require "rspec"
4
+ require "cfoundry"
5
+ require "cfoundry/test_support"
6
+ require "cf"
7
+ require "cf/test_support"
8
+ require "webmock"
9
+ require "ostruct"
10
+ require "fakefs/safe"
11
+
12
+ INTEGRATE_WITH = ENV["INTEGRATE_WITH"] || "default"
13
+ TRAVIS_BUILD_ID = ENV["TRAVIS_BUILD_ID"]
14
+
15
+ OriginalFile = File
16
+
17
+ class FakeFS::File
18
+ def self.fnmatch(*args, &blk)
19
+ OriginalFile.fnmatch(*args, &blk)
20
+ end
21
+ end
22
+
23
+ def cf_bin
24
+ cf = File.expand_path("#{SPEC_ROOT}/../bin/cf.dev")
25
+ if INTEGRATE_WITH != 'default'
26
+ "rvm #{INTEGRATE_WITH}@cf do #{cf}"
27
+ else
28
+ cf
29
+ end
30
+ end
31
+
32
+ Dir[File.expand_path('../support/**/*.rb', __FILE__)].each do |file|
33
+ require file
34
+ end
35
+
36
+ RSpec.configure do |c|
37
+ c.include Fake::FakeMethods
38
+ c.include ConsoleAppSpeckerMatchers
39
+
40
+ c.mock_with :rr
41
+
42
+ if RUBY_VERSION =~ /^1\.8\.\d/
43
+ c.filter_run_excluding :ruby19 => true
44
+ end
45
+
46
+ c.include FakeHomeDir
47
+ c.include CommandHelper
48
+ c.include InteractHelper
49
+ c.include ConfigHelper
50
+
51
+ c.before(:all) do
52
+ WebMock.disable_net_connect!
53
+ end
54
+
55
+ c.before do
56
+ CF::CLI.send(:class_variable_set, :@@client, nil)
57
+ end
58
+ end
59
+
60
+ def name_list(xs)
61
+ if xs.empty?
62
+ "none"
63
+ else
64
+ xs.collect(&:name).join(", ")
65
+ end
66
+ end
67
+
68
+ def run(command)
69
+ SpeckerRunner.new(command) do |runner|
70
+ yield runner
71
+ end
72
+ end
@@ -0,0 +1,81 @@
1
+ def command(klass, &specs)
2
+ describe klass do
3
+ let(:stub_precondition?) { true }
4
+
5
+ before do
6
+ any_instance_of klass do |cli|
7
+ stub(cli).precondition if stub_precondition?
8
+ stub(cli).client { client }
9
+ end
10
+ end
11
+
12
+ before(:all) do
13
+ klass.class_eval do
14
+ def wrap_errors
15
+ yield
16
+ rescue CF::UserError => e
17
+ err e.message
18
+ end
19
+ end
20
+ end
21
+
22
+ after(:all) do
23
+ klass.class_eval do
24
+ remove_method :wrap_errors
25
+ end
26
+ end
27
+
28
+ class_eval(&specs)
29
+ end
30
+ end
31
+
32
+ module CommandHelper
33
+ def cf(argv)
34
+ Mothership.new.exit_status 0
35
+ stub(CF::CLI).exit { |code| code }
36
+ capture_output { CF::CLI.start(argv + ["--debug", "--no-script"]) }
37
+ end
38
+
39
+ def bool_flag(flag)
40
+ "#{'no-' unless send(flag)}#{flag.to_s.gsub('_', '-')}"
41
+ end
42
+
43
+ attr_reader :stdout, :stderr, :stdin, :status
44
+
45
+ def capture_output
46
+ $real_stdout = $stdout
47
+ $real_stderr = $stderr
48
+ $real_stdin = $stdin
49
+ $stdout = @stdout = StringIO.new
50
+ $stderr = @stderr = StringIO.new
51
+ $stdin = @stdin = StringIO.new
52
+ @status = yield
53
+ @stdout.rewind
54
+ @stderr.rewind
55
+ @status
56
+ ensure
57
+ $stdout = $real_stdout
58
+ $stderr = $real_stderr
59
+ $stdin = $real_stdin
60
+ end
61
+
62
+ def output
63
+ @output ||= TrackingExpector.new(stdout)
64
+ end
65
+
66
+ def error_output
67
+ @error_output ||= TrackingExpector.new(stderr)
68
+ end
69
+
70
+ def mock_invoke(*args)
71
+ any_instance_of described_class do |cli|
72
+ mock(cli).invoke *args
73
+ end
74
+ end
75
+
76
+ def dont_allow_invoke(*args)
77
+ any_instance_of described_class do |cli|
78
+ dont_allow(cli).invoke *args
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ module ConfigHelper
2
+ def write_token_file(config={})
3
+ File.open(File.expand_path(tokens_file_path), 'w') do |f|
4
+ f.puts YAML.dump(
5
+ { "https://api.some-domain.com" =>
6
+ {
7
+ :version => 2,
8
+ :token => 'bearer token',
9
+ :refresh_token => nil
10
+ }.merge(config)
11
+ }
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,86 @@
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.respond_to?(:expect)
14
+ @matched = runner.expect(@expected_output, @timeout)
15
+ @full_output = runner.output
16
+ !!@matched
17
+ end
18
+
19
+ def failure_message
20
+ if @expected_output.is_a?(Hash)
21
+ expected_keys = @expected_output.keys.map{|key| "'#{key}'"}.join(', ')
22
+ "expected one of #{expected_keys} to be printed, but it wasn't. full output:\n#@full_output"
23
+ else
24
+ "expected '#{@expected_output}' to be printed, but it wasn't. full output:\n#@full_output"
25
+ end
26
+ end
27
+
28
+ def negative_failure_message
29
+ if @expected_output.is_a?(Hash)
30
+ match = @matched
31
+ else
32
+ match = @expected_output
33
+ end
34
+
35
+ "expected '#{match}' to not be printed, but it was. full output:\n#@full_output"
36
+ end
37
+ end
38
+
39
+
40
+ class ExitCodeMatcher
41
+ def initialize(expected_code)
42
+ @expected_code = expected_code
43
+ end
44
+
45
+ def matches?(runner)
46
+ raise InvalidInputError unless runner.respond_to?(:exit_code)
47
+
48
+ begin
49
+ Timeout.timeout(5) do
50
+ @actual_code = runner.exit_code
51
+ end
52
+
53
+ @actual_code == @expected_code
54
+ rescue Timeout::Error
55
+ @timed_out = true
56
+ false
57
+ end
58
+ end
59
+
60
+ def failure_message
61
+ if @timed_out
62
+ "expected process to exit with status #@expected_code, but it did not exit within 5 seconds"
63
+ else
64
+ "expected process to exit with status #{@expected_code}, but it exited with status #{@actual_code}"
65
+ end
66
+ end
67
+
68
+ def negative_failure_message
69
+ if @timed_out
70
+ "expected process to exit with status #@expected_code, but it did not exit within 5 seconds"
71
+ else
72
+ "expected process to not exit with status #{@expected_code}, but it did"
73
+ end
74
+ end
75
+ end
76
+
77
+ def say(expected_output, timeout = 30)
78
+ ExpectOutputMatcher.new(expected_output, timeout)
79
+ end
80
+
81
+ def have_exited_with(expected_code)
82
+ ExitCodeMatcher.new(expected_code)
83
+ end
84
+
85
+ alias :exit_with :have_exited_with
86
+ end