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 +16 -0
- data/bin/kitchen +2 -1
- data/lib/kitchen/busser.rb +34 -36
- data/lib/kitchen/cli.rb +5 -3
- data/lib/kitchen/driver/base.rb +3 -3
- data/lib/kitchen/driver/ssh_base.rb +14 -7
- data/lib/kitchen/driver.rb +4 -1
- data/lib/kitchen/errors.rb +74 -1
- data/lib/kitchen/instance.rb +18 -3
- data/lib/kitchen/loader/yaml.rb +5 -2
- data/lib/kitchen/logger.rb +1 -1
- data/lib/kitchen/version.rb +1 -1
- data/lib/kitchen.rb +6 -1
- data/spec/kitchen/loader/yaml_spec.rb +7 -0
- metadata +2 -2
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
data/lib/kitchen/busser.rb
CHANGED
@@ -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
|
26
|
-
#
|
27
|
-
#
|
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
|
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
|
47
|
-
#
|
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}#{
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
#{sudo}#{
|
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}#{
|
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
|
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}#{
|
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
|
-
|
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
|
-
"$(#{
|
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)[
|
140
|
-
|
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}#{
|
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
|
155
|
-
|
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
|
159
|
-
|
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
|
-
[
|
307
|
+
[instance.name, action]
|
308
308
|
end
|
309
309
|
|
310
310
|
def update_config!
|
311
311
|
if options[:log_level]
|
312
|
-
|
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
|
|
data/lib/kitchen/driver/base.rb
CHANGED
@@ -146,15 +146,15 @@ module Kitchen
|
|
146
146
|
super(cmd, base_options)
|
147
147
|
end
|
148
148
|
|
149
|
-
def
|
149
|
+
def busser_setup_cmd
|
150
150
|
busser.setup_cmd
|
151
151
|
end
|
152
152
|
|
153
|
-
def
|
153
|
+
def busser_sync_cmd
|
154
154
|
busser.sync_cmd
|
155
155
|
end
|
156
156
|
|
157
|
-
def
|
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
|
51
|
-
ssh(ssh_args,
|
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
|
59
|
-
ssh(ssh_args,
|
60
|
-
ssh(ssh_args,
|
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
|
-
|
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
|
data/lib/kitchen/driver.rb
CHANGED
@@ -39,7 +39,10 @@ module Kitchen
|
|
39
39
|
rescue UserError
|
40
40
|
raise
|
41
41
|
rescue LoadError, NameError
|
42
|
-
raise ClientError,
|
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
|
data/lib/kitchen/errors.rb
CHANGED
@@ -18,7 +18,28 @@
|
|
18
18
|
|
19
19
|
module Kitchen
|
20
20
|
|
21
|
-
module Error
|
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
|
data/lib/kitchen/instance.rb
CHANGED
@@ -260,10 +260,14 @@ module Kitchen
|
|
260
260
|
end
|
261
261
|
state[:last_action] = what.to_s
|
262
262
|
elapsed
|
263
|
-
rescue ActionFailed
|
264
|
-
|
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
|
-
|
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
|
#
|
data/lib/kitchen/loader/yaml.rb
CHANGED
@@ -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
|
101
|
-
raise UserError, "Error parsing #{file_name}
|
103
|
+
rescue SyntaxError, Psych::SyntaxError
|
104
|
+
raise UserError, "Error parsing #{file_name}"
|
102
105
|
end
|
103
106
|
end
|
104
107
|
end
|
data/lib/kitchen/logger.rb
CHANGED
data/lib/kitchen/version.rb
CHANGED
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.
|
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
|
+
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-
|
12
|
+
date: 2013-04-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: celluloid
|