test-kitchen 0.5.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 (37) hide show
  1. data/bin/kitchen +7 -0
  2. data/config/Cheffile +55 -0
  3. data/config/Kitchenfile +39 -0
  4. data/config/Vagrantfile +109 -0
  5. data/cookbooks/test-kitchen/attributes/default.rb +25 -0
  6. data/cookbooks/test-kitchen/libraries/helpers.rb +25 -0
  7. data/cookbooks/test-kitchen/metadata.rb +27 -0
  8. data/cookbooks/test-kitchen/recipes/chef.rb +19 -0
  9. data/cookbooks/test-kitchen/recipes/compat.rb +39 -0
  10. data/cookbooks/test-kitchen/recipes/default.rb +51 -0
  11. data/cookbooks/test-kitchen/recipes/erlang.rb +19 -0
  12. data/cookbooks/test-kitchen/recipes/ruby.rb +40 -0
  13. data/lib/test-kitchen.rb +34 -0
  14. data/lib/test-kitchen/cli.rb +267 -0
  15. data/lib/test-kitchen/cli/destroy.rb +42 -0
  16. data/lib/test-kitchen/cli/init.rb +37 -0
  17. data/lib/test-kitchen/cli/platform_list.rb +37 -0
  18. data/lib/test-kitchen/cli/project_info.rb +44 -0
  19. data/lib/test-kitchen/cli/ssh.rb +36 -0
  20. data/lib/test-kitchen/cli/status.rb +36 -0
  21. data/lib/test-kitchen/cli/test.rb +60 -0
  22. data/lib/test-kitchen/dsl.rb +59 -0
  23. data/lib/test-kitchen/environment.rb +164 -0
  24. data/lib/test-kitchen/platform.rb +63 -0
  25. data/lib/test-kitchen/project.rb +23 -0
  26. data/lib/test-kitchen/project/base.rb +159 -0
  27. data/lib/test-kitchen/project/cookbook.rb +87 -0
  28. data/lib/test-kitchen/project/cookbook_copy.rb +58 -0
  29. data/lib/test-kitchen/project/ruby.rb +37 -0
  30. data/lib/test-kitchen/project/supported_platforms.rb +75 -0
  31. data/lib/test-kitchen/runner.rb +20 -0
  32. data/lib/test-kitchen/runner/base.rb +144 -0
  33. data/lib/test-kitchen/runner/vagrant.rb +92 -0
  34. data/lib/test-kitchen/scaffold.rb +73 -0
  35. data/lib/test-kitchen/ui.rb +73 -0
  36. data/lib/test-kitchen/version.rb +21 -0
  37. metadata +196 -0
@@ -0,0 +1,159 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/mixin/params_validate'
20
+
21
+ module TestKitchen
22
+ module Project
23
+ class Base
24
+ include Chef::Mixin::ParamsValidate
25
+
26
+ PROJECT_ROOT_INDICATORS = ["Gemfile", "metadata.rb"]
27
+
28
+ attr_reader :name, :guest_source_root, :guest_test_root, :exclusions
29
+ attr_writer :language, :runtimes, :install, :script, :configurations, :root_path, :memory
30
+ attr_accessor :vm
31
+
32
+ def initialize(name, parent=nil, &block)
33
+ raise ArgumentError, "Project name must be specified" if name.nil? || name.empty?
34
+ @name = name
35
+ @parent = parent
36
+ @configurations = {}
37
+ @exclusions = []
38
+ @guest_source_root = '/test-kitchen/source'
39
+ @guest_test_root = '/test-kitchen/test'
40
+ instance_eval(&block) if block_given?
41
+ end
42
+
43
+ def each_build(platforms, active_config=nil)
44
+ raise ArgumentError if platforms.nil? || ! block_given?
45
+ c = Array(active_config ?
46
+ configurations[active_config] : configurations.values)
47
+ platforms.to_a.product(c).each do |platform,configuration|
48
+ yield [platform, configuration] unless exclusions.any? do |e|
49
+ e[:platform] == platform.split('-').first &&
50
+ ((! e[:configuration]) || e[:configuration] == configuration.name)
51
+ end
52
+ end
53
+ end
54
+
55
+ def configuration(name, &block)
56
+ @configurations[name] = self.class.new(name, self, &block)
57
+ end
58
+
59
+ def configurations
60
+ @configurations.empty? ? {:default => self} : @configurations
61
+ end
62
+
63
+ def tests_tag
64
+ @parent.nil? ? 'default' : name
65
+ end
66
+
67
+ def exclude(exclusion)
68
+ @exclusions << exclusion
69
+ end
70
+
71
+ def run_list
72
+ ['test-kitchen::default'] + run_list_extras
73
+ end
74
+
75
+ def run_list_extras(arg=nil)
76
+ set_or_return(:run_list_extras, arg, :default => [])
77
+ end
78
+
79
+ def runner(arg=nil)
80
+ set_or_return(:runner, arg, :default => 'vagrant')
81
+ end
82
+
83
+ def language(arg=nil)
84
+ set_or_return(:language, arg, :default => 'ruby')
85
+ end
86
+
87
+ def runtimes(arg=nil)
88
+ set_or_return(:runtimes, arg, :default =>
89
+ if language == 'ruby' || language == 'chef'
90
+ ['1.9.2']
91
+ else
92
+ []
93
+ end)
94
+ end
95
+
96
+ def install(arg=nil)
97
+ set_or_return(:install, arg,
98
+ :default => language == 'ruby' ? 'bundle install' : '')
99
+ end
100
+
101
+ def script(arg=nil)
102
+ set_or_return(:script, arg, :default => 'rspec spec')
103
+ end
104
+
105
+ def memory(arg=nil)
106
+ set_or_return(:memory, arg, {})
107
+ end
108
+
109
+ def specs(arg=nil)
110
+ set_or_return(:specs, arg, {:default => true})
111
+ end
112
+
113
+ def features(arg=nil)
114
+ set_or_return(:features, arg, {:default => true})
115
+ end
116
+
117
+ def root_path
118
+ return @root_path if defined?(@root_path)
119
+
120
+ root_finder = lambda do |path|
121
+ found = PROJECT_ROOT_INDICATORS.find do |rootfile|
122
+ File.exist?(File.join(path.to_s, rootfile))
123
+ end
124
+
125
+ return path if found
126
+ return nil if path.root? || !File.exist?(path)
127
+ root_finder.call(path.parent)
128
+ end
129
+
130
+ @root_path = root_finder.call(Pathname.new(Dir.pwd))
131
+ end
132
+
133
+ def update_code_command
134
+ "rsync -aHv --update --progress --checksum #{guest_source_root}/ #{guest_test_root}"
135
+ end
136
+
137
+ def preflight_command
138
+ nil
139
+ end
140
+
141
+ def install_command(runtime=nil)
142
+ raise NotImplementedError
143
+ end
144
+
145
+ def test_command(runtime=nil)
146
+ raise NotImplementedError
147
+ end
148
+
149
+ def to_hash
150
+ self.runtimes # hack
151
+ hash = {}
152
+ self.instance_variables.each do |var|
153
+ hash[var[1..-1].to_sym] = self.instance_variable_get(var)
154
+ end
155
+ hash
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,87 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module TestKitchen
20
+ module Project
21
+ class Cookbook < Ruby
22
+
23
+ include CookbookCopy
24
+ include SupportedPlatforms
25
+
26
+ attr_writer :lint
27
+ attr_writer :supported_platforms
28
+
29
+ def lint(arg=nil)
30
+ set_or_return(:lint, arg, {:default => true})
31
+ end
32
+
33
+ def run_list
34
+ super + ['recipe[minitest-handler]']
35
+ end
36
+
37
+ def preflight_command(cmd = nil)
38
+ return nil unless lint
39
+ parent_dir = File.join(root_path, '..')
40
+ set_or_return(:preflight_command, cmd, :default =>
41
+ "knife cookbook test -o #{parent_dir} #{name}" +
42
+ " && foodcritic -f ~FC007 -f correctness #{root_path}")
43
+ end
44
+
45
+ def script(arg=nil)
46
+ set_or_return(:script, arg, :default =>
47
+ %Q{if [ -d "features" ]; then bundle exec cucumber -t @#{tests_tag} features; fi})
48
+ end
49
+
50
+ def install_command(runtime=nil)
51
+ super(runtime, File.join(guest_test_root, 'test'))
52
+ end
53
+
54
+ def test_command(runtime=nil)
55
+ super(runtime, File.join(guest_test_root, 'test'))
56
+ end
57
+
58
+ def supported_platforms
59
+ @supported_platforms ||= extract_supported_platforms(
60
+ File.read(File.join(root_path, 'metadata.rb')))
61
+ end
62
+
63
+ def non_buildable_platforms(platform_names)
64
+ supported_platforms.sort - platform_names.map do |platform|
65
+ platform.split('-').first
66
+ end.sort.uniq
67
+ end
68
+
69
+ def each_build(platforms, active_config=nil, &block)
70
+ if supported_platforms.empty?
71
+ super(platforms, active_config, &block)
72
+ else
73
+ super(platforms.select do |platform|
74
+ supported_platforms.any? do |supported|
75
+ platform.start_with?("#{supported}-")
76
+ end
77
+ end, active_config, &block)
78
+ end
79
+ end
80
+
81
+ def cookbook_path(root_path, tmp_path)
82
+ @cookbook_path ||= copy_cookbook_under_test(root_path, tmp_path)
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ #
2
+ # Author:: Andrew Crump (<andrew@kotirisoftware.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module TestKitchen
20
+ module Project
21
+ module CookbookCopy
22
+
23
+ # This is a workaround to allow the top-level containing cookbook
24
+ # to be copied to the kitchen tmp subdirectory.
25
+ def copy_cookbook_under_test(root_path, tmp_path)
26
+ cookbook_path = root_path.parent.parent
27
+ source_paths = source_paths_excluding_test_dir(cookbook_path)
28
+ dest_path = File.join(tmp_path, 'cookbook_under_test')
29
+ copy_paths(source_paths, dest_path,
30
+ destination_paths(cookbook_path, source_paths, dest_path))
31
+ dest_path
32
+ end
33
+
34
+ def source_paths_excluding_test_dir(cookbook_path)
35
+ paths_from = Find.find(cookbook_path).reject do |path|
36
+ Find.prune if ['.git', 'test'].map do |dir|
37
+ File.join(cookbook_path, dir)
38
+ end.include?(path)
39
+ end.drop(1)
40
+ end
41
+
42
+ def destination_paths(cookbook_path, paths_from, dest_path)
43
+ paths_from.map do |file|
44
+ File.join(dest_path,
45
+ file.to_s.sub(%r{^#{Regexp.escape(cookbook_path.to_s)}/}, ''))
46
+ end
47
+ end
48
+
49
+ def copy_paths(source_paths, dest_path, dest_paths)
50
+ FileUtils.mkdir_p(dest_path)
51
+ source_paths.each_with_index do |from_path, index|
52
+ FileUtils.cp_r(from_path, dest_paths[index])
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module TestKitchen
20
+ module Project
21
+ class Ruby < Base
22
+
23
+ def install_command(runtime=nil, test_path=guest_test_root)
24
+ cmd = "cd #{test_path}"
25
+ cmd << " && rvm use #{runtime}" if runtime
26
+ cmd << " && gem install bundler"
27
+ cmd << " && #{install}"
28
+ end
29
+
30
+ def test_command(runtime=nil, test_path=guest_test_root)
31
+ cmd = "cd #{test_path}"
32
+ cmd << " && rvm use #{runtime}" if runtime
33
+ cmd << " && #{script}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,75 @@
1
+ #
2
+ # Author:: Andrew Crump (<andrew@kotirisoftware.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module TestKitchen
20
+ module Project
21
+ module SupportedPlatforms
22
+
23
+ def extract_supported_platforms(metadata)
24
+ raise ArgumentError, "Metadata must be provided" unless metadata
25
+ ast = parse_ruby(metadata)
26
+ supports = find_nodes(ast, [:command]).reject do |command|
27
+ find_nodes(command, [:@ident, 'supports']).empty?
28
+ end
29
+ string_literals(supports) + word_list(ast, supports)
30
+ end
31
+
32
+ private
33
+
34
+ def parse_ruby(ruby_str)
35
+ Ripper::SexpBuilder.new(ruby_str).parse
36
+ end
37
+
38
+ def word_list(ast, nodes)
39
+ nodes.map do |node|
40
+ var_name = find_nodes(find_nodes(node, [:var_ref]), [:@ident]).flatten
41
+ if var_name.length > 1
42
+ add_block = find_nodes(ast, [:method_add_block]).reject do |n|
43
+ find_nodes(n, [:@ident, "supports"]).empty?
44
+ end
45
+ unless find_nodes(find_nodes(add_block, [:do_block]),
46
+ [:@ident, var_name[1]]).flatten.empty?
47
+
48
+ find_nodes(find_nodes(add_block,
49
+ [:qwords_add]), [:@tstring_content]).uniq.map do |str|
50
+ str[1] if str.length > 1
51
+ end
52
+
53
+ end
54
+ end
55
+ end.flatten.compact
56
+ end
57
+
58
+ def string_literals(nodes)
59
+ nodes.map do |node|
60
+ tstring = find_nodes(node, [:@tstring_content]).find{|n| ! n.empty?}
61
+ tstring && tstring.length > 1 ? tstring[1] : nil
62
+ end.compact.flatten
63
+ end
64
+
65
+ def find_nodes(ast, node, result=[])
66
+ if ast.respond_to?(:each)
67
+ result << ast if ast.size > 1 and ast[0..(node.size - 1)] == node
68
+ ast.each { |child| find_nodes(child, node, result) }
69
+ end
70
+ result
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'test-kitchen/runner/base'
20
+ require 'test-kitchen/runner/vagrant'
@@ -0,0 +1,144 @@
1
+ #
2
+ # Author:: Seth Chisamore (<schisamo@opscode.com>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'librarian/chef/cli'
20
+
21
+ module TestKitchen
22
+ module Runner
23
+ class TestFailureError < StandardError; end
24
+ class Base
25
+
26
+ attr_accessor :platform
27
+ attr_accessor :configuration
28
+ attr_accessor :env
29
+
30
+ def initialize(env, options={})
31
+ raise ArgumentError, "Environment cannot be nil" if env.nil?
32
+ @env = env
33
+ @platform = options[:platform]
34
+ @configuration = options[:configuration]
35
+ end
36
+
37
+ def provision
38
+ assemble_cookbooks!
39
+ end
40
+
41
+ def run_list
42
+ ['test-kitchen::default']
43
+ end
44
+
45
+ def preflight_check
46
+ if env.project.preflight_command
47
+ system(env.project.preflight_command)
48
+ unless $?.success?
49
+ env.ui.info('Your cookbook had lint failures.', :red)
50
+ exit $?.exitstatus
51
+ end
52
+ end
53
+ end
54
+
55
+ def test
56
+ runtimes = configuration.runtimes
57
+ runtimes.each do |runtime|
58
+ message = "Synchronizing latest code from source root => test root."
59
+ execute_remote_command(platform, configuration.update_code_command, message)
60
+
61
+ message = "Updating dependencies for [#{configuration.name}]"
62
+ message << " under [#{runtime}]" if runtime
63
+ execute_remote_command(platform, configuration.install_command(runtime), message)
64
+
65
+ message = "Running tests for [#{configuration.name}]"
66
+ message << " under [#{runtime}]" if runtime
67
+ exit_code = execute_remote_command(platform, configuration.test_command(runtime), message)
68
+ raise TestFailureError unless exit_code == 0
69
+ end
70
+ end
71
+
72
+ def status
73
+ raise NotImplementedError
74
+ end
75
+
76
+ def destroy
77
+ raise NotImplementedError
78
+ end
79
+
80
+ def ssh
81
+ raise NotImplementedError
82
+ end
83
+
84
+ def execute_remote_command(platform, command, mesage=nil)
85
+ raise NotImplementedError
86
+ end
87
+
88
+ def self.inherited(subclass)
89
+ key = subclass.to_s.split('::').last.downcase
90
+ Runner.targets[key] = subclass
91
+ end
92
+
93
+ protected
94
+
95
+ def assemble_cookbooks!
96
+ # dump out a meta Cheffile
97
+ env.create_tmp_file('Cheffile',
98
+ IO.read(TestKitchen.source_root.join('config', 'Cheffile')))
99
+
100
+ env.ui.info("Assembling required cookbooks at [#{env.tmp_path.join('cookbooks')}].", :yellow)
101
+
102
+ # The following is a programatic version of `librarian-chef install`
103
+ Librarian::Action::Clean.new(librarian_env).run
104
+ Librarian::Action::Resolve.new(librarian_env).run
105
+ Librarian::Action::Install.new(librarian_env).run
106
+ end
107
+
108
+ def librarian_env
109
+ @librarian_env ||= Librarian::Chef::Environment.new(:project_path => env.tmp_path)
110
+ end
111
+
112
+ def test_recipe_name
113
+ ["#{env.project.name}_test", env.project.name].map do |cookbook_name|
114
+ if cookbook_exists?(cookbook_name)
115
+ cookbook_name + '::' + (if ! configuration || env.project.name == configuration.name
116
+ 'default'
117
+ else
118
+ configuration.name
119
+ end)
120
+ end
121
+ end.compact.first
122
+ end
123
+
124
+ def cookbook_exists?(name)
125
+ env.cookbook_paths.any?{|path| Dir.exists?(File.join(path, name)) }
126
+ end
127
+
128
+ end
129
+
130
+ def self.targets
131
+ @@targets ||= {}
132
+ end
133
+
134
+ def self.for_platform(env, options)
135
+ desired_platform = env.all_platforms[options[:platform]]
136
+ if desired_platform.box_url
137
+ TestKitchen::Runner.targets['vagrant'].new(env, options)
138
+ else
139
+ raise ArgumentError,
140
+ "No runner available for platform: #{desired_platform.name}"
141
+ end
142
+ end
143
+ end
144
+ end