test-kitchen 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +1 -1
  3. data/.rubocop.yml +3 -0
  4. data/.travis.yml +20 -9
  5. data/CHANGELOG.md +219 -108
  6. data/Gemfile +10 -6
  7. data/Guardfile +38 -9
  8. data/README.md +11 -1
  9. data/Rakefile +21 -37
  10. data/bin/kitchen +4 -4
  11. data/features/kitchen_action_commands.feature +161 -0
  12. data/features/kitchen_console_command.feature +34 -0
  13. data/features/kitchen_diagnose_command.feature +64 -0
  14. data/features/kitchen_init_command.feature +29 -17
  15. data/features/kitchen_list_command.feature +2 -2
  16. data/features/kitchen_login_command.feature +56 -0
  17. data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
  18. data/features/kitchen_test_command.feature +88 -0
  19. data/features/step_definitions/gem_steps.rb +8 -6
  20. data/features/step_definitions/git_steps.rb +4 -2
  21. data/features/step_definitions/output_steps.rb +5 -0
  22. data/features/support/env.rb +12 -9
  23. data/lib/kitchen.rb +60 -38
  24. data/lib/kitchen/base64_stream.rb +55 -0
  25. data/lib/kitchen/busser.rb +124 -58
  26. data/lib/kitchen/cli.rb +121 -38
  27. data/lib/kitchen/collection.rb +3 -3
  28. data/lib/kitchen/color.rb +4 -4
  29. data/lib/kitchen/command.rb +78 -11
  30. data/lib/kitchen/command/action.rb +3 -2
  31. data/lib/kitchen/command/console.rb +12 -5
  32. data/lib/kitchen/command/diagnose.rb +17 -3
  33. data/lib/kitchen/command/driver_discover.rb +26 -7
  34. data/lib/kitchen/command/exec.rb +41 -0
  35. data/lib/kitchen/command/list.rb +44 -14
  36. data/lib/kitchen/command/login.rb +2 -1
  37. data/lib/kitchen/command/sink.rb +2 -1
  38. data/lib/kitchen/command/test.rb +5 -4
  39. data/lib/kitchen/config.rb +146 -14
  40. data/lib/kitchen/configurable.rb +314 -0
  41. data/lib/kitchen/data_munger.rb +522 -18
  42. data/lib/kitchen/diagnostic.rb +43 -4
  43. data/lib/kitchen/driver.rb +4 -4
  44. data/lib/kitchen/driver/base.rb +80 -115
  45. data/lib/kitchen/driver/dummy.rb +34 -6
  46. data/lib/kitchen/driver/proxy.rb +14 -3
  47. data/lib/kitchen/driver/ssh_base.rb +61 -7
  48. data/lib/kitchen/errors.rb +109 -9
  49. data/lib/kitchen/generator/driver_create.rb +39 -5
  50. data/lib/kitchen/generator/init.rb +130 -45
  51. data/lib/kitchen/instance.rb +162 -28
  52. data/lib/kitchen/lazy_hash.rb +79 -7
  53. data/lib/kitchen/loader/yaml.rb +159 -27
  54. data/lib/kitchen/logger.rb +267 -21
  55. data/lib/kitchen/logging.rb +30 -3
  56. data/lib/kitchen/login_command.rb +11 -2
  57. data/lib/kitchen/metadata_chopper.rb +2 -2
  58. data/lib/kitchen/provisioner.rb +4 -4
  59. data/lib/kitchen/provisioner/base.rb +107 -103
  60. data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
  61. data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
  62. data/lib/kitchen/provisioner/chef_base.rb +206 -167
  63. data/lib/kitchen/provisioner/chef_solo.rb +25 -7
  64. data/lib/kitchen/provisioner/chef_zero.rb +105 -29
  65. data/lib/kitchen/provisioner/dummy.rb +1 -1
  66. data/lib/kitchen/provisioner/shell.rb +21 -6
  67. data/lib/kitchen/rake_tasks.rb +8 -3
  68. data/lib/kitchen/shell_out.rb +15 -18
  69. data/lib/kitchen/ssh.rb +122 -27
  70. data/lib/kitchen/state_file.rb +24 -7
  71. data/lib/kitchen/thor_tasks.rb +9 -4
  72. data/lib/kitchen/util.rb +43 -118
  73. data/lib/kitchen/version.rb +1 -1
  74. data/lib/vendor/hash_recursive_merge.rb +10 -2
  75. data/spec/kitchen/base64_stream_spec.rb +77 -0
  76. data/spec/kitchen/busser_spec.rb +490 -0
  77. data/spec/kitchen/collection_spec.rb +10 -10
  78. data/spec/kitchen/color_spec.rb +2 -2
  79. data/spec/kitchen/config_spec.rb +234 -62
  80. data/spec/kitchen/configurable_spec.rb +490 -0
  81. data/spec/kitchen/data_munger_spec.rb +1070 -862
  82. data/spec/kitchen/diagnostic_spec.rb +79 -0
  83. data/spec/kitchen/driver/base_spec.rb +80 -85
  84. data/spec/kitchen/driver/dummy_spec.rb +43 -14
  85. data/spec/kitchen/driver/proxy_spec.rb +134 -0
  86. data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
  87. data/spec/kitchen/driver_spec.rb +15 -15
  88. data/spec/kitchen/errors_spec.rb +309 -0
  89. data/spec/kitchen/instance_spec.rb +143 -46
  90. data/spec/kitchen/lazy_hash_spec.rb +36 -9
  91. data/spec/kitchen/loader/yaml_spec.rb +237 -226
  92. data/spec/kitchen/logger_spec.rb +419 -0
  93. data/spec/kitchen/logging_spec.rb +59 -0
  94. data/spec/kitchen/login_command_spec.rb +49 -0
  95. data/spec/kitchen/metadata_chopper_spec.rb +82 -0
  96. data/spec/kitchen/platform_spec.rb +4 -4
  97. data/spec/kitchen/provisioner/base_spec.rb +65 -125
  98. data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
  99. data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
  100. data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
  101. data/spec/kitchen/provisioner/shell_spec.rb +269 -0
  102. data/spec/kitchen/provisioner_spec.rb +6 -6
  103. data/spec/kitchen/shell_out_spec.rb +143 -0
  104. data/spec/kitchen/ssh_spec.rb +683 -0
  105. data/spec/kitchen/state_file_spec.rb +28 -21
  106. data/spec/kitchen/suite_spec.rb +7 -7
  107. data/spec/kitchen/util_spec.rb +68 -10
  108. data/spec/kitchen_spec.rb +107 -0
  109. data/spec/spec_helper.rb +18 -13
  110. data/support/chef-client-zero.rb +10 -9
  111. data/support/chef_helpers.sh +16 -0
  112. data/support/download_helpers.sh +109 -0
  113. data/test-kitchen.gemspec +42 -33
  114. metadata +107 -33
@@ -16,7 +16,8 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'kitchen/util'
19
+ require "kitchen/util"
20
+ require "kitchen/version"
20
21
 
21
22
  module Kitchen
22
23
 
@@ -26,12 +27,23 @@ module Kitchen
26
27
  # @author Fletcher Nichol <fnichol@nichol.ca>
27
28
  class Diagnostic
28
29
 
30
+ # Constructs a new Diagnostic object with an optional loader and optional
31
+ # instances array.
32
+ #
33
+ # @param options [Hash] optional configuration
34
+ # @option options [#diagnose,Hash] :loader a loader instance that responds
35
+ # to `#diagnose` or an error Hash
36
+ # @option options [Array<#diagnose>,Hash] :instances an Array of instances
37
+ # that respond to `#diagnose` or an error Hash
29
38
  def initialize(options = {})
30
39
  @loader = options.fetch(:loader, nil)
31
40
  @instances = options.fetch(:instances, [])
32
41
  @result = Hash.new
33
42
  end
34
43
 
44
+ # Returns a Hash with stringified keys containing diagnostic information.
45
+ #
46
+ # @return [Hash] a configuration Hash
35
47
  def read
36
48
  prepare_common
37
49
  prepare_loader
@@ -42,13 +54,31 @@ module Kitchen
42
54
 
43
55
  private
44
56
 
45
- attr_reader :result, :loader, :instances
57
+ # @return [Hash] a result hash
58
+ # @api private
59
+ attr_reader :result
46
60
 
61
+ # @return [#diagnose,Hash] a loader instance that responds to `#diagnose`
62
+ # or an error Hash
63
+ # @api private
64
+ attr_reader :loader
65
+
66
+ # @return [Array<#diagnose>,Hash] an Array of instances that respond to
67
+ # `#diagnose` or an error Hash
68
+ # @api private
69
+ attr_reader :instances
70
+
71
+ # Adds common information to the result Hash.
72
+ #
73
+ # @api private
47
74
  def prepare_common
48
- result[:timestamp] = Time.now.gmtime
75
+ result[:timestamp] = Time.now.gmtime.to_s
49
76
  result[:kitchen_version] = Kitchen::VERSION
50
77
  end
51
78
 
79
+ # Adds loader information to the result Hash.
80
+ #
81
+ # @api private
52
82
  def prepare_loader
53
83
  if error_hash?(loader)
54
84
  result[:loader] = loader
@@ -57,6 +87,9 @@ module Kitchen
57
87
  end
58
88
  end
59
89
 
90
+ # Adds instance information to the result Hash.
91
+ #
92
+ # @api private
60
93
  def prepare_instances
61
94
  result[:instances] = Hash.new
62
95
  if error_hash?(instances)
@@ -66,8 +99,14 @@ module Kitchen
66
99
  end
67
100
  end
68
101
 
102
+ # Determins whether or not the object is an error hash. An error hash is
103
+ # defined as a Hash containing an `:error` key.
104
+ #
105
+ # @param obj [Object] an object
106
+ # @return [true,false] whether or not the object is an error hash
107
+ # @api private
69
108
  def error_hash?(obj)
70
- obj.is_a?(Hash) && obj.has_key?(:error)
109
+ obj.is_a?(Hash) && obj.key?(:error)
71
110
  end
72
111
  end
73
112
  end
@@ -16,7 +16,7 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'thor/util'
19
+ require "thor/util"
20
20
 
21
21
  module Kitchen
22
22
 
@@ -40,7 +40,7 @@ module Kitchen
40
40
  first_load = require("kitchen/driver/#{plugin}")
41
41
 
42
42
  str_const = Thor::Util.camel_case(plugin)
43
- klass = self.const_get(str_const)
43
+ klass = const_get(str_const)
44
44
  object = klass.new(config)
45
45
  object.verify_dependencies if first_load
46
46
  object
@@ -48,8 +48,8 @@ module Kitchen
48
48
  raise
49
49
  rescue LoadError, NameError
50
50
  raise ClientError,
51
- "Could not load the '#{plugin}' driver from the load path." +
52
- " Please ensure that your driver is installed as a gem or included" +
51
+ "Could not load the '#{plugin}' driver from the load path." \
52
+ " Please ensure that your driver is installed as a gem or included" \
53
53
  " in your Gemfile if using Bundler."
54
54
  end
55
55
  end
@@ -16,9 +16,9 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'thor/util'
19
+ require "thor/util"
20
20
 
21
- require 'kitchen/lazy_hash'
21
+ require "kitchen/lazy_hash"
22
22
 
23
23
  module Kitchen
24
24
 
@@ -30,78 +30,58 @@ module Kitchen
30
30
  class Base
31
31
 
32
32
  include ShellOut
33
+ include Configurable
33
34
  include Logging
34
35
 
35
- attr_accessor :instance
36
-
37
- class << self
38
- attr_reader :serial_actions
39
- end
40
-
36
+ # Creates a new Driver object using the provided configuration data
37
+ # which will be merged with any default configuration.
38
+ #
39
+ # @param config [Hash] provided driver configuration
41
40
  def initialize(config = {})
42
- @config = LazyHash.new(config, self)
43
- self.class.defaults.each do |attr, value|
44
- @config[attr] = value unless @config.has_key?(attr)
45
- end
46
- end
47
-
48
- def validate_config!
49
- Array(self.class.validations).each do |tuple|
50
- tuple.last.call(tuple.first, config[tuple.first], self)
51
- end
41
+ init_config(config)
52
42
  end
53
43
 
54
44
  # Returns the name of this driver, suitable for display in a CLI.
55
45
  #
56
46
  # @return [String] name of this driver
57
47
  def name
58
- self.class.name.split('::').last
59
- end
60
-
61
- # Provides hash-like access to configuration keys.
62
- #
63
- # @param attr [Object] configuration key
64
- # @return [Object] value at configuration key
65
- def [](attr)
66
- config[attr]
67
- end
68
-
69
- # Returns an array of configuration keys.
70
- #
71
- # @return [Array] array of configuration keys
72
- def config_keys
73
- config.keys
48
+ self.class.name.split("::").last
74
49
  end
75
50
 
76
51
  # Creates an instance.
77
52
  #
78
53
  # @param state [Hash] mutable instance and driver state
79
54
  # @raise [ActionFailed] if the action could not be completed
80
- def create(state) ; end
55
+ def create(state) # rubocop:disable Lint/UnusedMethodArgument
56
+ end
81
57
 
82
58
  # Converges a running instance.
83
59
  #
84
60
  # @param state [Hash] mutable instance and driver state
85
61
  # @raise [ActionFailed] if the action could not be completed
86
- def converge(state) ; end
62
+ def converge(state) # rubocop:disable Lint/UnusedMethodArgument
63
+ end
87
64
 
88
65
  # Sets up an instance.
89
66
  #
90
67
  # @param state [Hash] mutable instance and driver state
91
68
  # @raise [ActionFailed] if the action could not be completed
92
- def setup(state) ; end
69
+ def setup(state) # rubocop:disable Lint/UnusedMethodArgument
70
+ end
93
71
 
94
72
  # Verifies a converged instance.
95
73
  #
96
74
  # @param state [Hash] mutable instance and driver state
97
75
  # @raise [ActionFailed] if the action could not be completed
98
- def verify(state) ; end
76
+ def verify(state) # rubocop:disable Lint/UnusedMethodArgument
77
+ end
99
78
 
100
79
  # Destroys an instance.
101
80
  #
102
81
  # @param state [Hash] mutable instance and driver state
103
82
  # @raise [ActionFailed] if the action could not be completed
104
- def destroy(state) ; end
83
+ def destroy(state) # rubocop:disable Lint/UnusedMethodArgument
84
+ end
105
85
 
106
86
  # Returns the shell command that will log into an instance.
107
87
  #
@@ -109,7 +89,7 @@ module Kitchen
109
89
  # @return [LoginCommand] an object containing the array of command line
110
90
  # tokens and exec options to be used in a fork/exec
111
91
  # @raise [ActionFailed] if the action could not be completed
112
- def login_command(state)
92
+ def login_command(state) # rubocop:disable Lint/UnusedMethodArgument
113
93
  raise ActionFailed, "Remote login is not supported in this driver."
114
94
  end
115
95
 
@@ -120,36 +100,80 @@ module Kitchen
120
100
  #
121
101
  # @raise [UserError] if the driver will not be able to perform or if a
122
102
  # documented dependency is missing from the system
123
- def verify_dependencies ; end
103
+ def verify_dependencies
104
+ end
124
105
 
125
- # Returns a Hash of configuration and other useful diagnostic information.
126
- #
127
- # @return [Hash] a diagnostic hash
128
- def diagnose
129
- result = Hash.new
130
- config_keys.sort.each { |k| result[k] = config[k] }
131
- result
106
+ class << self
107
+ # @return [Array<Symbol>] an array of action method names that cannot
108
+ # be run concurrently and must be run in serial via a shared mutex
109
+ attr_reader :serial_actions
132
110
  end
133
111
 
134
- protected
112
+ # Registers certain driver actions that cannot be safely run concurrently
113
+ # in threads across multiple instances. Typically this might be used
114
+ # for create or destroy actions that use an underlying resource that
115
+ # cannot be used at the same time.
116
+ #
117
+ # A shared mutex for this driver object will be used to synchronize all
118
+ # registered methods.
119
+ #
120
+ # @example a single action method that cannot be run concurrently
121
+ #
122
+ # no_parallel_for :create
123
+ #
124
+ # @example multiple action methods that cannot be run concurrently
125
+ #
126
+ # no_parallel_for :create, :destroy
127
+ #
128
+ # @param methods [Array<Symbol>] one or more actions as symbols
129
+ # @raise [ClientError] if any method is not a valid action method name
130
+ def self.no_parallel_for(*methods)
131
+ action_methods = [:create, :converge, :setup, :verify, :destroy]
135
132
 
136
- attr_reader :config
133
+ Array(methods).each do |meth|
134
+ next if action_methods.include?(meth)
137
135
 
138
- ACTION_METHODS = %w{create converge setup verify destroy}.
139
- map(&:to_sym).freeze
136
+ raise ClientError, "##{meth} is not a valid no_parallel_for method"
137
+ end
140
138
 
139
+ @serial_actions ||= []
140
+ @serial_actions += methods
141
+ end
142
+
143
+ private
144
+
145
+ # Returns a suitable logger to use for output.
146
+ #
147
+ # @return [Kitchen::Logger] a logger
141
148
  def logger
142
149
  instance ? instance.logger : Kitchen.logger
143
150
  end
144
151
 
152
+ # Intercepts any bare #puts calls in subclasses and issues an INFO log
153
+ # event instead.
154
+ #
155
+ # @param msg [String] message string
145
156
  def puts(msg)
146
157
  info(msg)
147
158
  end
148
159
 
160
+ # Intercepts any bare #print calls in subclasses and issues an INFO log
161
+ # event instead.
162
+ #
163
+ # @param msg [String] message string
149
164
  def print(msg)
150
165
  info(msg)
151
166
  end
152
167
 
168
+ # Delegates to Kitchen::ShellOut.run_command, overriding some default
169
+ # options:
170
+ #
171
+ # * `:use_sudo` defaults to the value of `config[:use_sudo]` in the
172
+ # Driver object
173
+ # * `:log_subject` defaults to a String representation of the Driver's
174
+ # class name
175
+ #
176
+ # @see ShellOut#run_command
153
177
  def run_command(cmd, options = {})
154
178
  base_options = {
155
179
  :use_sudo => config[:use_sudo],
@@ -158,70 +182,11 @@ module Kitchen
158
182
  super(cmd, base_options)
159
183
  end
160
184
 
161
- def busser_setup_cmd
162
- busser.setup_cmd
163
- end
164
-
165
- def busser_sync_cmd
166
- busser.sync_cmd
167
- end
168
-
169
- def busser_run_cmd
170
- busser.run_cmd
171
- end
172
-
185
+ # Returns the Busser object associated with the driver.
186
+ #
187
+ # @return [Busser] a busser
173
188
  def busser
174
- @busser ||= begin
175
- raise ClientError, "Instance must be set for Driver" if instance.nil?
176
- instance.busser
177
- end
178
- end
179
-
180
- def self.defaults
181
- @defaults ||= Hash.new.merge(super_defaults)
182
- end
183
-
184
- def self.super_defaults
185
- klass = self.superclass
186
-
187
- if klass.respond_to?(:defaults)
188
- klass.defaults
189
- else
190
- Hash.new
191
- end
192
- end
193
-
194
- def self.default_config(attr, value = nil, &block)
195
- defaults[attr] = block_given? ? block : value
196
- end
197
-
198
- def self.validations
199
- @validations
200
- end
201
-
202
- def self.required_config(attr, &block)
203
- @validations = [] if @validations.nil?
204
- if ! block_given?
205
- klass = self
206
- block = lambda do |attr, value, driver|
207
- if value.nil? || value.to_s.empty?
208
- attribute = "#{klass}#{driver.instance.to_str}#config[:#{attr}]"
209
- raise UserError, "#{attribute} cannot be blank"
210
- end
211
- end
212
- end
213
- @validations << [attr, block]
214
- end
215
-
216
- def self.no_parallel_for(*methods)
217
- Array(methods).each do |meth|
218
- if ! ACTION_METHODS.include?(meth)
219
- raise ClientError, "##{meth} is not a valid no_parallel_for method"
220
- end
221
- end
222
-
223
- @serial_actions ||= []
224
- @serial_actions += methods
189
+ instance.busser
225
190
  end
226
191
  end
227
192
  end
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
4
  #
5
- # Copyright (C) 2012, Fletcher Nichol
5
+ # Copyright (C) 2012, 2013, 2014 Fletcher Nichol
6
6
  #
7
7
  # Licensed under the Apache License, Version 2.0 (the "License");
8
8
  # you may not use this file except in compliance with the License.
@@ -16,13 +16,16 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'kitchen'
19
+ require "kitchen"
20
20
 
21
21
  module Kitchen
22
22
 
23
23
  module Driver
24
24
 
25
- # Dummy driver for Kitchen.
25
+ # Dummy driver for Kitchen. This driver does nothing but report what would
26
+ # happen if this driver did anything of consequence. As a result it may
27
+ # be a useful driver to use when debugging or developing new features or
28
+ # plugins.
26
29
  #
27
30
  # @author Fletcher Nichol <fnichol@nichol.ca>
28
31
  class Dummy < Kitchen::Driver::Base
@@ -30,23 +33,28 @@ module Kitchen
30
33
  default_config :sleep, 0
31
34
  default_config :random_failure, false
32
35
 
36
+ # (see Base#create)
33
37
  def create(state)
34
38
  state[:my_id] = "#{instance.name}-#{Time.now.to_i}"
35
39
  report(:create, state)
36
40
  end
37
41
 
42
+ # (see Base#converge)
38
43
  def converge(state)
39
44
  report(:converge, state)
40
45
  end
41
46
 
47
+ # (see Base#setup)
42
48
  def setup(state)
43
49
  report(:setup, state)
44
50
  end
45
51
 
52
+ # (see Base#verify)
46
53
  def verify(state)
47
54
  report(:verify, state)
48
55
  end
49
56
 
57
+ # (see Base#destroy)
50
58
  def destroy(state)
51
59
  report(:destroy, state)
52
60
  state.delete(:my_id)
@@ -54,25 +62,45 @@ module Kitchen
54
62
 
55
63
  private
56
64
 
65
+ # Report what action is taking place, sleeping if so configured, and
66
+ # possibly fail randomly.
67
+ #
68
+ # @param action [Symbol] the action currently taking place
69
+ # @param state [Hash] the state hash
70
+ # @api private
57
71
  def report(action, state)
58
72
  what = action.capitalize
59
73
  info("[Dummy] #{what} on instance=#{instance} with state=#{state}")
60
74
  sleep_if_set
61
- random_failure_if_set(action)
75
+ failure_if_set(action)
62
76
  debug("[Dummy] #{what} completed (#{config[:sleep]}s).")
63
77
  end
64
78
 
79
+ # Sleep for a period of time, if a value is set in the config.
80
+ #
81
+ # @api private
65
82
  def sleep_if_set
66
83
  sleep(config[:sleep].to_f) if config[:sleep].to_f > 0.0
67
84
  end
68
85
 
69
- def random_failure_if_set(action)
70
- if config[:random_failure] && randomly_fail?
86
+ # Simulate a failure in an action, if set in the config.
87
+ #
88
+ # @param action [Symbol] the action currently taking place
89
+ # @api private
90
+ def failure_if_set(action)
91
+ if config[:"fail_#{action}"]
92
+ debug("[Dummy] Failure for action ##{action}.")
93
+ raise ActionFailed, "Action ##{action} failed for #{instance.to_str}."
94
+ elsif config[:random_failure] && randomly_fail?
71
95
  debug("[Dummy] Random failure for action ##{action}.")
72
96
  raise ActionFailed, "Action ##{action} failed for #{instance.to_str}."
73
97
  end
74
98
  end
75
99
 
100
+ # Determine whether or not to randomly fail.
101
+ #
102
+ # @return [true, false]
103
+ # @api private
76
104
  def randomly_fail?
77
105
  [true, false].sample
78
106
  end