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