workable-pact-provider-verifier 1.24.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +296 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +143 -0
- data/bin/pact-provider-verifier +3 -0
- data/lib/pact/provider_verifier.rb +2 -0
- data/lib/pact/provider_verifier/add_header_middlware.rb +34 -0
- data/lib/pact/provider_verifier/aggregate_pact_configs.rb +51 -0
- data/lib/pact/provider_verifier/app.rb +229 -0
- data/lib/pact/provider_verifier/cli/custom_thor.rb +71 -0
- data/lib/pact/provider_verifier/cli/verify.rb +80 -0
- data/lib/pact/provider_verifier/custom_middleware.rb +33 -0
- data/lib/pact/provider_verifier/pact_helper.rb +25 -0
- data/lib/pact/provider_verifier/provider_states/add_provider_states_header.rb +36 -0
- data/lib/pact/provider_verifier/provider_states/remove_provider_states_header_middleware.rb +19 -0
- data/lib/pact/provider_verifier/rspec_json_formatter_monkeypatch.rb +34 -0
- data/lib/pact/provider_verifier/set_up_provider_state.rb +112 -0
- data/lib/pact/provider_verifier/underscored_headers_monkeypatch.rb +96 -0
- data/lib/pact/provider_verifier/version.rb +5 -0
- data/lib/pact/wait_until_server_available.rb +24 -0
- metadata +324 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pact
|
2
|
+
module ProviderVerifier
|
3
|
+
class AddHeaderMiddlware
|
4
|
+
|
5
|
+
def initialize app, headers
|
6
|
+
@app = app
|
7
|
+
@headers = headers
|
8
|
+
end
|
9
|
+
|
10
|
+
def call env
|
11
|
+
@app.call(add_headers(env))
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_headers env
|
15
|
+
new_env = env.dup
|
16
|
+
@headers.each_pair do | key, value |
|
17
|
+
header_name = "HTTP_#{key.upcase.gsub("-", "_")}"
|
18
|
+
warn_about_header_replacement key, new_env[header_name], value
|
19
|
+
new_env[header_name] = value
|
20
|
+
end
|
21
|
+
new_env
|
22
|
+
end
|
23
|
+
|
24
|
+
def warn_about_header_replacement header_name, existing_value, new_value
|
25
|
+
if existing_value.nil?
|
26
|
+
$stderr.puts "WARN: Adding header '#{header_name}: #{new_value}' to replayed request. This header did not exist in the pact, and hence this test cannot confirm that the request will work in real life."
|
27
|
+
else
|
28
|
+
# Don't mess up the json formatter by using stdout here
|
29
|
+
$stderr.puts "INFO: Replacing header '#{header_name}: #{existing_value}' with '#{header_name}: #{new_value}'"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'pact/pact_broker'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'pact/provider/pact_uri'
|
4
|
+
|
5
|
+
module Pact
|
6
|
+
module ProviderVerifier
|
7
|
+
class AggregatePactConfigs
|
8
|
+
|
9
|
+
def self.call(pact_urls, provider_name, consumer_version_tags, provider_version_tags, pact_broker_base_url, http_client_options)
|
10
|
+
new(pact_urls, provider_name, consumer_version_tags, provider_version_tags, pact_broker_base_url, http_client_options).call
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(pact_urls, provider_name, consumer_version_tags, provider_version_tags, pact_broker_base_url, http_client_options)
|
14
|
+
@pact_urls = pact_urls
|
15
|
+
@provider_name = provider_name
|
16
|
+
@consumer_version_tags = consumer_version_tags
|
17
|
+
@provider_version_tags = provider_version_tags
|
18
|
+
@pact_broker_base_url = pact_broker_base_url
|
19
|
+
@http_client_options = http_client_options
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
pacts_urls_from_broker + specified_pact_uris
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :pact_urls, :provider_name, :consumer_version_tags, :provider_version_tags, :pact_broker_base_url, :http_client_options
|
29
|
+
|
30
|
+
def specified_pact_uris
|
31
|
+
pact_urls.collect{ | url | Pact::PactBroker.build_pact_uri(url) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def pacts_urls_from_broker
|
35
|
+
if pact_broker_base_url && provider_name
|
36
|
+
pacts_for_verification
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def pacts_for_verification
|
43
|
+
@pacts_for_verification ||= Pact::PactBroker.fetch_pact_uris_for_verification(provider_name, consumer_version_selectors, provider_version_tags, pact_broker_base_url, http_client_options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def consumer_version_selectors
|
47
|
+
consumer_version_tags.collect{ |tag| { tag: tag, latest: true } }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'pact/wait_until_server_available'
|
2
|
+
require 'pact/provider_verifier/add_header_middlware'
|
3
|
+
require 'pact/provider_verifier/provider_states/add_provider_states_header'
|
4
|
+
require 'pact/provider_verifier/provider_states/remove_provider_states_header_middleware'
|
5
|
+
require 'pact/provider_verifier/custom_middleware'
|
6
|
+
require 'pact/provider/rspec'
|
7
|
+
require 'pact/message'
|
8
|
+
require 'pact/cli/run_pact_verification'
|
9
|
+
require 'pact/provider_verifier/aggregate_pact_configs'
|
10
|
+
require 'rack/reverse_proxy'
|
11
|
+
require 'faraday_middleware'
|
12
|
+
require 'json'
|
13
|
+
|
14
|
+
module Pact
|
15
|
+
module ProviderVerifier
|
16
|
+
class App
|
17
|
+
include Pact::WaitUntilServerAvailable
|
18
|
+
|
19
|
+
PROXY_PACT_HELPER = File.expand_path(File.join(File.dirname(__FILE__), "pact_helper.rb"))
|
20
|
+
attr_reader :pact_urls, :options, :consumer_version_tags, :provider_version_tags
|
21
|
+
|
22
|
+
def initialize pact_urls, options = {}
|
23
|
+
@pact_urls = pact_urls
|
24
|
+
@options = options
|
25
|
+
@consumer_version_tags = options[:consumer_version_tag] || []
|
26
|
+
@provider_version_tags = options[:provider_version_tag] || []
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.call pact_urls, options
|
30
|
+
new(pact_urls, options).call
|
31
|
+
end
|
32
|
+
|
33
|
+
def call
|
34
|
+
setup
|
35
|
+
|
36
|
+
pact_urls = all_pact_urls
|
37
|
+
wait_until_provider_available
|
38
|
+
exit_statuses = pact_urls.collect do |pact_uri|
|
39
|
+
verify_pact pact_uri
|
40
|
+
end
|
41
|
+
|
42
|
+
exit_statuses.all?{ | status | status == 0 }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
|
48
|
+
def setup
|
49
|
+
print_deprecation_note
|
50
|
+
set_environment_variables
|
51
|
+
require_rspec_monkeypatch_for_jsonl
|
52
|
+
require_pact_project_pact_helper # Beth: not sure if this is needed, hangover from pact-provider-proxy?
|
53
|
+
set_broker_token_env_var
|
54
|
+
end
|
55
|
+
|
56
|
+
def set_environment_variables
|
57
|
+
ENV['PROVIDER_STATES_SETUP_URL'] = options.provider_states_setup_url
|
58
|
+
ENV['VERBOSE_LOGGING'] = options.verbose if options.verbose
|
59
|
+
ENV['CUSTOM_PROVIDER_HEADER'] = custom_provider_headers_for_env_var if custom_provider_headers_for_env_var
|
60
|
+
ENV['MONKEYPATCH'] = options.monkeypatch.join("\n") if options.monkeypatch && options.monkeypatch.any?
|
61
|
+
end
|
62
|
+
|
63
|
+
def configure_service_provider
|
64
|
+
# Have to declare these locally as the class scope gets lost within the block
|
65
|
+
application = configure_reverse_proxy
|
66
|
+
application = configure_provider_states_header_removal_middleware(application)
|
67
|
+
application = configure_custom_middleware(application)
|
68
|
+
application = configure_custom_header_middleware(application)
|
69
|
+
|
70
|
+
this = self
|
71
|
+
|
72
|
+
Pact.service_provider "Running Provider Application" do
|
73
|
+
app do
|
74
|
+
application
|
75
|
+
end
|
76
|
+
|
77
|
+
if this.options.provider_app_version
|
78
|
+
app_version this.options.provider_app_version
|
79
|
+
end
|
80
|
+
|
81
|
+
if this.provider_version_tags.any?
|
82
|
+
app_version_tags this.provider_version_tags
|
83
|
+
end
|
84
|
+
|
85
|
+
publish_verification_results this.options.publish_verification_results
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def configure_reverse_proxy
|
90
|
+
provider_base_url = options.provider_base_url
|
91
|
+
Rack::ReverseProxy.new do
|
92
|
+
reverse_proxy_options(
|
93
|
+
verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
94
|
+
preserve_host: true,
|
95
|
+
x_forwarded_headers: false
|
96
|
+
)
|
97
|
+
reverse_proxy %r{(.*)}, "#{provider_base_url}$1"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def configure_custom_header_middleware rack_reverse_proxy
|
102
|
+
if options.custom_provider_header
|
103
|
+
Pact::ProviderVerifier::AddHeaderMiddlware.new(rack_reverse_proxy, parse_header)
|
104
|
+
else
|
105
|
+
rack_reverse_proxy
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def configure_custom_middleware app
|
110
|
+
if options.custom_middleware && options.custom_middleware.any?
|
111
|
+
require_custom_middlware
|
112
|
+
apply_custom_middleware(app)
|
113
|
+
else
|
114
|
+
app
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def configure_provider_states_header_removal_middleware app
|
119
|
+
ProviderStates::RemoveProviderStatesHeaderMiddleware.new(app)
|
120
|
+
end
|
121
|
+
|
122
|
+
def require_custom_middlware
|
123
|
+
options.custom_middleware.each do |file|
|
124
|
+
$stdout.puts "DEBUG: Requiring custom middleware file #{file}" if options.verbose
|
125
|
+
begin
|
126
|
+
require file
|
127
|
+
rescue LoadError => e
|
128
|
+
$stderr.puts "ERROR: #{e.class} - #{e.message}. Please specify an absolute path."
|
129
|
+
exit(1)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def apply_custom_middleware app
|
135
|
+
CustomMiddleware.descendants.inject(app) do | app, clazz |
|
136
|
+
Pact.configuration.output_stream.puts "INFO: Adding custom middleware #{clazz}"
|
137
|
+
clazz.new(app)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def verify_pact(pact_uri)
|
142
|
+
begin
|
143
|
+
verify_options = {
|
144
|
+
pact_helper: PROXY_PACT_HELPER,
|
145
|
+
pact_uri: pact_uri,
|
146
|
+
backtrace: ENV['BACKTRACE'] == 'true',
|
147
|
+
pact_broker_username: options.broker_username,
|
148
|
+
pact_broker_password: options.broker_password,
|
149
|
+
pact_broker_token: options.broker_token,
|
150
|
+
format: options.format,
|
151
|
+
out: options.out,
|
152
|
+
request_customizer: ProviderStates::AddProviderStatesHeader
|
153
|
+
}
|
154
|
+
verify_options[:description] = ENV['PACT_DESCRIPTION'] if ENV['PACT_DESCRIPTION']
|
155
|
+
verify_options[:provider_state] = ENV['PACT_PROVIDER_STATE'] if ENV['PACT_PROVIDER_STATE']
|
156
|
+
|
157
|
+
reset_pact_configuration
|
158
|
+
# Really, this should call the PactSpecRunner directly, rather than using the CLI class.
|
159
|
+
Cli::RunPactVerification.call(verify_options)
|
160
|
+
rescue SystemExit => e
|
161
|
+
e.status
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def reset_pact_configuration
|
166
|
+
require 'pact/configuration'
|
167
|
+
require 'pact/consumer/world'
|
168
|
+
require 'pact/provider/world'
|
169
|
+
Pact.clear_configuration
|
170
|
+
Pact.clear_consumer_world
|
171
|
+
Pact.clear_provider_world
|
172
|
+
configure_service_provider
|
173
|
+
end
|
174
|
+
|
175
|
+
def all_pact_urls
|
176
|
+
http_client_options = { username: options.broker_username, password: options.broker_password, token: options.broker_token, verbose: options.verbose }
|
177
|
+
AggregatePactConfigs.call(pact_urls, options.provider, consumer_version_tags, provider_version_tags, options.pact_broker_base_url, http_client_options)
|
178
|
+
end
|
179
|
+
|
180
|
+
def require_pact_project_pact_helper
|
181
|
+
require ENV['PACT_PROJECT_PACT_HELPER'] if ENV.fetch('PACT_PROJECT_PACT_HELPER','') != ''
|
182
|
+
end
|
183
|
+
|
184
|
+
def require_rspec_monkeypatch_for_jsonl
|
185
|
+
if options.format == 'json'
|
186
|
+
require 'pact/provider_verifier/rspec_json_formatter_monkeypatch'
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def custom_provider_headers_for_env_var
|
191
|
+
if options.custom_provider_header && options.custom_provider_header.any?
|
192
|
+
options.custom_provider_header.join("\n")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def parse_header
|
197
|
+
options.custom_provider_header.each_with_object({}) do | custom_provider_header, header_hash |
|
198
|
+
header_name, header_value = custom_provider_header.split(":", 2).collect(&:strip)
|
199
|
+
header_hash[header_name] = header_value
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def print_deprecation_note
|
204
|
+
if options.provider_states_url
|
205
|
+
$stderr.puts "WARN: The --provider-states-url option is deprecated and the URL endpoint can be removed from the application"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def wait_until_provider_available
|
210
|
+
if options.wait && options.wait != 0
|
211
|
+
uri = URI(options.provider_base_url)
|
212
|
+
$stderr.puts "INFO: Polling for up to #{options.wait} seconds for provider to become available at #{uri.host}:#{uri.port}..."
|
213
|
+
up = wait_until_server_available(uri.host, uri.port, options.wait)
|
214
|
+
if up
|
215
|
+
$stderr.puts "INFO: Provider available, proceeding with verifications"
|
216
|
+
else
|
217
|
+
$stderr.puts "WARN: Provider does not appear to be up on #{uri.host}:#{uri.port}... proceeding with verifications anyway"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def set_broker_token_env_var
|
223
|
+
if options.broker_token && !ENV['PACT_BROKER_TOKEN']
|
224
|
+
ENV['PACT_BROKER_TOKEN'] = options.broker_token
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Pact
|
4
|
+
module ProviderVerifier
|
5
|
+
module CLI
|
6
|
+
##
|
7
|
+
# Custom Thor task allows the following:
|
8
|
+
#
|
9
|
+
# `script arg1 arg2` to be interpreted as `script <default_task> arg1 arg2`
|
10
|
+
# `--option 1 --option 2` to be interpreted as `--option 1 2` (the standard Thor format for multiple value options)
|
11
|
+
# `script --help` to display the help for the default task instead of the command list
|
12
|
+
#
|
13
|
+
class CustomThor < ::Thor
|
14
|
+
|
15
|
+
no_commands do
|
16
|
+
def self.start given_args = ARGV, config = {}
|
17
|
+
super(massage_args(given_args))
|
18
|
+
end
|
19
|
+
|
20
|
+
def help *args
|
21
|
+
if args.empty?
|
22
|
+
super(self.class.default_task)
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.massage_args argv
|
29
|
+
prepend_default_task_name(turn_muliple_tag_options_into_array(argv))
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.prepend_default_task_name argv
|
33
|
+
if known_first_arguments.include?(argv[0])
|
34
|
+
argv
|
35
|
+
else
|
36
|
+
[default_command] + argv
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# other task names, help, and the help shortcuts
|
41
|
+
def self.known_first_arguments
|
42
|
+
@known_first_arguments ||= tasks.keys + ::Thor::HELP_MAPPINGS + ['help']
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.turn_muliple_tag_options_into_array argv
|
46
|
+
new_argv = []
|
47
|
+
opt_name = nil
|
48
|
+
argv.each_with_index do | arg, i |
|
49
|
+
if arg.start_with?('-')
|
50
|
+
opt_name = arg
|
51
|
+
existing = new_argv.find { | a | a.first == opt_name }
|
52
|
+
if !existing
|
53
|
+
new_argv << [arg]
|
54
|
+
end
|
55
|
+
else
|
56
|
+
if opt_name
|
57
|
+
existing = new_argv.find { | a | a.first == opt_name }
|
58
|
+
existing << arg
|
59
|
+
opt_name = nil
|
60
|
+
else
|
61
|
+
new_argv << [arg]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
new_argv.flatten
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'socket'
|
3
|
+
require 'pact/provider_verifier/app'
|
4
|
+
require 'pact/provider_verifier/cli/custom_thor'
|
5
|
+
|
6
|
+
module Pact
|
7
|
+
module ProviderVerifier
|
8
|
+
module CLI
|
9
|
+
class Verify < CustomThor
|
10
|
+
|
11
|
+
class InvalidArgumentsError < ::Thor::Error; end
|
12
|
+
|
13
|
+
desc 'PACT_URL ...', "Verify pact(s) against a provider. Supports local and networked (http-based) files."
|
14
|
+
method_option :provider_base_url, aliases: "-h", desc: "Provider host URL", :required => true
|
15
|
+
method_option :provider_states_setup_url, aliases: "-c", desc: "Base URL to setup the provider states at", :required => false
|
16
|
+
method_option :pact_broker_base_url, desc: "Base URL of the Pact Broker from which to retrieve the pacts.", :required => false
|
17
|
+
method_option :broker_username, aliases: "-n", desc: "Pact Broker basic auth username", :required => false
|
18
|
+
method_option :broker_password, aliases: "-p", desc: "Pact Broker basic auth password", :required => false
|
19
|
+
method_option :broker_token, aliases: "-k", desc: "Pact Broker bearer token", :required => false
|
20
|
+
method_option :provider, required: false
|
21
|
+
method_option :consumer_version_tag, type: :array, banner: "TAG", desc: "Retrieve the latest pacts with this consumer version tag. Used in conjunction with --provider. May be specified multiple times.", :required => false
|
22
|
+
method_option :provider_version_tag, type: :array, banner: "TAG", desc: "Tag to apply to the provider application version. May be specified multiple times.", :required => false
|
23
|
+
method_option :provider_app_version, aliases: "-a", desc: "Provider application version, required when publishing verification results", :required => false
|
24
|
+
method_option :publish_verification_results, aliases: "-r", desc: "Publish verification results to the broker", required: false, type: :boolean, default: false
|
25
|
+
method_option :custom_provider_header, type: :array, banner: 'CUSTOM_PROVIDER_HEADER', desc: "Header to add to provider state set up and pact verification requests. eg 'Authorization: Basic cGFjdDpwYWN0'. May be specified multiple times.", :required => false
|
26
|
+
method_option :custom_middleware, type: :array, banner: 'FILE', desc: "Ruby file containing a class implementing Pact::ProviderVerifier::CustomMiddleware. This allows the response to be modified before replaying. Use with caution!", :required => false
|
27
|
+
method_option :monkeypatch, hide: true, type: :array, :required => false
|
28
|
+
method_option :verbose, aliases: "-v", desc: "Verbose output", :required => false
|
29
|
+
method_option :provider_states_url, aliases: "-s", :required => false, hide: true
|
30
|
+
method_option :format, banner: "FORMATTER", aliases: "-f", desc: "RSpec formatter. Defaults to custom Pact formatter. Other options are json and RspecJunitFormatter (which outputs xml)."
|
31
|
+
method_option :out, aliases: "-o", banner: "FILE", desc: "Write output to a file instead of $stdout."
|
32
|
+
method_option :ignore_failures, type: :boolean, default: false, desc: "If specified, process will always exit with exit code 0", hide: true
|
33
|
+
method_option :pact_urls, aliases: "-u", hide: true, :required => false
|
34
|
+
method_option :wait, banner: "SECONDS", required: false, type: :numeric, desc: "The number of seconds to poll for the provider to become available before running the verification", default: 0
|
35
|
+
|
36
|
+
def verify(*pact_urls)
|
37
|
+
validate_verify
|
38
|
+
print_deprecation_warnings
|
39
|
+
success = Pact::ProviderVerifier::App.call(merged_urls(pact_urls), options)
|
40
|
+
exit_with_non_zero_status if !success && !options.ignore_failures
|
41
|
+
end
|
42
|
+
|
43
|
+
default_task :verify
|
44
|
+
|
45
|
+
desc 'version', 'Show the pact-provider-verifier gem version'
|
46
|
+
def version
|
47
|
+
require 'pact/provider_verifier/version'
|
48
|
+
puts Pact::ProviderVerifier::VERSION
|
49
|
+
end
|
50
|
+
|
51
|
+
no_commands do
|
52
|
+
def merged_urls pact_urls_from_args
|
53
|
+
from_opts = options.pact_urls ? options.pact_urls.split(',') : []
|
54
|
+
from_opts + pact_urls_from_args
|
55
|
+
end
|
56
|
+
|
57
|
+
def print_deprecation_warnings
|
58
|
+
if options.pact_urls
|
59
|
+
$stderr.puts "WARN: The --pact-urls option is deprecated. Please pass in a space separated list of URLs as the first arguments to the pact-provider-verifier command."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_verify
|
64
|
+
if options.pact_broker_base_url && (options.provider.nil? || options.provider == "")
|
65
|
+
raise InvalidArgumentsError, "No value provided for required option '--provider'"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def exit_with_non_zero_status
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def exit_on_failure?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|