foodtaster 0.0.1
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/.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: []
|