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