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

Sign up to get free protection for your applications and to get access to all the features.
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