test-kitchen 1.2.1 → 1.3.0

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