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,7 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'kitchen/command'
19
+ require "kitchen/command"
20
20
 
21
21
  module Kitchen
22
22
 
@@ -27,6 +27,7 @@ module Kitchen
27
27
  # @author Fletcher Nichol <fnichol@nichol.ca>
28
28
  class Login < Kitchen::Command::Base
29
29
 
30
+ # Invoke the command.
30
31
  def call
31
32
  results = parse_subcommand(args.first)
32
33
  if results.size > 1
@@ -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 'kitchen/command'
19
+ require "kitchen/command"
20
20
 
21
21
  module Kitchen
22
22
 
@@ -27,6 +27,7 @@ module Kitchen
27
27
  # @author Seth Vargo <sethvargo@gmail.com>
28
28
  class Sink < Kitchen::Command::Base
29
29
 
30
+ # Invoke the command.
30
31
  def call
31
32
  puts [
32
33
  "",
@@ -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 'kitchen/command'
19
+ require "kitchen/command"
20
20
 
21
- require 'benchmark'
21
+ require "benchmark"
22
22
 
23
23
  module Kitchen
24
24
 
@@ -31,15 +31,16 @@ module Kitchen
31
31
 
32
32
  include RunAction
33
33
 
34
+ # Invoke the command.
34
35
  def call
35
- if ! %w{passing always never}.include?(options[:destroy])
36
+ if !%w[passing always never].include?(options[:destroy])
36
37
  raise ArgumentError, "Destroy mode must be passing, always, or never."
37
38
  end
38
39
 
39
40
  banner "Starting Kitchen (v#{Kitchen::VERSION})"
40
41
  elapsed = Benchmark.measure do
41
42
  destroy_mode = options[:destroy].to_sym
42
- results = parse_subcommand(args.join('|'))
43
+ results = parse_subcommand(args.join("|"))
43
44
 
44
45
  run_action(:test, results, destroy_mode)
45
46
  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.
@@ -20,24 +20,76 @@ module Kitchen
20
20
 
21
21
  # Base configuration class for Kitchen. This class exposes configuration such
22
22
  # as the location of the Kitchen config file, instances, log_levels, etc.
23
+ # This object is a factory object, meaning that it is responsible for
24
+ # consuming the desired testing configuration in and returning Ruby objects
25
+ # which are used to perfom the work.
26
+ #
27
+ # Most internal objects are created with the expectation of being
28
+ # *immutable*, meaning that internal state cannot be modified after creation.
29
+ # Any data manipulation or thread-unsafe activity is performed in this object
30
+ # so that the subsequently created objects (such as Instances, Platforms,
31
+ # Drivers, etc.) can safely run in concurrent threads of execution. To
32
+ # prevent the re-creation of duplicate objects, most created objects are
33
+ # memoized. The consequence of this is that once the Instance Array has
34
+ # been requested (with the `#instances` message), you will always be returned
35
+ # the same Instance objects.
36
+ #
37
+ # @example fetching all instances
38
+ #
39
+ # Kitchen::Config.new.instances
40
+ #
41
+ # @example fetching an instance by name
42
+ #
43
+ # Kitchen::Config.new.instances.get("default-ubuntu-12.04")
44
+ #
45
+ # @example fetching all instances matching a regular expression
46
+ #
47
+ # Kitchen::Config.new.instances.get_all(/ubuntu/)
23
48
  #
24
49
  # @author Fletcher Nichol <fnichol@nichol.ca>
25
50
  class Config
26
51
 
52
+ # @return [String] the absolute path to the root of a Test Kitchen project
53
+ # @api private
27
54
  attr_reader :kitchen_root
55
+
56
+ # @return [String] the absolute path to the directory into which all Test
57
+ # Kitchen log files will be written
58
+ # @api private
28
59
  attr_reader :log_root
60
+
61
+ # @return [String] the absolute path to the directory containing test
62
+ # suites and other testing-related file and directories
63
+ # @api private
29
64
  attr_reader :test_base_path
65
+
66
+ # @return [#read] the data loader that responds to a `#read` message,
67
+ # returning a Hash data structure
68
+ # @api private
30
69
  attr_reader :loader
70
+
71
+ # @return [Symbol] the logging verbosity level
72
+ # @api private
31
73
  attr_accessor :log_level
32
74
 
33
- # Creates a new configuration.
75
+ # Creates a new configuration, representing a particular testing
76
+ # configuration for a project.
34
77
  #
35
78
  # @param [Hash] options configuration
36
- # @option options [#read] :loader
37
- # @option options [String] :kitchen_root
38
- # @option options [String] :log_root
39
- # @option options [String] :test_base_path
40
- # @option options [Symbol] :log_level
79
+ # @option options [#read] :loader an object that responds to `#read` with
80
+ # a Hash structure suitable for manipulating
81
+ # (default: `Kitchen::Loader::YAML.new`)
82
+ # @option options [String] :kitchen_root an absolute path to the root of a
83
+ # Test Kitchen project, usually containing a `.kitchen.yml` file
84
+ # (default `Dir.pwd`)
85
+ # @option options [String] :log_root an absolute path to the directory
86
+ # into which all Test Kitchen log files will be written
87
+ # (default: `"#{kitchen_root}/.kitchen/logs"`)
88
+ # @option options [String] :test_base_path an absolute path to the
89
+ # directory containing test suites and other testing-related files and
90
+ # directories (default: `"#{kitchen_root}/test/integration"`)
91
+ # @option options [Symbol] :log_level the log level verbosity that the
92
+ # loggers will use when outputing information (default: `:info`)
41
93
  def initialize(options = {})
42
94
  @loader = options.fetch(:loader) { Kitchen::Loader::YAML.new }
43
95
  @kitchen_root = options.fetch(:kitchen_root) { Dir.pwd }
@@ -46,20 +98,20 @@ module Kitchen
46
98
  @test_base_path = options.fetch(:test_base_path) { default_test_base_path }
47
99
  end
48
100
 
49
- # @return [Array<Instance>] all instances, resulting from all platform and
50
- # suite combinations
101
+ # @return [Collection<Instance>] all instances, resulting from all
102
+ # platform and suite combinations
51
103
  def instances
52
104
  @instances ||= Collection.new(build_instances)
53
105
  end
54
106
 
55
- # @return [Array<Platform>] all defined platforms which will be used in
56
- # convergence integration
107
+ # @return [Collection<Platform>] all defined platforms which will be used
108
+ # in convergence integration
57
109
  def platforms
58
110
  @platforms ||= Collection.new(
59
111
  data.platform_data.map { |pdata| Platform.new(pdata) })
60
112
  end
61
113
 
62
- # @return [Array<Suite>] all defined suites which will be used in
114
+ # @return [Collection<Suite>] all defined suites which will be used in
63
115
  # convergence integration
64
116
  def suites
65
117
  @suites ||= Collection.new(
@@ -68,24 +120,51 @@ module Kitchen
68
120
 
69
121
  private
70
122
 
123
+ # Builds the filtered list of Instance objects.
124
+ #
125
+ # @return [Array<Instance] an array of Instances
126
+ # @api private
71
127
  def build_instances
72
128
  filter_instances.map.with_index do |(suite, platform), index|
73
129
  new_instance(suite, platform, index)
74
130
  end
75
131
  end
76
132
 
133
+ # Returns an object which can generate configuration hashes for all the
134
+ # primary Test Kitchen objects such as Drivers, Provisioners, etc.
135
+ #
136
+ # @return [DataMunger] a data manipulator
137
+ # @api private
77
138
  def data
78
139
  @data ||= DataMunger.new(loader.read, kitchen_config)
79
140
  end
80
141
 
142
+ # Determines the default absolute path to a log directory, based on the
143
+ # value of `#kitchen_root`.
144
+ #
145
+ # @return [String] an absolute path to the log directory
146
+ # @api private
81
147
  def default_log_root
82
148
  File.join(kitchen_root, Kitchen::DEFAULT_LOG_DIR)
83
149
  end
84
150
 
151
+ # Determines the default absolute path to the testing files directory,
152
+ # based on the the value of `#kitchen_root`.
153
+ #
154
+ # @return [String] an absolute path to the testing files directory
155
+ # @api private
85
156
  def default_test_base_path
86
157
  File.join(kitchen_root, Kitchen::DEFAULT_TEST_DIR)
87
158
  end
88
159
 
160
+ # Generates a filtered Array of tuples (Suite/Platform pairs) which is the
161
+ # cartesian product of suites and platforms. A Suite has two optional
162
+ # arrays (`#includes` and `#excludes`) which can be used to drop or
163
+ # select certain Platforms with which to join.
164
+ #
165
+ # @return [Array<Array<Suite, Platform>>] an Array of Suite/Platform
166
+ # tuples
167
+ # @api private
89
168
  def filter_instances
90
169
  suites.product(platforms).select do |suite, platform|
91
170
  if !suite.includes.empty?
@@ -98,10 +177,21 @@ module Kitchen
98
177
  end
99
178
  end
100
179
 
180
+ # Determines the String name for an Instance, given a Suite and a Platform.
181
+ #
182
+ # @param suite [Suite,#name] a Suite
183
+ # @param platform [Platform,#name] a Platform
184
+ # @return [String] an Instance name
185
+ # @api private
101
186
  def instance_name(suite, platform)
102
187
  Instance.name_for(suite, platform)
103
188
  end
104
189
 
190
+ # Generates the immutable Test Kitchen configuration and reasonable
191
+ # defaults for Drivers and Provisioners.
192
+ #
193
+ # @return [Hash] a configuration Hash
194
+ # @api private
105
195
  def kitchen_config
106
196
  @kitchen_config ||= {
107
197
  :defaults => {
@@ -110,20 +200,40 @@ module Kitchen
110
200
  },
111
201
  :kitchen_root => kitchen_root,
112
202
  :test_base_path => test_base_path,
113
- :log_level => log_level,
203
+ :log_level => log_level
114
204
  }
115
205
  end
116
206
 
207
+ # Builds a newly configured Busser object, for a given a Suite and Platform.
208
+ #
209
+ # @param suite [Suite,#name] a Suite
210
+ # @param platform [Platform,#name] a Platform
211
+ # @return [Busser] a new Busser object
212
+ # @api private
117
213
  def new_busser(suite, platform)
118
214
  bdata = data.busser_data_for(suite.name, platform.name)
119
215
  Busser.new(suite.name, bdata)
120
216
  end
121
217
 
218
+ # Builds a newly configured Driver object, for a given Suite and Platform.
219
+ #
220
+ # @param suite [Suite,#name] a Suite
221
+ # @param platform [Platform,#name] a Platform
222
+ # @return [Driver] a new Driver object
223
+ # @api private
122
224
  def new_driver(suite, platform)
123
225
  ddata = data.driver_data_for(suite.name, platform.name)
124
226
  Driver.for_plugin(ddata[:name], ddata)
125
227
  end
126
228
 
229
+ # Builds a newly configured Instance object, for a given Suite and
230
+ # Platform.
231
+ #
232
+ # @param suite [Suite,#name] a Suite
233
+ # @param platform [Platform,#name] a Platform
234
+ # @param index [Integer] an index used for colorizing output
235
+ # @return [Instance] a new Instance object
236
+ # @api private
127
237
  def new_instance(suite, platform, index)
128
238
  Instance.new(
129
239
  :busser => new_busser(suite, platform),
@@ -136,22 +246,44 @@ module Kitchen
136
246
  )
137
247
  end
138
248
 
249
+ # Builds a newly configured Logger object, for a given Suite and
250
+ # Platform.
251
+ #
252
+ # @param suite [Suite,#name] a Suite
253
+ # @param platform [Platform,#name] a Platform
254
+ # @param index [Integer] an index used for colorizing output
255
+ # @return [Logger] a new Logger object
256
+ # @api private
139
257
  def new_logger(suite, platform, index)
140
258
  name = instance_name(suite, platform)
141
259
  Logger.new(
142
260
  :stdout => STDOUT,
143
261
  :color => Color::COLORS[index % Color::COLORS.size].to_sym,
144
262
  :logdev => File.join(log_root, "#{name}.log"),
145
- :level => Util.to_logger_level(self.log_level),
263
+ :level => Util.to_logger_level(log_level),
146
264
  :progname => name
147
265
  )
148
266
  end
149
267
 
268
+ # Builds a newly configured Provisioner object, for a given Suite and
269
+ # Platform.
270
+ #
271
+ # @param suite [Suite,#name] a Suite
272
+ # @param platform [Platform,#name] a Platform
273
+ # @return [Provisioner] a new Provisioner object
274
+ # @api private
150
275
  def new_provisioner(suite, platform)
151
276
  pdata = data.provisioner_data_for(suite.name, platform.name)
152
277
  Provisioner.for_plugin(pdata[:name], pdata)
153
278
  end
154
279
 
280
+ # Builds a newly configured StateFile object, for a given Suite and
281
+ # Platform.
282
+ #
283
+ # @param suite [Suite,#name] a Suite
284
+ # @param platform [Platform,#name] a Platform
285
+ # @return [StateFile] a new StateFile object
286
+ # @api private
155
287
  def new_state_file(suite, platform)
156
288
  StateFile.new(kitchen_root, instance_name(suite, platform))
157
289
  end
@@ -0,0 +1,314 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2014, Fletcher Nichol
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require "thor/util"
20
+
21
+ require "kitchen/lazy_hash"
22
+
23
+ module Kitchen
24
+
25
+ # A mixin for providing configuration-related behavior such as default
26
+ # config (static, computed, inherited), required config, local path
27
+ # expansion, etc.
28
+ #
29
+ # @author Fletcher Nichol <fnichol@nichol.ca>
30
+ module Configurable
31
+
32
+ def self.included(base)
33
+ base.extend(ClassMethods)
34
+ end
35
+
36
+ # @return [Kitchen::Instance] the associated instance
37
+ attr_reader :instance
38
+
39
+ # A lifecycle method that should be invoked when the object is about ready
40
+ # to be used. A reference to an Instance is required as configuration
41
+ # dependant data may be access through an Instance. This also acts as a
42
+ # hook point where the object may wish to perform other last minute
43
+ # checks, validations, or configuration expansions.
44
+ #
45
+ # @param instance [Instance] an associated instance
46
+ # @return [self] itself, for use in chaining
47
+ # @raise [ClientError] if instance parameter is nil
48
+ def finalize_config!(instance)
49
+ if instance.nil?
50
+ raise ClientError, "Instance must be provided to #{self}"
51
+ end
52
+
53
+ @instance = instance
54
+ expand_paths!
55
+ validate_config!
56
+
57
+ self
58
+ end
59
+
60
+ # Provides hash-like access to configuration keys.
61
+ #
62
+ # @param attr [Object] configuration key
63
+ # @return [Object] value at configuration key
64
+ def [](attr)
65
+ config[attr]
66
+ end
67
+
68
+ # Find an appropriate path to a file or directory, based on graceful
69
+ # fallback rules or returns nil if path cannot be determined.
70
+ #
71
+ # Given an instance with suite named `"server"`, a `test_base_path` of
72
+ # `"/a/b"`, and a path segement of `"roles"` then following will be tried
73
+ # in order (first match that exists wins):
74
+ #
75
+ # 1. /a/b/server/roles
76
+ # 2. /a/b/roles
77
+ # 3. $PWD/roles
78
+ #
79
+ # @param path [String] the base path segment to search for
80
+ # @param opts [Hash] options
81
+ # @option opts [Symbol] :type either `:file` or `:directory` (default)
82
+ # @option opts [Symbol] :base_path a custom base path to search under,
83
+ # default uses value from `config[:test_base_path]`
84
+ # @return [String] path to the existing file or directory, or nil if file
85
+ # or directory was not found
86
+ # @raise [UserError] if `config[:test_base_path]` is used and is not set
87
+ def calculate_path(path, opts = {})
88
+ type = opts.fetch(:type, :directory)
89
+ base = opts.fetch(:base_path) do
90
+ config.fetch(:test_base_path) do |key|
91
+ raise UserError, "#{key} is not found in #{self}"
92
+ end
93
+ end
94
+
95
+ [
96
+ File.join(base, instance.suite.name, path),
97
+ File.join(base, path),
98
+ File.join(Dir.pwd, path)
99
+ ].find do |candidate|
100
+ type == :directory ? File.directory?(candidate) : File.file?(candidate)
101
+ end
102
+ end
103
+
104
+ # Returns an array of configuration keys.
105
+ #
106
+ # @return [Array] array of configuration keys
107
+ def config_keys
108
+ config.keys
109
+ end
110
+
111
+ # Returns a Hash of configuration and other useful diagnostic information.
112
+ #
113
+ # @return [Hash] a diagnostic hash
114
+ def diagnose
115
+ result = Hash.new
116
+ config_keys.sort.each { |k| result[k] = config[k] }
117
+ result
118
+ end
119
+
120
+ private
121
+
122
+ # @return [LzayHash] a configuration hash
123
+ # @api private
124
+ attr_reader :config
125
+
126
+ # Initializes an internal configuration hash. The hash may contain
127
+ # callable blocks as values that are meant to be called lazily. This
128
+ # method is intended to be included in an object's .initialize method.
129
+ #
130
+ # @param config [Hash] initial provided configuration
131
+ # @api private
132
+ def init_config(config)
133
+ @config = LazyHash.new(config, self)
134
+ self.class.defaults.each do |attr, value|
135
+ @config[attr] = value unless @config.key?(attr)
136
+ end
137
+ end
138
+
139
+ # Expands file paths for certain configuration values. A configuration
140
+ # value is marked for file expansion with a expand_path_for declaration
141
+ # in the included class.
142
+ #
143
+ # @api private
144
+ def expand_paths!
145
+ root_path = config[:kitchen_root] || Dir.pwd
146
+ expanded_paths = LazyHash.new(self.class.expanded_paths, self).to_hash
147
+
148
+ expanded_paths.each do |key, should_expand|
149
+ next if !should_expand || config[key].nil?
150
+
151
+ config[key] = File.expand_path(config[key], root_path)
152
+ end
153
+ end
154
+
155
+ # Runs all validations set up for the included class. Each validation is
156
+ # for a specific configuration attribute and has an associated callable
157
+ # block. Each validation block is called with the attribute, its value,
158
+ # and the included object for context.
159
+ #
160
+ # @api private
161
+ def validate_config!
162
+ self.class.validations.each do |attr, block|
163
+ block.call(attr, config[attr], self)
164
+ end
165
+ end
166
+
167
+ # Class methods which will be mixed in on inclusion of Configurable module.
168
+ module ClassMethods
169
+
170
+ # Sets a sane default value for a configuration attribute. These values
171
+ # can be overridden by provided configuration or in a subclass with
172
+ # another default_config declaration.
173
+ #
174
+ # @example a nil default value
175
+ #
176
+ # default_config :i_am_nil
177
+ #
178
+ # @example a primitive default value
179
+ #
180
+ # default_config :use_sudo, true
181
+ #
182
+ # @example a block to compute a default value
183
+ #
184
+ # default_config :box_name do |subject|
185
+ # subject.instance.platform.name
186
+ # end
187
+ #
188
+ # @param attr [String] configuration attribute name
189
+ # @param value [Object, nil] static default value for attribute
190
+ # @yieldparam object [Object] a reference to the instantiated object
191
+ # @yieldreturn [Object, nil] dynamically computed value for the attribute
192
+ def default_config(attr, value = nil, &block)
193
+ defaults[attr] = block_given? ? block : value
194
+ end
195
+
196
+ # Ensures that an attribute which is a path will be fully expanded at
197
+ # the right time. This helps make the configuration unambiguous and much
198
+ # easier to debug and diagnose.
199
+ #
200
+ # Note that the file path expansion is only intended for paths on the
201
+ # local workstation invking the Test Kitchen code.
202
+ #
203
+ # @example the default usage
204
+ #
205
+ # expand_path_for :data_path
206
+ #
207
+ # @example disabling path expansion with a falsey value
208
+ #
209
+ # expand_path_for :relative_path, false
210
+ #
211
+ # @example using a block to determine whether or not to expand
212
+ #
213
+ # expand_path_for :relative_or_not_path do |subject|
214
+ # subject.instance.name =~ /default/
215
+ # end
216
+ #
217
+ # @param attr [String] configuration attribute name
218
+ # @param value [Object, nil] whether or not to exand the file path
219
+ # @yieldparam object [Object] a reference to the instantiated object
220
+ # @yieldreturn [Object, nil] dynamically compute whether or not to
221
+ # perform the file expansion
222
+ def expand_path_for(attr, value = true, &block)
223
+ expanded_paths[attr] = block_given? ? block : value
224
+ end
225
+
226
+ # Ensures that an attribute must have a non-nil, non-empty String value.
227
+ # The default behavior will be to raise a user error and thereby halting
228
+ # further configuration processing. Good use cases for require_config
229
+ # might be cloud provider credential keys and other similar data.
230
+ #
231
+ # @example a value that must not be nil or an empty String
232
+ #
233
+ # required_config :cloud_api_token
234
+ #
235
+ # @example using a block to use custom validation logic
236
+ #
237
+ # required_config :email do |attr, value, subject|
238
+ # raise UserError, "Must be an email address" unless value =~ /@/
239
+ # end
240
+ #
241
+ # @param attr [String] configuration attribute name
242
+ # @yieldparam attr [Symbol] the attribute name
243
+ # @yieldparam value [Object] the current value of the attribute
244
+ # @yieldparam object [Object] a reference to the instantiated object
245
+ def required_config(attr, &block)
246
+ if !block_given?
247
+ klass = self
248
+ block = lambda do |_, value, thing|
249
+ if value.nil? || value.to_s.empty?
250
+ attribute = "#{klass}#{thing.instance.to_str}#config[:#{attr}]"
251
+ raise UserError, "#{attribute} cannot be blank"
252
+ end
253
+ end
254
+ end
255
+ validations[attr] = block
256
+ end
257
+
258
+ # @return [Hash] a hash of attribute keys and default values which has
259
+ # been merged with any superclass defaults
260
+ # @api private
261
+ def defaults
262
+ @defaults ||= Hash.new.merge(super_defaults)
263
+ end
264
+
265
+ # @return [Hash] a hash of defaults from the included class' superclass
266
+ # if defined in the superclass, or an empty hash otherwise
267
+ # @api private
268
+ def super_defaults
269
+ if superclass.respond_to?(:defaults)
270
+ superclass.defaults
271
+ else
272
+ Hash.new
273
+ end
274
+ end
275
+
276
+ # @return [Hash] a hash of attribute keys and truthy/falsey values to
277
+ # determine if said attribute needs to be fully file path expanded,
278
+ # which has been merged with any superclass expanded paths
279
+ # @api private
280
+ def expanded_paths
281
+ @expanded_paths ||= Hash.new.merge(super_expanded_paths)
282
+ end
283
+
284
+ # @return [Hash] a hash of expanded paths from the included class'
285
+ # superclass if defined in the superclass, or an empty hash otherwise
286
+ # @api private
287
+ def super_expanded_paths
288
+ if superclass.respond_to?(:expanded_paths)
289
+ superclass.expanded_paths
290
+ else
291
+ Hash.new
292
+ end
293
+ end
294
+
295
+ # @return [Hash] a hash of attribute keys and valudation callable blocks
296
+ # which has been merged with any superclass valudations
297
+ # @api private
298
+ def validations
299
+ @validations ||= Hash.new.merge(super_validations)
300
+ end
301
+
302
+ # @return [Hash] a hash of validations from the included class'
303
+ # superclass if defined in the superclass, or an empty hash otherwise
304
+ # @api private
305
+ def super_validations
306
+ if superclass.respond_to?(:validations)
307
+ superclass.validations
308
+ else
309
+ Hash.new
310
+ end
311
+ end
312
+ end
313
+ end
314
+ end