test-kitchen 1.0.0.alpha.4 → 1.0.0.alpha.5

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.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 1.0.0.alpha.5 / 2013-04-23
2
+
3
+ ### Improvements
4
+
5
+ * Pull request [#81][]: Clean up error reporting in CLI output. ([@fnichol][])
6
+ * Pull request [#76][]: Swap out shell-based kb for Ruby-based Busser gem. ([@fnichol][])
7
+ * Pull request [#82][], issue [#61][]: Install Omnibus package via either wget or curl. ([@fnichol][])
8
+ * Catch YAML data merging errors as user errors. ([@fnichol][])
9
+ * Issue [#80][]: Add a more helpful error message when a driver could not be loaded. ([@fnichol][])
10
+
11
+
1
12
  ## 1.0.0.alpha.4 / 2013-04-10
2
13
 
3
14
  ### Bug fixes
@@ -70,11 +81,16 @@
70
81
  The initial release.
71
82
 
72
83
  <!--- The following link definition list is generated by PimpMyChangelog --->
84
+ [#61]: https://github.com/opscode/test/issues/61
73
85
  [#64]: https://github.com/opscode/test/issues/64
74
86
  [#65]: https://github.com/opscode/test/issues/65
75
87
  [#71]: https://github.com/opscode/test/issues/71
76
88
  [#73]: https://github.com/opscode/test/issues/73
77
89
  [#74]: https://github.com/opscode/test/issues/74
90
+ [#76]: https://github.com/opscode/test/issues/76
91
+ [#80]: https://github.com/opscode/test/issues/80
92
+ [#81]: https://github.com/opscode/test/issues/81
93
+ [#82]: https://github.com/opscode/test/issues/82
78
94
  [@ChrisLundquist]: https://github.com/ChrisLundquist
79
95
  [@bryanwb]: https://github.com/bryanwb
80
96
  [@fnichol]: https://github.com/fnichol
data/bin/kitchen CHANGED
@@ -8,5 +8,6 @@ Signal.trap("INT") { exit 1 }
8
8
  $:.unshift File.join(File.dirname(__FILE__), %w{.. lib})
9
9
  require 'rubygems'
10
10
  require 'kitchen/cli'
11
+ require 'kitchen/errors'
11
12
 
12
- Kitchen::CLI.start
13
+ Kitchen.with_friendly_errors { Kitchen::CLI.start }
@@ -18,18 +18,17 @@
18
18
 
19
19
  require 'base64'
20
20
  require 'digest'
21
- require 'net/https'
22
21
 
23
22
  module Kitchen
24
23
 
25
- # Command string generator to interface with Kitchen Busser (kb). The
26
- # commands that are generated are safe to pass to an SSH command or as an
27
- # unix command argument (escaped in single quotes).
24
+ # Command string generator to interface with Busser. The commands that are
25
+ # generated are safe to pass to an SSH command or as an unix command
26
+ # argument (escaped in single quotes).
28
27
  #
29
28
  # @author Fletcher Nichol <fnichol@nichol.ca>
30
29
  class Busser
31
30
 
32
- # Constructs a new busser command generator, given a suite name.
31
+ # Constructs a new Busser command generator, given a suite name.
33
32
  #
34
33
  # @param [String] suite_name name of suite on which to operate
35
34
  # (**Required**)
@@ -43,8 +42,8 @@ module Kitchen
43
42
  @use_sudo = opts[:use_sudo]
44
43
  end
45
44
 
46
- # Returns a command string which installs the Kitchen Busser (kb), installs
47
- # all required kb plugins for the suite.
45
+ # Returns a command string which installs Busser, and installs all
46
+ # required Busser plugins for the suite.
48
47
  #
49
48
  # If no work needs to be performed, for example if there are no tests for
50
49
  # the given suite, then `nil` will be returned.
@@ -56,11 +55,11 @@ module Kitchen
56
55
  nil
57
56
  else
58
57
  <<-INSTALL_CMD.gsub(/^ {10}/, '')
59
- #{sudo}#{ruby_bin} -e "$(cat <<"EOF"
60
- #{install_script}
61
- EOF
62
- )"
63
- #{sudo}#{kb_bin} install #{plugins.join(' ')}
58
+ if ! #{sudo}#{ruby_binpath}/gem list busser -i >/dev/null ; then
59
+ #{sudo}#{ruby_binpath}/gem install #{busser_gem} --no-rdoc --no-ri
60
+ fi
61
+ #{sudo}#{ruby_binpath}/busser setup
62
+ #{sudo}#{busser_bin} plugin install #{plugins.join(' ')}
64
63
  INSTALL_CMD
65
64
  end
66
65
  end
@@ -78,13 +77,13 @@ module Kitchen
78
77
  nil
79
78
  else
80
79
  <<-INSTALL_CMD.gsub(/^ {10}/, '')
81
- #{sudo}#{kb_bin} cleanup-suites
80
+ #{sudo}#{busser_bin} suite cleanup
82
81
  #{local_suite_files.map { |f| stream_file(f, remote_file(f)) }.join}
83
82
  INSTALL_CMD
84
83
  end
85
84
  end
86
85
 
87
- # Returns a command string which runs all kb suite tests for the suite.
86
+ # Returns a command string which runs all Busser suite tests for the suite.
88
87
  #
89
88
  # If no work needs to be performed, for example if there are no tests for
90
89
  # the given suite, then `nil` will be returned.
@@ -92,34 +91,23 @@ module Kitchen
92
91
  # @return [String] a command string to run the test suites, or nil if no
93
92
  # work needs to be performed
94
93
  def run_cmd
95
- @run_cmd ||= local_suite_files.empty? ? nil : "#{sudo}#{kb_bin} test"
94
+ @run_cmd ||= local_suite_files.empty? ? nil : "#{sudo}#{busser_bin} test"
96
95
  end
97
96
 
98
97
  private
99
98
 
100
- INSTALL_URL = "https://raw.github.com/opscode/kb/go".freeze
101
99
  DEFAULT_RUBY_BINPATH = "/opt/chef/embedded/bin".freeze
102
- DEFAULT_KB_ROOT = "/opt/kb".freeze
100
+ DEFAULT_BUSSER_ROOT = "/opt/busser".freeze
103
101
  DEFAULT_TEST_ROOT = File.join(Dir.pwd, "test/integration").freeze
104
102
 
105
103
  def validate_options(suite_name)
106
104
  raise ClientError, "Busser#new requires a suite_name" if suite_name.nil?
107
105
  end
108
106
 
109
- def install_script
110
- @install_script ||= begin
111
- uri = URI.parse(INSTALL_URL)
112
- http = Net::HTTP.new(uri.host, 443)
113
- http.use_ssl = true
114
- response = http.request(Net::HTTP::Get.new(uri.path))
115
- response.body
116
- end
117
- end
118
-
119
107
  def plugins
120
108
  Dir.glob(File.join(test_root, @suite_name, "*")).select { |d|
121
109
  File.directory?(d) && File.basename(d) != "data_bags"
122
- }.map { |d| File.basename(d) }.sort.uniq
110
+ }.map { |d| "busser-#{File.basename(d)}" }.sort.uniq
123
111
  end
124
112
 
125
113
  def local_suite_files
@@ -130,18 +118,24 @@ module Kitchen
130
118
 
131
119
  def remote_file(file)
132
120
  local_prefix = File.join(test_root, @suite_name)
133
- "$(#{kb_bin} suitepath)/".concat(file.sub(%r{^#{local_prefix}/}, ''))
121
+ "$(#{busser_bin} suite path)/".concat(file.sub(%r{^#{local_prefix}/}, ''))
134
122
  end
135
123
 
136
124
  def stream_file(local_path, remote_path)
137
125
  local_file = IO.read(local_path)
138
126
  md5 = Digest::MD5.hexdigest(local_file)
139
- perms = sprintf("%o", File.stat(local_path).mode)[3, 3]
140
- kb_stream_file = "#{kb_bin} stream-file #{remote_path} #{md5} #{perms}"
127
+ perms = sprintf("%o", File.stat(local_path).mode)[2, 4]
128
+ stream_cmd = [
129
+ busser_bin,
130
+ "deserialize",
131
+ "--destination=#{remote_path}",
132
+ "--md5sum=#{md5}",
133
+ "--perms=#{perms}"
134
+ ].join(" ")
141
135
 
142
136
  <<-STREAMFILE.gsub(/^ {8}/, '')
143
137
  echo "Uploading #{remote_path} (mode=#{perms})"
144
- cat <<"__EOFSTREAM__" | #{sudo}#{kb_stream_file}
138
+ cat <<"__EOFSTREAM__" | #{sudo}#{stream_cmd}
145
139
  #{Base64.encode64(local_file)}
146
140
  __EOFSTREAM__
147
141
  STREAMFILE
@@ -151,12 +145,16 @@ module Kitchen
151
145
  @use_sudo ? "sudo " : ""
152
146
  end
153
147
 
154
- def ruby_bin
155
- File.join(DEFAULT_RUBY_BINPATH, "ruby")
148
+ def ruby_binpath
149
+ DEFAULT_RUBY_BINPATH
150
+ end
151
+
152
+ def busser_bin
153
+ File.join(DEFAULT_BUSSER_ROOT, "bin/busser")
156
154
  end
157
155
 
158
- def kb_bin
159
- File.join(DEFAULT_KB_ROOT, "bin/kb")
156
+ def busser_gem
157
+ "busser"
160
158
  end
161
159
 
162
160
  def test_root
data/lib/kitchen/cli.rb CHANGED
@@ -39,12 +39,12 @@ module Kitchen
39
39
  def initialize(*args)
40
40
  super
41
41
  $stdout.sync = true
42
+ Kitchen.logger = Kitchen.default_file_logger
42
43
  @config = Kitchen::Config.new(
43
44
  :loader => Kitchen::Loader::YAML.new(ENV['KITCHEN_YAML']),
44
45
  :log_level => ENV['KITCHEN_LOG'] && ENV['KITCHEN_LOG'].downcase.to_sym,
45
46
  :supervised => false
46
47
  )
47
- Kitchen.logger = Kitchen.default_file_logger
48
48
  end
49
49
 
50
50
  desc "list [(all|<REGEX>)]", "List all instances"
@@ -304,12 +304,14 @@ module Kitchen
304
304
  when nil then set_color("<Not Created>", :red)
305
305
  else set_color("<Unknown>", :white)
306
306
  end
307
- [set_color(instance.name, :white), action]
307
+ [instance.name, action]
308
308
  end
309
309
 
310
310
  def update_config!
311
311
  if options[:log_level]
312
- @config.log_level = options[:log_level].downcase.to_sym
312
+ level = options[:log_level].downcase.to_sym
313
+ @config.log_level = level
314
+ Kitchen.logger.level = Util.to_logger_level(level)
313
315
  end
314
316
  end
315
317
 
@@ -146,15 +146,15 @@ module Kitchen
146
146
  super(cmd, base_options)
147
147
  end
148
148
 
149
- def kb_setup_cmd
149
+ def busser_setup_cmd
150
150
  busser.setup_cmd
151
151
  end
152
152
 
153
- def kb_sync_cmd
153
+ def busser_sync_cmd
154
154
  busser.sync_cmd
155
155
  end
156
156
 
157
- def kb_run_cmd
157
+ def busser_run_cmd
158
158
  busser.run_cmd
159
159
  end
160
160
 
@@ -47,17 +47,17 @@ module Kitchen
47
47
  def setup(state)
48
48
  ssh_args = build_ssh_args(state)
49
49
 
50
- if kb_setup_cmd
51
- ssh(ssh_args, kb_setup_cmd)
50
+ if busser_setup_cmd
51
+ ssh(ssh_args, busser_setup_cmd)
52
52
  end
53
53
  end
54
54
 
55
55
  def verify(state)
56
56
  ssh_args = build_ssh_args(state)
57
57
 
58
- if kb_run_cmd
59
- ssh(ssh_args, kb_sync_cmd)
60
- ssh(ssh_args, kb_run_cmd)
58
+ if busser_run_cmd
59
+ ssh(ssh_args, busser_sync_cmd)
60
+ ssh(ssh_args, busser_run_cmd)
61
61
  end
62
62
  end
63
63
 
@@ -98,6 +98,7 @@ module Kitchen
98
98
  end
99
99
 
100
100
  def install_omnibus(ssh_args)
101
+ url = "https://www.opscode.com/chef/install.sh"
101
102
  flag = config[:require_chef_omnibus]
102
103
  version = if flag.is_a?(String) && flag != "latest"
103
104
  "-s -- -v #{flag.downcase}"
@@ -115,8 +116,14 @@ module Kitchen
115
116
 
116
117
  if [ ! -d "/opt/chef" ] || should_update_chef ; then
117
118
  echo "-----> Installing Chef Omnibus (#{flag})"
118
- curl -sSL https://www.opscode.com/chef/install.sh \
119
- | sudo bash #{version}
119
+ if command -v wget &>/dev/null ; then
120
+ wget #{url} -O - | sudo bash #{version}
121
+ elif command -v curl &>/dev/null ; then
122
+ curl -sSL #{url} | sudo bash #{version}
123
+ else
124
+ echo ">>>>>> Neither wget nor curl found on this instance."
125
+ exit 1
126
+ fi
120
127
  fi
121
128
  INSTALL
122
129
  end
@@ -39,7 +39,10 @@ module Kitchen
39
39
  rescue UserError
40
40
  raise
41
41
  rescue LoadError, NameError
42
- raise ClientError, "Could not require '#{plugin}' plugin from load path"
42
+ raise ClientError,
43
+ "Could not load the '#{plugin}' driver from the load path." +
44
+ " Please ensure that your driver is installed as a gem or included" +
45
+ " in your Gemfile if using Bundler."
43
46
  end
44
47
  end
45
48
  end
@@ -18,7 +18,28 @@
18
18
 
19
19
  module Kitchen
20
20
 
21
- module Error ; end
21
+ module Error
22
+
23
+ def self.formatted_trace(exception)
24
+ arr = formatted_exception(exception).dup
25
+ last = arr.pop
26
+ if exception.respond_to?(:original) && exception.original
27
+ arr += formatted_exception(exception.original, "Nested Exception")
28
+ last = arr.pop
29
+ end
30
+ arr += ["Backtrace".center(22, "-"), exception.backtrace, last].flatten
31
+ arr
32
+ end
33
+
34
+ def self.formatted_exception(exception, title = "Exception")
35
+ [
36
+ title.center(22, "-"),
37
+ "Class: #{exception.class}",
38
+ "Message: #{exception.message}",
39
+ "".center(22, "-"),
40
+ ]
41
+ end
42
+ end
22
43
 
23
44
  # Base exception class from which all Kitchen exceptions derive. This class
24
45
  # nests an exception when this class is re-raised from a rescue block.
@@ -49,4 +70,56 @@ module Kitchen
49
70
  # Exception class for any exceptions raised when performing an instance
50
71
  # action.
51
72
  class ActionFailed < TransientFailure ; end
73
+
74
+ # Exception class capturing what caused an instance to die.
75
+ class InstanceFailure < TransientFailure ; end
76
+
77
+ def self.with_friendly_errors
78
+ yield
79
+ rescue Kitchen::InstanceFailure => e
80
+ Kitchen.mutex.synchronize do
81
+ handle_instance_failure(e)
82
+ end
83
+ exit 10
84
+ rescue Kitchen::Error => e
85
+ Kitchen.mutex.synchronize do
86
+ handle_error(e)
87
+ end
88
+ exit 20
89
+ end
90
+
91
+ private
92
+
93
+ def self.file_log(level, lines)
94
+ Array(lines).each do |line|
95
+ if Kitchen.logger.debug?
96
+ Kitchen.logger.debug(line)
97
+ else
98
+ Kitchen.logger.logdev && Kitchen.logger.logdev.public_send(level, line)
99
+ end
100
+ end
101
+ end
102
+
103
+ def self.stderr_log(lines)
104
+ Array(lines).each do |line|
105
+ $stderr.puts(Color.colorize(">>>>>> #{line}", :red))
106
+ end
107
+ end
108
+
109
+ def self.debug_log(lines)
110
+ Array(lines).each { |line| Kitchen.logger.debug(line) }
111
+ end
112
+
113
+ def self.handle_instance_failure(e)
114
+ stderr_log(e.message.split(/\s{2,}/))
115
+ stderr_log(Error.formatted_exception(e.original))
116
+ file_log(:error, e.message.split(/\s{2,}/).first)
117
+ debug_log(Error.formatted_trace(e))
118
+ end
119
+
120
+ def self.handle_error(e)
121
+ stderr_log(Error.formatted_exception(e))
122
+ stderr_log("Please see .kitchen/logs/kitchen.log for more details\n")
123
+ file_log(:error, Error.formatted_trace(e))
124
+ end
52
125
  end
@@ -260,10 +260,14 @@ module Kitchen
260
260
  end
261
261
  state[:last_action] = what.to_s
262
262
  elapsed
263
- rescue ActionFailed
264
- raise
263
+ rescue ActionFailed => e
264
+ log_failure(what, e)
265
+ raise InstanceFailure, failure_message(what) +
266
+ " Please see .kitchen/logs/#{self.name}.log for more details", caller
265
267
  rescue Exception => e
266
- raise ActionFailed, "Failed to complete ##{what} action: [#{e.message}]"
268
+ log_failure(what, e)
269
+ raise ActionFailed,
270
+ "Failed to complete ##{what} action: [#{e.message}]", caller
267
271
  ensure
268
272
  state_file.write(state)
269
273
  end
@@ -289,6 +293,17 @@ module Kitchen
289
293
  super
290
294
  end
291
295
 
296
+ def log_failure(what, e)
297
+ return if logger.logdev.nil?
298
+
299
+ logger.logdev.error(failure_message(what))
300
+ Error.formatted_trace(e).each { |line| logger.logdev.error(line) }
301
+ end
302
+
303
+ def failure_message(what)
304
+ "#{what.capitalize} failed on instance #{self.to_str}."
305
+ end
306
+
292
307
  # The simplest finite state machine pseudo-implementation needed to manage
293
308
  # an Instance.
294
309
  #
@@ -69,6 +69,9 @@ module Kitchen
69
69
 
70
70
  def combined_hash
71
71
  @process_local ? yaml.rmerge(local_yaml) : yaml
72
+ rescue NoMethodError
73
+ raise UserError, "Error merging #{File.basename(config_file)} and" +
74
+ "#{File.basename(local_config_file)}"
72
75
  end
73
76
 
74
77
  def yaml
@@ -97,8 +100,8 @@ module Kitchen
97
100
  return Hash.new if string.nil? || string.empty?
98
101
 
99
102
  ::YAML.safe_load(string)
100
- rescue SyntaxError, Psych::SyntaxError => ex
101
- raise UserError, "Error parsing #{file_name} (#{ex.message})"
103
+ rescue SyntaxError, Psych::SyntaxError
104
+ raise UserError, "Error parsing #{file_name}"
102
105
  end
103
106
  end
104
107
  end
@@ -34,7 +34,7 @@ module Kitchen
34
34
  attr_reader :logdev
35
35
 
36
36
  def initialize(options = {})
37
- color = options[:color] || :bright_white
37
+ color = options[:color]
38
38
 
39
39
  @loggers = []
40
40
  @loggers << @logdev = logdev_logger(options[:logdev]) if options[:logdev]
@@ -18,5 +18,5 @@
18
18
 
19
19
  module Kitchen
20
20
 
21
- VERSION = "1.0.0.alpha.4"
21
+ VERSION = "1.0.0.alpha.5"
22
22
  end
data/lib/kitchen.rb CHANGED
@@ -72,6 +72,11 @@ module Kitchen
72
72
  Logger.new(:stdout => STDOUT, :logdev => logfile, :level => env_log)
73
73
  end
74
74
 
75
+ def celluloid_file_logger
76
+ logfile = File.expand_path(File.join(".kitchen", "logs", "celluloid.log"))
77
+ Logger.new(:logdev => logfile, :level => env_log, :progname => "Celluloid")
78
+ end
79
+
75
80
  private
76
81
 
77
82
  def env_log
@@ -86,7 +91,7 @@ end
86
91
 
87
92
  # Initialize the base logger and use that for Celluloid's logger
88
93
  Kitchen.logger = Kitchen.default_logger
89
- Celluloid.logger = Kitchen.logger
94
+ Celluloid.logger = Kitchen.celluloid_file_logger
90
95
 
91
96
  # Setup a collection of instance crash exceptions for error reporting
92
97
  Kitchen.crashes = []
@@ -152,6 +152,13 @@ describe Kitchen::Loader::YAML do
152
152
  proc { loader.read }.must_raise Kitchen::UserError
153
153
  end
154
154
 
155
+ it "raises a UserError if kitchen.yml cannot be parsed" do
156
+ FileUtils.mkdir_p "/tmp"
157
+ File.open("/tmp/.kitchen.yml", "wb") { |f| f.write 'uhoh' }
158
+
159
+ proc { loader.read }.must_raise Kitchen::UserError
160
+ end
161
+
155
162
  it "raises a UserError if kitchen.local.yml cannot be parsed" do
156
163
  FileUtils.mkdir_p "/tmp"
157
164
  File.open("/tmp/.kitchen.local.yml", "wb") { |f| f.write '&*%^*' }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-kitchen
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.alpha.4
4
+ version: 1.0.0.alpha.5
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-10 00:00:00.000000000 Z
12
+ date: 2013-04-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: celluloid