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,35 +16,107 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'delegate'
19
+ require "delegate"
20
20
 
21
21
  module Kitchen
22
22
 
23
- # A modifed Hash object that may contain procs as a value which must be
24
- # executed in the context of another object.
23
+ # A modifed Hash object that may contain callables as a value which must be
24
+ # executed in the context of another object. This allows for delayed
25
+ # evaluation of a hash value while still looking and largely feeling like a
26
+ # normal Ruby Hash.
27
+ #
28
+ # @example normal hash accessing with regular values
29
+ #
30
+ # data = {
31
+ # :symbol => true,
32
+ # "string" => "stuff"
33
+ # }
34
+ # context = "any object"
35
+ # lazy = Kitchen::Hash.new(data, context)
36
+ #
37
+ # lazy[:symbol] # => true
38
+ # lazy.fetch("string") # => "stuff"
39
+ #
40
+ # @example hash with callable blocks as values
41
+ #
42
+ # data = {
43
+ # :lambda => ->(c) { c.length },
44
+ # :proc => Proc.new { |c| c.reverse },
45
+ # :simple => "value"
46
+ # }
47
+ # context = "any object"
48
+ # lazy = Kitchen::Hash.new(data, context)
49
+ #
50
+ # lazy[:lambda] # => 10
51
+ # lazy.fetch(:proc) # => "tcejbo yna"
52
+ # lazy[:simple] # => "value"
25
53
  #
26
54
  # @author Fletcher Nichol <fnichol@nichol.ca>
27
55
  class LazyHash < SimpleDelegator
28
56
 
57
+ # Creates a new LazyHash using a Hash-like object to populate itself and
58
+ # an object that can be used as context in value-callable blocks. The
59
+ # context object can be used to compute values for keys at the time of
60
+ # fetching the value.
61
+ #
62
+ # @param obj [Hash, Object] a hash-like object
63
+ # @param context [Object] an object that can be used to compute values
29
64
  def initialize(obj, context)
30
65
  @context = context
31
66
  super(obj)
32
67
  end
33
68
 
69
+ # Retrieves the rendered value object corresponding to the key object. If
70
+ # not found, returns the default value.
71
+ #
72
+ # @param key [Object] hash key
73
+ # @return [Object, nil] the value for key or the default value if key is
74
+ # not found
34
75
  def [](key)
35
- proc_or_val = __getobj__[key]
76
+ proc_or_val(__getobj__[key])
77
+ end
36
78
 
37
- if proc_or_val.respond_to?(:call)
38
- proc_or_val.call(@context)
79
+ # Returns a rendered value from the hash for the given key. If the key
80
+ # can't be found, there are several options: With no other arguments, it
81
+ # will raise an KeyError exception; if default is given, then that will be
82
+ # returned; if the optional code block is specified, then that will be run
83
+ # and its result returned.
84
+ #
85
+ # @param key [Object] hash key
86
+ # @param default [Object] default value if key is not set (optional)
87
+ # @return [Object, nil] the value for the key or the default value if key
88
+ # is not found
89
+ # @raise [KeyError] if the key is not found
90
+ def fetch(key, default = :__undefined__, &block)
91
+ case default
92
+ when :__undefined__
93
+ proc_or_val(__getobj__.fetch(key, &block))
39
94
  else
40
- proc_or_val
95
+ proc_or_val(__getobj__.fetch(key, default, &block))
41
96
  end
42
97
  end
43
98
 
99
+ # Returns a new Hash with all keys and rendered values of the LazyHash.
100
+ #
101
+ # @return [Hash] a new hash
44
102
  def to_hash
45
103
  hash = Hash.new
46
104
  __getobj__.keys.each { |key| hash[key] = self[key] }
47
105
  hash
48
106
  end
107
+
108
+ private
109
+
110
+ # Returns an object or invokes call with context if object is callable.
111
+ #
112
+ # @return [Object] an object
113
+ # @api private
114
+ def proc_or_val(thing)
115
+ if thing.respond_to?(:call)
116
+ thing.call(@context)
117
+ else
118
+ thing
119
+ end
120
+ end
49
121
  end
50
122
  end
@@ -16,15 +16,15 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'erb'
20
- require 'vendor/hash_recursive_merge'
19
+ require "erb"
20
+ require "vendor/hash_recursive_merge"
21
21
 
22
22
  if RUBY_VERSION <= "1.9.3"
23
23
  # ensure that Psych and not Syck is used for Ruby 1.9.2
24
- require 'yaml'
25
- YAML::ENGINE.yamler = 'psych'
24
+ require "yaml"
25
+ YAML::ENGINE.yamler = "psych"
26
26
  end
27
- require 'safe_yaml/load'
27
+ require "safe_yaml/load"
28
28
 
29
29
  module Kitchen
30
30
 
@@ -46,7 +46,7 @@ module Kitchen
46
46
  # config YAML file (default: `./.kitchen.yml`)
47
47
  # @option options [String] :local_config path to the Kitchen local
48
48
  # config YAML file (default: `./.kitchen.local.yml`)
49
- # @option options [String] :local_config path to the Kitchen global
49
+ # @option options [String] :global_config path to the Kitchen global
50
50
  # config YAML file (default: `$HOME/.kitchen/config.yml`)
51
51
  # @option options [String] :process_erb whether or not to process YAML
52
52
  # through an ERB processor (default: `true`)
@@ -70,7 +70,7 @@ module Kitchen
70
70
  #
71
71
  # @return [Hash] merged configuration data
72
72
  def read
73
- if ! File.exists?(config_file)
73
+ if !File.exist?(config_file)
74
74
  raise UserError, "Kitchen YAML file #{config_file} does not exist."
75
75
  end
76
76
 
@@ -92,64 +92,146 @@ module Kitchen
92
92
  result
93
93
  end
94
94
 
95
- protected
95
+ private
96
96
 
97
- attr_reader :config_file, :local_config_file, :global_config_file
97
+ # @return [String] the absolute path to the Kitchen config YAML file
98
+ # @api private
99
+ attr_reader :config_file
98
100
 
99
- def default_config_file
100
- File.join(Dir.pwd, '.kitchen.yml')
101
- end
101
+ # @return [String] the absolute path to the Kitchen local config YAML
102
+ # file
103
+ # @api private
104
+ attr_reader :local_config_file
105
+
106
+ # @return [String] the absolute path to the Kitchen global config YAML
107
+ # file
108
+ # @api private
109
+ attr_reader :global_config_file
102
110
 
111
+ # Performed a prioritized recursive merge of several source Hashes and
112
+ # returns a new merged Hash. There are 3 sources of configuration data:
113
+ #
114
+ # 1. local config
115
+ # 2. project config
116
+ # 3. global config
117
+ #
118
+ # The merge order is local -> project -> global, meaning that elements at
119
+ # the top of the above list will be merged last, and have greater
120
+ # precedence than elements at the bottom of the list.
121
+ #
122
+ # @return [Hash] a new merged Hash
123
+ # @api private
103
124
  def combined_hash
104
- y = if @process_local
105
- normalize(yaml).rmerge(normalize(local_yaml))
125
+ y = if @process_global
126
+ normalize(global_yaml).rmerge(normalize(yaml))
106
127
  else
107
128
  normalize(yaml)
108
129
  end
109
- @process_global ? y.rmerge(normalize(global_yaml)) : y
130
+ @process_local ? y.rmerge(normalize(local_yaml)) : y
110
131
  end
111
132
 
133
+ # Loads and returns the Kitchen config YAML as a Hash.
134
+ #
135
+ # @return [Hash] the config hash
136
+ # @api private
112
137
  def yaml
113
138
  parse_yaml_string(yaml_string(config_file), config_file)
114
139
  end
115
140
 
141
+ # Loads and returns the Kitchen local config YAML as a Hash.
142
+ #
143
+ # @return [Hash] the config hash
144
+ # @api private
116
145
  def local_yaml
117
146
  parse_yaml_string(yaml_string(local_config_file), local_config_file)
118
147
  end
119
148
 
149
+ # Loads and returns the Kitchen global config YAML as a Hash.
150
+ #
151
+ # @return [Hash] the config hash
152
+ # @api private
120
153
  def global_yaml
121
154
  parse_yaml_string(yaml_string(global_config_file), global_config_file)
122
155
  end
123
156
 
157
+ # Loads a file to a string and optionally passes it through an ERb
158
+ # process.
159
+ #
160
+ # @return [String] a file's contents as a string
161
+ # @api private
124
162
  def yaml_string(file)
125
163
  string = read_file(file)
126
164
 
127
165
  @process_erb ? process_erb(string, file) : string
128
166
  end
129
167
 
168
+ # Passes a string through ERb to evaulate any ERb blocks.
169
+ #
170
+ # @param string [String] the string to process
171
+ # @param file [String] an absolute path to the file represented as the
172
+ # passed in string, used for error reporting
173
+ # @return [String] a new string, passed through an ERb process
174
+ # @raise [UserError] if an ERb parsing error occurs
175
+ # @api private
130
176
  def process_erb(string, file)
131
- ERB.new(string).result
177
+ tpl = ERB.new(string)
178
+ tpl.filename = file
179
+ tpl.result
132
180
  rescue => e
133
- raise UserError, "Error parsing ERB content in #{file} " +
134
- "(#{e.class}: #{e.message}).\n" +
135
- "Please run `kitchen diagnose --no-instances --loader' to help " +
181
+ raise UserError, "Error parsing ERB content in #{file} " \
182
+ "(#{e.class}: #{e.message}).\n" \
183
+ "Please run `kitchen diagnose --no-instances --loader' to help " \
136
184
  "debug your issue."
137
185
  end
138
186
 
187
+ # Reads a file and returns its contents as a string.
188
+ #
189
+ # @param file [String] a path to a file
190
+ # @return [String] the files contents, or an empty string if the file
191
+ # does not exist
192
+ # @api private
139
193
  def read_file(file)
140
- File.exists?(file.to_s) ? IO.read(file) : ""
194
+ File.exist?(file.to_s) ? IO.read(file) : ""
195
+ end
196
+
197
+ # Determines the default absolute path to the Kitchen config YAML file,
198
+ # based on current working directory.
199
+ #
200
+ # @return [String] an absolute path to a Kitchen config YAML file
201
+ # @api private
202
+ def default_config_file
203
+ File.join(Dir.pwd, ".kitchen.yml")
141
204
  end
142
205
 
206
+ # Determines the default absolute path to the Kitchen local YAML file,
207
+ # based on the base Kitchen config YAML file.
208
+ #
209
+ # @return [String] an absolute path to a Kitchen local YAML file
210
+ # @api private
143
211
  def default_local_config_file
144
212
  config_file.sub(/(#{File.extname(config_file)})$/, '.local\1')
145
213
  end
146
214
 
215
+ # Determines the default absolute path to the Kitchen global YAML file,
216
+ # based on the base Kitchen config YAML file.
217
+ #
218
+ # @return [String] an absolute path to a Kitchen global YAML file
219
+ # @api private
147
220
  def default_global_config_file
148
221
  File.join(File.expand_path(ENV["HOME"]), ".kitchen", "config.yml")
149
222
  end
150
223
 
224
+ # Generate a diganose Hash for a particular YAML file Hash. If an error
225
+ # occurs when loading the data, then a failure hash will be inserted
226
+ # into the `:raw_data` sub-hash.
227
+ #
228
+ # @param component [Symbol] a YAML source component
229
+ # @param file [String] the absolute path to a file which is used for
230
+ # reporting (default: `nil`)
231
+ # @return [Hash] a hash data structure
232
+ # @api private
151
233
  def diagnose_component(component, file = nil)
152
- return if file && !File.exists?(file)
234
+ return if file && !File.exist?(file)
153
235
 
154
236
  hash = begin
155
237
  send(component)
@@ -160,6 +242,12 @@ module Kitchen
160
242
  { :filename => file, :raw_data => hash }
161
243
  end
162
244
 
245
+ # Generates a Hash respresenting a failure, given an Exception object.
246
+ #
247
+ # @param e [Exception] an exception
248
+ # @param file [String] the absolute path to a file (default: `nil`)
249
+ # @return [Hash] a hash data structure
250
+ # @api private
163
251
  def failure_hash(e, file = nil)
164
252
  result = {
165
253
  :error => {
@@ -172,14 +260,50 @@ module Kitchen
172
260
  result
173
261
  end
174
262
 
263
+ # Destructively modify an object containing one or more hashes so that
264
+ # the resulting formatted data can be consumed upstream.
265
+ #
266
+ # @param obj [Object] an object
267
+ # @return [Object] an object
268
+ # @api private
175
269
  def normalize(obj)
176
270
  if obj.is_a?(Hash)
177
- obj.inject(Hash.new) { |h, (k, v)| normalize_hash(h, k, v) ; h }
271
+ obj.inject(Hash.new) { |h, (k, v)| normalize_hash(h, k, v); h }
178
272
  else
179
273
  obj
180
274
  end
181
275
  end
182
276
 
277
+ # Normalizes certain keys in the root of a data hash to be a proper
278
+ # sub-hash in all cases. Specifically handled are the following cases:
279
+ #
280
+ # * If the value for certain keys (`"driver"`, `"provisioner"`,
281
+ # `"busser"`) are set to `nil`, a new Hash will be put in its place.
282
+ # * If the value for certain keys is a String, then the value is
283
+ # converted to a new Hash with a default key pointing to the original
284
+ # String.
285
+ #
286
+ # Given a hash:
287
+ #
288
+ # { "driver" => nil }
289
+ #
290
+ # this method would return:
291
+ #
292
+ # { "driver" => {} }
293
+ #
294
+ # Given a hash:
295
+ #
296
+ # { :driver => "coolbeans" }
297
+ #
298
+ # this method would return:
299
+ #
300
+ # { :name => { "driver" => "coolbeans" } }
301
+ #
302
+ #
303
+ # @param hash [Hash] the Hash to normalize
304
+ # @param key [Symbol] the key to normalize
305
+ # @param value [Object] the value to normalize
306
+ # @api private
183
307
  def normalize_hash(hash, key, value)
184
308
  case key
185
309
  when "driver", "provisioner", "busser"
@@ -196,20 +320,28 @@ module Kitchen
196
320
  end
197
321
  end
198
322
 
323
+ # Parses a YAML string and returns a Hash.
324
+ #
325
+ # @param string [String] a yaml document as a string
326
+ # @param file_name [String] an absolute path to the file represented as
327
+ # the passed in string, used for error reporting
328
+ # @return [Hash] a hash
329
+ # @raise [UserError] if the string document cannot be parsed
330
+ # @api private
199
331
  def parse_yaml_string(string, file_name)
200
332
  return Hash.new if string.nil? || string.empty?
201
333
 
202
334
  result = SafeYAML.load(string) || Hash.new
203
335
  unless result.is_a?(Hash)
204
- raise UserError, "Error parsing #{file_name} as YAML " +
205
- "(Result of parse was not a Hash, but was a #{result.class}).\n" +
206
- "Please run `kitchen diagnose --no-instances --loader' to help " +
336
+ raise UserError, "Error parsing #{file_name} as YAML " \
337
+ "(Result of parse was not a Hash, but was a #{result.class}).\n" \
338
+ "Please run `kitchen diagnose --no-instances --loader' to help " \
207
339
  "debug your issue."
208
340
  end
209
341
  result
210
342
  rescue SyntaxError, Psych::SyntaxError
211
- raise UserError, "Error parsing #{file_name} as YAML.\n" +
212
- "Please run `kitchen diagnose --no-instances --loader' to help " +
343
+ raise UserError, "Error parsing #{file_name} as YAML.\n" \
344
+ "Please run `kitchen diagnose --no-instances --loader' to help " \
213
345
  "debug your issue."
214
346
  end
215
347
  end
@@ -16,8 +16,8 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
 
19
- require 'fileutils'
20
- require 'logger'
19
+ require "fileutils"
20
+ require "logger"
21
21
 
22
22
  module Kitchen
23
23
 
@@ -31,55 +31,261 @@ module Kitchen
31
31
 
32
32
  include ::Logger::Severity
33
33
 
34
+ # @return [IO] the log device
34
35
  attr_reader :logdev
35
36
 
37
+ # Constructs a new logger.
38
+ #
39
+ # @param options [Hash] configuration for a new logger
40
+ # @option options [Symbol] :color color to use when when outputting
41
+ # messages
42
+ # @option options [Integer] :level the logging severity threshold
43
+ # (default: `Kitchen::DEFAULT_LOG_LEVEL`)
44
+ # @option options [String,IO] :logdev filepath String or IO object to be
45
+ # used for logging (default: `nil`)
46
+ # @option options [String] :progname program name to include in log
47
+ # messages (default: `"Kitchen"`)
48
+ # @option options [IO] :stdout a standard out IO object to use
49
+ # (default: `$stdout`)
36
50
  def initialize(options = {})
37
51
  color = options[:color]
38
52
 
39
53
  @loggers = []
40
54
  @loggers << @logdev = logdev_logger(options[:logdev]) if options[:logdev]
41
55
  @loggers << stdout_logger(options[:stdout], color) if options[:stdout]
42
- @loggers << stdout_logger(STDOUT, color) if @loggers.empty?
56
+ @loggers << stdout_logger($stdout, color) if @loggers.empty?
43
57
 
44
58
  self.progname = options[:progname] || "Kitchen"
45
59
  self.level = options[:level] || default_log_level
46
60
  end
47
61
 
48
- %w{ level progname datetime_format debug? info? error? warn? fatal?
49
- }.each do |meth|
50
- define_method(meth) do |*args|
51
- @loggers.first.public_send(meth, *args)
62
+ class << self
63
+
64
+ private
65
+
66
+ # @api private
67
+ # @!macro delegate_to_first_logger
68
+ # @method $1()
69
+ def delegate_to_first_logger(meth)
70
+ define_method(meth) { |*args| @loggers.first.public_send(meth, *args) }
52
71
  end
53
- end
54
72
 
55
- %w{ level= progname= datetime_format= add <<
56
- banner debug info error warn fatal unknown close
57
- }.map(&:to_sym).each do |meth|
58
- define_method(meth) do |*args|
59
- result = nil
60
- @loggers.each { |l| result = l.public_send(meth, *args) }
61
- result
73
+ # @api private
74
+ # @!macro delegate_to_all_loggers
75
+ # @method $1()
76
+ def delegate_to_all_loggers(meth)
77
+ define_method(meth) do |*args|
78
+ result = nil
79
+ @loggers.each { |l| result = l.public_send(meth, *args) }
80
+ result
81
+ end
62
82
  end
63
83
  end
64
84
 
85
+ # @return [Integer] the logging severity threshold
86
+ # @see http://is.gd/Okuy5p
87
+ delegate_to_first_logger :level
88
+
89
+ # Sets the logging severity threshold.
90
+ #
91
+ # @param level [Integer] the logging severity threshold
92
+ # @see http://is.gd/H1VBFH
93
+ delegate_to_all_loggers :level=
94
+
95
+ # @return [String] program name to include in log messages
96
+ # @see http://is.gd/5uHGK0
97
+ delegate_to_first_logger :progname
98
+
99
+ # Sets the program name to include in log messages.
100
+ #
101
+ # @param progname [String] the program name to include in log messages
102
+ # @see http://is.gd/f2U5Xj
103
+ delegate_to_all_loggers :progname=
104
+
105
+ # @return [String] the date format being used
106
+ # @see http://is.gd/btmFWJ
107
+ delegate_to_first_logger :datetime_format
108
+
109
+ # Sets the date format being used.
110
+ #
111
+ # @param format [String] the date format
112
+ # @see http://is.gd/M36ml8
113
+ delegate_to_all_loggers :datetime_format=
114
+
115
+ # Log a message if the given severity is high enough.
116
+ #
117
+ # @see http://is.gd/5opBW0
118
+ delegate_to_all_loggers :add
119
+
120
+ # Dump one or more messages to info.
121
+ #
122
+ # @param message [#to_s] the message to log
123
+ # @see http://is.gd/BCp5KV
124
+ delegate_to_all_loggers :<<
125
+
126
+ # Log a message with severity of banner (high level).
127
+ #
128
+ # @param message_or_progname [#to_s] the message to log. In the block
129
+ # form, this is the progname to use in the log message.
130
+ # @yield evaluates to the message to log. This is not evaluated unless the
131
+ # logger's level is sufficient to log the message. This allows you to
132
+ # create potentially expensive logging messages that are only called when
133
+ # the logger is configured to show them.
134
+ # @return [nil,true] when the given severity is not high enough (for this
135
+ # particular logger), log no message, and return true
136
+ # @see http://is.gd/pYUCYU
137
+ delegate_to_all_loggers :banner
138
+
139
+ # Log a message with severity of debug.
140
+ #
141
+ # @param message_or_progname [#to_s] the message to log. In the block
142
+ # form, this is the progname to use in the log message.
143
+ # @yield evaluates to the message to log. This is not evaluated unless the
144
+ # logger's level is sufficient to log the message. This allows you to
145
+ # create potentially expensive logging messages that are only called when
146
+ # the logger is configured to show them.
147
+ # @return [nil,true] when the given severity is not high enough (for this
148
+ # particular logger), log no message, and return true
149
+ # @see http://is.gd/Re97Zp
150
+ delegate_to_all_loggers :debug
151
+
152
+ # @return [true,false] whether or not the current severity level
153
+ # allows for the printing of debug messages
154
+ # @see http://is.gd/Iq08xB
155
+ delegate_to_first_logger :debug?
156
+
157
+ # Log a message with severity of info.
158
+ #
159
+ # @param message_or_progname [#to_s] the message to log. In the block
160
+ # form, this is the progname to use in the log message.
161
+ # @yield evaluates to the message to log. This is not evaluated unless the
162
+ # logger's level is sufficient to log the message. This allows you to
163
+ # create potentially expensive logging messages that are only called when
164
+ # the logger is configured to show them.
165
+ # @return [nil,true] when the given severity is not high enough (for this
166
+ # particular logger), log no message, and return true
167
+ # @see http://is.gd/pYUCYU
168
+ delegate_to_all_loggers :info
169
+
170
+ # @return [true,false] whether or not the current severity level
171
+ # allows for the printing of info messages
172
+ # @see http://is.gd/lBtJkT
173
+ delegate_to_first_logger :info?
174
+
175
+ # Log a message with severity of error.
176
+ #
177
+ # @param message_or_progname [#to_s] the message to log. In the block
178
+ # form, this is the progname to use in the log message.
179
+ # @yield evaluates to the message to log. This is not evaluated unless the
180
+ # logger's level is sufficient to log the message. This allows you to
181
+ # create potentially expensive logging messages that are only called when
182
+ # the logger is configured to show them.
183
+ # @return [nil,true] when the given severity is not high enough (for this
184
+ # particular logger), log no message, and return true
185
+ # @see http://is.gd/mLwYMl
186
+ delegate_to_all_loggers :error
187
+
188
+ # @return [true,false] whether or not the current severity level
189
+ # allows for the printing of error messages
190
+ # @see http://is.gd/QY19JL
191
+ delegate_to_first_logger :error?
192
+
193
+ # Log a message with severity of warn.
194
+ #
195
+ # @param message_or_progname [#to_s] the message to log. In the block
196
+ # form, this is the progname to use in the log message.
197
+ # @yield evaluates to the message to log. This is not evaluated unless the
198
+ # logger's level is sufficient to log the message. This allows you to
199
+ # create potentially expensive logging messages that are only called when
200
+ # the logger is configured to show them.
201
+ # @return [nil,true] when the given severity is not high enough (for this
202
+ # particular logger), log no message, and return true
203
+ # @see http://is.gd/PX9AIS
204
+ delegate_to_all_loggers :warn
205
+
206
+ # @return [true,false] whether or not the current severity level
207
+ # allows for the printing of warn messages
208
+ # @see http://is.gd/Gdr4lD
209
+ delegate_to_first_logger :warn?
210
+
211
+ # Log a message with severity of fatal.
212
+ #
213
+ # @param message_or_progname [#to_s] the message to log. In the block
214
+ # form, this is the progname to use in the log message.
215
+ # @yield evaluates to the message to log. This is not evaluated unless the
216
+ # logger's level is sufficient to log the message. This allows you to
217
+ # create potentially expensive logging messages that are only called when
218
+ # the logger is configured to show them.
219
+ # @return [nil,true] when the given severity is not high enough (for this
220
+ # particular logger), log no message, and return true
221
+ # @see http://is.gd/5ElFPK
222
+ delegate_to_all_loggers :fatal
223
+
224
+ # @return [true,false] whether or not the current severity level
225
+ # allows for the printing of fatal messages
226
+ # @see http://is.gd/7PgwRl
227
+ delegate_to_first_logger :fatal?
228
+
229
+ # Log a message with severity of unknown.
230
+ #
231
+ # @param message_or_progname [#to_s] the message to log. In the block
232
+ # form, this is the progname to use in the log message.
233
+ # @yield evaluates to the message to log. This is not evaluated unless the
234
+ # logger's level is sufficient to log the message. This allows you to
235
+ # create potentially expensive logging messages that are only called when
236
+ # the logger is configured to show them.
237
+ # @return [nil,true] when the given severity is not high enough (for this
238
+ # particular logger), log no message, and return true
239
+ # @see http://is.gd/Y4hqpf
240
+ delegate_to_all_loggers :unknown
241
+
242
+ # Close the logging devices.
243
+ #
244
+ # @see http://is.gd/b13cVn
245
+ delegate_to_all_loggers :close
246
+
65
247
  private
66
248
 
249
+ # @return [Integer] the default logger level
250
+ # @api private
67
251
  def default_log_level
68
252
  Util.to_logger_level(Kitchen::DEFAULT_LOG_LEVEL)
69
253
  end
70
254
 
255
+ # Construct a new standard out logger.
256
+ #
257
+ # @param stdout [IO] the IO object that represents stdout (or similar)
258
+ # @param color [Symbol] color to use when outputing messages
259
+ # @return [StdoutLogger] a new logger
260
+ # @api private
71
261
  def stdout_logger(stdout, color)
72
262
  logger = StdoutLogger.new(stdout)
73
- logger.formatter = proc do |severity, datetime, progname, msg|
74
- Color.colorize("#{msg}\n", color)
263
+ if Kitchen.tty?
264
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
265
+ Color.colorize("#{msg}", color).concat("\n")
266
+ end
267
+ else
268
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
269
+ msg.concat("\n")
270
+ end
75
271
  end
76
272
  logger
77
273
  end
78
274
 
275
+ # Construct a new logdev logger.
276
+ #
277
+ # @param filepath_or_logdev [String,IO] a filepath String or IO object
278
+ # @return [LogdevLogger] a new logger
279
+ # @api private
79
280
  def logdev_logger(filepath_or_logdev)
80
281
  LogdevLogger.new(resolve_logdev(filepath_or_logdev))
81
282
  end
82
283
 
284
+ # Return an IO object from a filepath String or the IO object itself.
285
+ #
286
+ # @param filepath_or_logdev [String,IO] a filepath String or IO object
287
+ # @return [IO] an IO object
288
+ # @api private
83
289
  def resolve_logdev(filepath_or_logdev)
84
290
  if filepath_or_logdev.is_a? String
85
291
  FileUtils.mkdir_p(File.dirname(filepath_or_logdev))
@@ -97,21 +303,39 @@ module Kitchen
97
303
 
98
304
  alias_method :super_info, :info
99
305
 
306
+ # Dump one or more messages to info.
307
+ #
308
+ # @param msg [String] a message
100
309
  def <<(msg)
101
- msg =~ /\n/ ? msg.split("\n").each { |l| format_line(l) } : super
310
+ @buffer ||= ""
311
+ lines, _, remainder = msg.rpartition("\n")
312
+ if lines.empty?
313
+ @buffer << remainder
314
+ else
315
+ lines.insert(0, @buffer)
316
+ lines.split("\n").each { |l| format_line(l.chomp) }
317
+ @buffer = ""
318
+ end
102
319
  end
103
320
 
321
+ # Log a banner message.
322
+ #
323
+ # @param msg [String] a message
104
324
  def banner(msg = nil, &block)
105
325
  super_info("-----> #{msg}", &block)
106
326
  end
107
327
 
108
328
  private
109
329
 
330
+ # Reformat a line if it already contains log formatting.
331
+ #
332
+ # @param line [String] a message line
333
+ # @api private
110
334
  def format_line(line)
111
335
  case line
112
- when %r{^-----> } then banner(line.gsub(%r{^[ >-]{6} }, ''))
113
- when %r{^>>>>>> } then error(line.gsub(%r{^[ >-]{6} }, ''))
114
- when %r{^ } then info(line.gsub(%r{^[ >-]{6} }, ''))
336
+ when %r{^-----> } then banner(line.gsub(%r{^[ >-]{6} }, ""))
337
+ when %r{^>>>>>> } then error(line.gsub(%r{^[ >-]{6} }, ""))
338
+ when %r{^ } then info(line.gsub(%r{^[ >-]{6} }, ""))
115
339
  else info(line)
116
340
  end
117
341
  end
@@ -121,25 +345,47 @@ module Kitchen
121
345
  # output.
122
346
  class StdoutLogger < LogdevLogger
123
347
 
348
+ # Log a debug message
349
+ #
350
+ # @param msg [String] a message
124
351
  def debug(msg = nil, &block)
125
352
  super("D #{msg}", &block)
126
353
  end
127
354
 
355
+ # Log an info message
356
+ #
357
+ # @param msg [String] a message
128
358
  def info(msg = nil, &block)
129
359
  super(" #{msg}", &block)
130
360
  end
131
361
 
362
+ # Log a warn message
363
+ #
364
+ # @param msg [String] a message
132
365
  def warn(msg = nil, &block)
133
366
  super("$$$$$$ #{msg}", &block)
134
367
  end
135
368
 
369
+ # Log an error message
370
+ #
371
+ # @param msg [String] a message
136
372
  def error(msg = nil, &block)
137
373
  super(">>>>>> #{msg}", &block)
138
374
  end
139
375
 
376
+ # Log a fatal message
377
+ #
378
+ # @param msg [String] a message
140
379
  def fatal(msg = nil, &block)
141
380
  super("!!!!!! #{msg}", &block)
142
381
  end
382
+
383
+ # Log an unknown message
384
+ #
385
+ # @param msg [String] a message
386
+ def unknown(msg = nil, &block)
387
+ super("?????? #{msg}", &block)
388
+ end
143
389
  end
144
390
  end
145
391
  end