foodtaster 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +104 -0
- data/Rakefile +1 -0
- data/foodtaster.gemspec +21 -0
- data/lib/foodtaster/client.rb +30 -0
- data/lib/foodtaster/config.rb +31 -0
- data/lib/foodtaster/rspec/config.rb +12 -0
- data/lib/foodtaster/rspec/dsl_methods.rb +19 -0
- data/lib/foodtaster/rspec/example_methods.rb +40 -0
- data/lib/foodtaster/rspec/matchers/file_matcher.rb +72 -0
- data/lib/foodtaster/rspec/matchers/simple_matchers.rb +120 -0
- data/lib/foodtaster/rspec/matchers/user_matcher.rb +63 -0
- data/lib/foodtaster/rspec.rb +15 -0
- data/lib/foodtaster/rspec_run.rb +97 -0
- data/lib/foodtaster/version.rb +3 -0
- data/lib/foodtaster/vm.rb +58 -0
- data/lib/foodtaster.rb +23 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Mike Lapshin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Foodtaster
|
2
|
+
|
3
|
+
Foodtaster is a library for testing your Chef code with RSpec. Specs
|
4
|
+
are actually executed on VirtualBox machine(s) managed by
|
5
|
+
[Vagrant](http://www.vagrantup.com/).
|
6
|
+
|
7
|
+
Foodtaster uses VM snapshots to bring something like DB transactions
|
8
|
+
into your cookbook specs. Before each Chef Run VM is rolled-back into
|
9
|
+
initial 'clean' state which removes any modifications made by
|
10
|
+
previously executed specs. It allows you to independently test different
|
11
|
+
cookbooks on a single VM.
|
12
|
+
|
13
|
+
Of course, you aren't limited by just one VM for your specs, you may
|
14
|
+
run as many as you need. PostgreSQL replication, load balancing and
|
15
|
+
even entire application environments becomes testable (of course, if
|
16
|
+
you have enought amount of RAM).
|
17
|
+
|
18
|
+
Foodtaster is on early development stage, so feedback is very
|
19
|
+
appreciated.
|
20
|
+
|
21
|
+
## Quick Example
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'spec_helper'
|
25
|
+
|
26
|
+
describe "nginx::default" do
|
27
|
+
run_chef_on :vm0 do |c|
|
28
|
+
c.json = {}
|
29
|
+
c.add_recipe 'nginx'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should install nginx as a daemon" do
|
33
|
+
vm0.should have_package 'nginx'
|
34
|
+
vm0.should have_user('www-data').in_group('www-data')
|
35
|
+
vm0.should listen_port(80)
|
36
|
+
vm0.should open_page("http://localhost/")
|
37
|
+
|
38
|
+
vm0.should have_file("/etc/init.d/nginx")
|
39
|
+
vm0.should have_file("/etc/nginx/nginx.conf").with_content(/gzip on/)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have valid nginx config" do
|
43
|
+
result = vm0.execute("nginx -t")
|
44
|
+
|
45
|
+
result.should be_successfull
|
46
|
+
result.stdout.should include("/etc/nginx/nginx.conf syntax is ok")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
First, install Vagrant for your system following [official
|
54
|
+
instructions](http://docs.vagrantup.com/v2/installation/index.html).
|
55
|
+
Then, install two plugins: `sahara` and `vagrant-foodtaster-server`:
|
56
|
+
|
57
|
+
vagrant plugin install sahara
|
58
|
+
vagrant plugin install vagrant-foodtaster-server
|
59
|
+
|
60
|
+
That's all, you are ready to go.
|
61
|
+
|
62
|
+
## Usage
|
63
|
+
|
64
|
+
In your Chef repository, create a basic Gemfile:
|
65
|
+
|
66
|
+
source 'https://rubygems.org/'
|
67
|
+
|
68
|
+
gem 'rspec'
|
69
|
+
gem 'foodtaster'
|
70
|
+
|
71
|
+
Then, create a Vagrantfile describing VMs you need for specs. Here is
|
72
|
+
[example
|
73
|
+
Vagrantfile](http://raw.github.com/mlapshin/foodtaster-example/master/Vagrantfile).
|
74
|
+
|
75
|
+
Create `spec` folder with `spec_helper.rb` file:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
require 'foodtaster'
|
79
|
+
|
80
|
+
RSpec.configure do |config|
|
81
|
+
config.color_enabled = true
|
82
|
+
end
|
83
|
+
|
84
|
+
Foodtaster.configure do |config|
|
85
|
+
config.log_level = :info
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
You are ready to write cookbook specs. Run them as usual with command:
|
90
|
+
|
91
|
+
bundle exec rspec spec
|
92
|
+
|
93
|
+
## Contributing
|
94
|
+
|
95
|
+
1. Fork it
|
96
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
97
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
98
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
99
|
+
5. Create new Pull Request
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
Foodtaster is distributed under [MIT
|
104
|
+
License](http://raw.github.com/mlapshin/foodtaster/master/LICENSE).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/foodtaster.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'foodtaster/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "foodtaster"
|
8
|
+
gem.version = Foodtaster::VERSION
|
9
|
+
gem.authors = ["Mike Lapshin"]
|
10
|
+
gem.email = ["mikhail.a.lapshin@gmail.com"]
|
11
|
+
gem.description = %q{RSpec for Chef cookbooks run on Vagrant}
|
12
|
+
gem.summary = %q{Foodtaster is a library for testing your Chef code with RSpec.}
|
13
|
+
gem.homepage = "http://github.com/mlapshin/foodtaster"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency('rspec', '>= 2.10.0')
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module Foodtaster
|
4
|
+
class Client
|
5
|
+
def initialize(drb_port)
|
6
|
+
# start local service to be able to redirect stdout & stderr
|
7
|
+
# to client
|
8
|
+
DRb.start_service("druby://localhost:0")
|
9
|
+
@v = DRbObject.new_with_uri("druby://localhost:#{drb_port}")
|
10
|
+
|
11
|
+
init
|
12
|
+
end
|
13
|
+
|
14
|
+
[:vm_defined?, :prepare_vm, :rollback_vm,
|
15
|
+
:run_chef_on_vm, :execute_command_on_vm].each do |method_name|
|
16
|
+
define_method method_name do |*args|
|
17
|
+
@v.send(method_name, *args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def init
|
24
|
+
$stdout.extend DRbUndumped
|
25
|
+
$stderr.extend DRbUndumped
|
26
|
+
|
27
|
+
@v.redirect_stdstreams($stdout, $stderr)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
class Config
|
3
|
+
%w(log_level drb_port vagrant_binary).each do |attr|
|
4
|
+
attr_accessor attr.to_sym
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@log_level = :info
|
9
|
+
@drb_port = 35672
|
10
|
+
@vagrant_binary = 'vagrant'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.default
|
14
|
+
self.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def config
|
20
|
+
@config ||= Config.default
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
if block_given?
|
25
|
+
yield(self.config)
|
26
|
+
else
|
27
|
+
raise ArgumentError, "No block given"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
RSpec::configure do |config|
|
2
|
+
config.include Foodtaster::RSpec::ExampleMethods
|
3
|
+
config.extend Foodtaster::RSpec::DslMethods
|
4
|
+
|
5
|
+
config.before(:suite) do
|
6
|
+
Foodtaster::RSpecRun.current.start
|
7
|
+
end
|
8
|
+
|
9
|
+
config.after(:suite) do
|
10
|
+
Foodtaster::RSpecRun.current.stop
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
module RSpec
|
3
|
+
module DslMethods
|
4
|
+
def run_chef_on(vm_name, &block)
|
5
|
+
Foodtaster::RSpecRun.current.require_vm(vm_name)
|
6
|
+
|
7
|
+
skip_rollback = true
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
vm = get_vm(vm_name)
|
11
|
+
vm.rollback unless skip_rollback
|
12
|
+
run_chef_on(vm_name, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(vm_name) { get_vm(vm_name) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
module RSpec
|
3
|
+
module ExampleMethods
|
4
|
+
def get_vm(vm_name)
|
5
|
+
Foodtaster::RSpecRun.current.get_vm(vm_name)
|
6
|
+
end
|
7
|
+
|
8
|
+
def run_chef_on(vm_name, &block)
|
9
|
+
chef_config = ChefConfig.new.tap{ |conf| block.call(conf) }.to_hash
|
10
|
+
vm = get_vm(vm_name)
|
11
|
+
vm.run_chef(chef_config)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
class ChefConfig
|
17
|
+
attr_accessor :json, :run_list
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@json = {}
|
21
|
+
@run_list = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_recipe(name)
|
25
|
+
name = "recipe[#{name}]" unless name =~ /^recipe\[(.+?)\]$/
|
26
|
+
run_list << name
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_role(name)
|
30
|
+
name = "role[#{name}]" unless name =~ /^role\[(.+?)\]$/
|
31
|
+
run_list << name
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
{ json: json, run_list: run_list }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
module RSpec
|
3
|
+
module Matchers
|
4
|
+
class FileMatcher
|
5
|
+
def initialize(path)
|
6
|
+
@path = path
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(vm)
|
10
|
+
@vm = vm
|
11
|
+
@results = {}
|
12
|
+
return false unless vm.execute("sudo test -e #{@path}").successful?
|
13
|
+
|
14
|
+
|
15
|
+
if @content
|
16
|
+
@actual_content = vm.execute("sudo cat #{@path}").stdout
|
17
|
+
|
18
|
+
if @content.is_a?(Regexp)
|
19
|
+
@results[:content] = !!@actual_content.match(@content)
|
20
|
+
else
|
21
|
+
@results[:content] = (@actual_content.to_s == @content.to_s)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if @owner
|
26
|
+
@actual_owner = vm.execute("sudo stat #{@path} -c \"%U\"").stdout.chomp
|
27
|
+
|
28
|
+
@results[:owner] = (@actual_owner.to_s == @owner.to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
@results.values.all?
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_content(content)
|
35
|
+
@content = content
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def with_owner(owner)
|
41
|
+
@owner = owner
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def failure_message_for_should
|
47
|
+
["expected that #{@vm.name} should have file '#{@path}'",
|
48
|
+
@content && !@results[:content] && "with content #{@content.inspect}, but actual content is:\n#{@actual_content.inspect}\n",
|
49
|
+
@owner && !@results[:owner] && "with owner #{@owner}, but actual owner is #{@actual_owner}"].delete_if { |a| !a }.join(" ")
|
50
|
+
end
|
51
|
+
|
52
|
+
def failure_message_for_should_not
|
53
|
+
"expected that #{@vm.name} should not have file '#{@path}'"
|
54
|
+
end
|
55
|
+
|
56
|
+
def description
|
57
|
+
["have file '#{@path}'",
|
58
|
+
@content && "with content #{@content.inspect}",
|
59
|
+
@owner && "with owner #{@owner}"].delete_if { |a| !a }.join(" ")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module MatcherMethods
|
64
|
+
def have_file(path)
|
65
|
+
FileMatcher.new(path)
|
66
|
+
end
|
67
|
+
|
68
|
+
alias_method :have_directory, :have_file
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
# RSpec::Matchers.send(:include, VagrantHelper::Matchers::MatcherMethods)
|
4
|
+
|
5
|
+
RSpec::Matchers.define :have_running_process do |process|
|
6
|
+
match do |vm|
|
7
|
+
vm.execute("pgrep #{process}").successful?
|
8
|
+
end
|
9
|
+
|
10
|
+
failure_message_for_should do |vm|
|
11
|
+
"expected that #{vm.name} should have running process '#{process}'"
|
12
|
+
end
|
13
|
+
|
14
|
+
failure_message_for_should_not do |vm|
|
15
|
+
"expected that #{vm.name} should not have running process '#{process}'"
|
16
|
+
end
|
17
|
+
|
18
|
+
description do
|
19
|
+
"have running process '#{process}'"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec::Matchers.define :have_package do |package|
|
24
|
+
match do |vm|
|
25
|
+
vm.execute("dpkg --status #{package}").successful?
|
26
|
+
end
|
27
|
+
|
28
|
+
failure_message_for_should do |vm|
|
29
|
+
"expected that #{vm.name} should have installed package '#{package}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
failure_message_for_should_not do |vm|
|
33
|
+
"expected that #{vm.name} should not have installed package '#{package}'"
|
34
|
+
end
|
35
|
+
|
36
|
+
description do
|
37
|
+
"have installed package '#{package}'"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO: I'm not sure if lsof is installed by default
|
42
|
+
RSpec::Matchers.define :listen_port do |port|
|
43
|
+
match do |vm|
|
44
|
+
->{ vm.execute("sudo lsof -i :#{port.to_s} > /dev/null") }.should be_successful
|
45
|
+
end
|
46
|
+
|
47
|
+
failure_message_for_should do |vm|
|
48
|
+
"expected that #{vm.name} should listen port '#{port}'"
|
49
|
+
end
|
50
|
+
|
51
|
+
failure_message_for_should_not do |vm|
|
52
|
+
"expected that #{vm.name} should not listen port '#{port}'"
|
53
|
+
end
|
54
|
+
|
55
|
+
description do
|
56
|
+
"listen port '#{port}'"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
RSpec::Matchers.define :have_group do |group|
|
61
|
+
match do |vm|
|
62
|
+
vm.execute("cat /etc/group | cut -d: -f1 | grep \"\\<#{group}\\>\"").successful?
|
63
|
+
end
|
64
|
+
|
65
|
+
failure_message_for_should do |vm|
|
66
|
+
"expected that #{vm.name} should have group '#{group}'"
|
67
|
+
end
|
68
|
+
|
69
|
+
failure_message_for_should_not do |vm|
|
70
|
+
"expected that #{vm.name} should not have group '#{group}'"
|
71
|
+
end
|
72
|
+
|
73
|
+
description do
|
74
|
+
"have group '#{group}'"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
RSpec::Matchers.define :open_page do |address|
|
79
|
+
match do |vm|
|
80
|
+
result = vm.execute("wget #{address} -O /tmp/test-page").successful?
|
81
|
+
vm.execute("rm /tmp/test-page")
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
failure_message_for_should do |vm|
|
86
|
+
"expected that #{vm.name} should open page '#{address}'"
|
87
|
+
end
|
88
|
+
|
89
|
+
failure_message_for_should_not do |vm|
|
90
|
+
"expected that #{vm.name} should not open page '#{address}'"
|
91
|
+
end
|
92
|
+
|
93
|
+
description do
|
94
|
+
"open page '#{address}'"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def wait_until(_timeout = 5)
|
99
|
+
begin
|
100
|
+
timeout _timeout do
|
101
|
+
until (result = yield)
|
102
|
+
sleep 0.5
|
103
|
+
end
|
104
|
+
result
|
105
|
+
end
|
106
|
+
rescue Timeout::Error
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
RSpec::Matchers.define :be_successful do |opts = {}|
|
112
|
+
match do |command|
|
113
|
+
if command.respond_to?(:call)
|
114
|
+
wait_until(opts[:timeout] || 5) { command.call.successful? }
|
115
|
+
else
|
116
|
+
command.successful?
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
module RSpec
|
3
|
+
module Matchers
|
4
|
+
class UserMatcher
|
5
|
+
def initialize(username)
|
6
|
+
@username = username
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(vm)
|
10
|
+
@vm = vm
|
11
|
+
@results = {}
|
12
|
+
|
13
|
+
unless vm.execute("cat /etc/passwd | cut -d: -f1 | grep \"\\<#{@username}\\>\"").successful?
|
14
|
+
@results[:user] = false
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
if @group
|
19
|
+
@actual_groups = vm.execute("groups #{@username}").stdout.to_s.chomp.split(" ")[2..-1] || []
|
20
|
+
@results[:group] = !!@actual_groups.include?(@group)
|
21
|
+
end
|
22
|
+
|
23
|
+
@results.values.all?
|
24
|
+
end
|
25
|
+
|
26
|
+
def in_group(group)
|
27
|
+
@group = group
|
28
|
+
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def failure_message_for_should
|
33
|
+
msg = ["expected that #{@vm.name} should have user '#{@username}'"]
|
34
|
+
|
35
|
+
if @group
|
36
|
+
msg << "in group #{@group.inspect}"
|
37
|
+
|
38
|
+
if @results.key?(:group) && !@results[:group]
|
39
|
+
msg << " but actual user groups are:\n#{@actual_groups.join(", ")}\n"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
msg.join(" ")
|
44
|
+
end
|
45
|
+
|
46
|
+
def failure_message_for_should_not
|
47
|
+
"expected that #{@vm.name} should not have user '#{@username}'"
|
48
|
+
end
|
49
|
+
|
50
|
+
def description
|
51
|
+
["have user '#{@username}'",
|
52
|
+
@group && "in group #{@group}"].delete_if { |a| !a }.join(" ")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module MatcherMethods
|
57
|
+
def have_user(username)
|
58
|
+
UserMatcher.new(username)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
module RSpec
|
3
|
+
autoload :ExampleMethods, "foodtaster/rspec/example_methods"
|
4
|
+
autoload :DslMethods, "foodtaster/rspec/dsl_methods"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'foodtaster/rspec/config'
|
9
|
+
|
10
|
+
# require all matchers
|
11
|
+
Dir[File.dirname(__FILE__) + "/rspec/matchers/*.rb"].each do |f|
|
12
|
+
require f
|
13
|
+
end
|
14
|
+
|
15
|
+
RSpec::Matchers.send(:include, Foodtaster::RSpec::Matchers::MatcherMethods)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Foodtaster
|
4
|
+
class RSpecRun
|
5
|
+
def initialize
|
6
|
+
@required_vm_names = Set.new
|
7
|
+
@client = nil
|
8
|
+
@server_pid = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def require_vm(vm_name)
|
12
|
+
@required_vm_names.add(vm_name.to_sym)
|
13
|
+
end
|
14
|
+
|
15
|
+
def required_vm_names
|
16
|
+
@required_vm_names
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_vm(vm_name)
|
20
|
+
Foodtaster::Vm.new(vm_name, @client)
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
at_exit { self.stop }
|
25
|
+
|
26
|
+
Foodtaster.logger.debug "Starting Foodtaster specs run"
|
27
|
+
start_server_and_connect_client
|
28
|
+
prepare_required_vms
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop
|
32
|
+
puts "" # newline after rspec output
|
33
|
+
terminate_server
|
34
|
+
end
|
35
|
+
|
36
|
+
def client
|
37
|
+
@client
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
@instance = nil
|
42
|
+
|
43
|
+
def current
|
44
|
+
@instance ||= self.new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def prepare_required_vms
|
51
|
+
self.required_vm_names.each { |vm_name| get_vm(vm_name).prepare }
|
52
|
+
end
|
53
|
+
|
54
|
+
def start_server_and_connect_client(drb_port = Foodtaster.config.drb_port)
|
55
|
+
vagrant_binary = Foodtaster.config.vagrant_binary
|
56
|
+
vagrant_server_cmd = "#{vagrant_binary} foodtaster-server #{drb_port.to_s} &> /tmp/vagrant-foodtaster-server-output.txt"
|
57
|
+
|
58
|
+
@server_pid = Process.spawn(vagrant_server_cmd, pgroup: true)
|
59
|
+
Foodtaster.logger.debug "Started foodtaster-server on port #{drb_port} with PID #{@server_pid}"
|
60
|
+
|
61
|
+
connect_client(drb_port)
|
62
|
+
end
|
63
|
+
|
64
|
+
def connect_client(drb_port)
|
65
|
+
retry_count = 0
|
66
|
+
begin
|
67
|
+
sleep 0.2
|
68
|
+
@client = Foodtaster::Client.new(drb_port)
|
69
|
+
rescue DRb::DRbConnError => e
|
70
|
+
Foodtaster.logger.debug "DRb connection failed: #{e.message}"
|
71
|
+
retry_count += 1
|
72
|
+
retry if retry_count < 10
|
73
|
+
end
|
74
|
+
|
75
|
+
if @client.nil?
|
76
|
+
server_output = File.read("/tmp/vagrant-foodtaster-server-output.txt")
|
77
|
+
|
78
|
+
Foodtaster.logger.fatal "Cannot start or connect to Foodtaster DRb server."
|
79
|
+
Foodtaster.logger.fatal "Server output:\n#{server_output}\n"
|
80
|
+
|
81
|
+
exit 1
|
82
|
+
else
|
83
|
+
Foodtaster.logger.debug "DRb connection established"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def terminate_server
|
88
|
+
pgid = Process.getpgid(@server_pid) rescue 0
|
89
|
+
|
90
|
+
if pgid > 0
|
91
|
+
Process.kill("INT", -pgid)
|
92
|
+
Process.wait(-pgid)
|
93
|
+
Foodtaster.logger.debug "Terminated foodtaster-server process"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Foodtaster
|
2
|
+
class Vm
|
3
|
+
class ExecResult
|
4
|
+
attr_reader :stderr
|
5
|
+
attr_reader :stdout
|
6
|
+
attr_reader :exit_status
|
7
|
+
|
8
|
+
def initialize(hash)
|
9
|
+
@stderr = hash[:stderr]
|
10
|
+
@stdout = hash[:stdout]
|
11
|
+
@exit_status = hash[:exit_status]
|
12
|
+
end
|
13
|
+
|
14
|
+
def successful?
|
15
|
+
exit_status == 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
def initialize(name, client)
|
22
|
+
@name = name
|
23
|
+
@client = client
|
24
|
+
|
25
|
+
unless @client.vm_defined?(name)
|
26
|
+
raise ArgumentError, "No machine defined with name #{name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def prepare
|
31
|
+
Foodtaster.logger.info "#{name}: Preparing VM"
|
32
|
+
@client.prepare_vm(name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def rollback
|
36
|
+
Foodtaster.logger.info "#{name}: Rollbacking VM"
|
37
|
+
@client.rollback_vm(name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute(command)
|
41
|
+
Foodtaster.logger.debug "#{name}: Executing #{command}"
|
42
|
+
exec_result_hash = @client.execute_command_on_vm(name, command)
|
43
|
+
|
44
|
+
Foodtaster.logger.debug "#{name}: Finished with #{exec_result_hash[:exit_status]}"
|
45
|
+
Foodtaster.logger.debug "#{name}: STDOUT: #{exec_result_hash[:stdout].chomp}"
|
46
|
+
Foodtaster.logger.debug "#{name}: STDERR: #{exec_result_hash[:stderr].chomp}"
|
47
|
+
|
48
|
+
ExecResult.new(exec_result_hash)
|
49
|
+
end
|
50
|
+
|
51
|
+
def run_chef(config)
|
52
|
+
Foodtaster.logger.info "#{name}: Running Chef with Run List #{config[:run_list].join(', ')}"
|
53
|
+
Foodtaster.logger.debug "#{name}: with JSON: #{config[:json].inspect}"
|
54
|
+
@client.run_chef_on_vm(name, config)
|
55
|
+
Foodtaster.logger.debug "#{name}: Chef Run finished"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/foodtaster.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'foodtaster/config'
|
2
|
+
require 'foodtaster/rspec'
|
3
|
+
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Foodtaster
|
7
|
+
autoload :Client, 'foodtaster/client'
|
8
|
+
autoload :Vm, 'foodtaster/vm'
|
9
|
+
autoload :RSpecRun, 'foodtaster/rspec_run'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def logger
|
13
|
+
@logger ||= Logger.new(STDOUT).tap do |log|
|
14
|
+
log_level = ENV['FT_LOGLEVEL'] || self.config.log_level.to_s.upcase
|
15
|
+
log.level = Logger.const_get(log_level)
|
16
|
+
|
17
|
+
log.formatter = proc do |severity, datetime, progname, msg|
|
18
|
+
"[FT #{severity}]: #{msg}\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: foodtaster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Lapshin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.10.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.10.0
|
30
|
+
description: RSpec for Chef cookbooks run on Vagrant
|
31
|
+
email:
|
32
|
+
- mikhail.a.lapshin@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- LICENSE
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- foodtaster.gemspec
|
43
|
+
- lib/foodtaster.rb
|
44
|
+
- lib/foodtaster/client.rb
|
45
|
+
- lib/foodtaster/config.rb
|
46
|
+
- lib/foodtaster/rspec.rb
|
47
|
+
- lib/foodtaster/rspec/config.rb
|
48
|
+
- lib/foodtaster/rspec/dsl_methods.rb
|
49
|
+
- lib/foodtaster/rspec/example_methods.rb
|
50
|
+
- lib/foodtaster/rspec/matchers/file_matcher.rb
|
51
|
+
- lib/foodtaster/rspec/matchers/simple_matchers.rb
|
52
|
+
- lib/foodtaster/rspec/matchers/user_matcher.rb
|
53
|
+
- lib/foodtaster/rspec_run.rb
|
54
|
+
- lib/foodtaster/version.rb
|
55
|
+
- lib/foodtaster/vm.rb
|
56
|
+
homepage: http://github.com/mlapshin/foodtaster
|
57
|
+
licenses: []
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.8.24
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: Foodtaster is a library for testing your Chef code with RSpec.
|
80
|
+
test_files: []
|