test-kitchen 0.5.0

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