foodtaster 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,5 +1,7 @@
1
1
  *.gem
2
2
  *.rbc
3
+ *.swp
4
+ *.swo
3
5
  .bundle
4
6
  .config
5
7
  .yardoc
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Foodtaster
1
+ # Foodtæster
2
2
 
3
3
  Foodtaster is a library for testing your Chef code with RSpec. Specs
4
4
  are actually executed on VirtualBox machine(s) managed by
@@ -36,13 +36,13 @@ describe "nginx::default" do
36
36
  vm0.should open_page("http://localhost/")
37
37
 
38
38
  vm0.should have_file("/etc/init.d/nginx")
39
- vm0.should have_file("/etc/nginx/nginx.conf").with_content(/gzip on/)
39
+ vm0.should have_file("/etc/nginx/nginx.conf").with_content(/gzip on/).with_mode(0644)
40
40
  end
41
41
 
42
42
  it "should have valid nginx config" do
43
43
  result = vm0.execute("nginx -t")
44
44
 
45
- result.should be_successfull
45
+ result.should be_successful
46
46
  result.stdout.should include("/etc/nginx/nginx.conf syntax is ok")
47
47
  end
48
48
  end
@@ -52,7 +52,9 @@ end
52
52
 
53
53
  First, install Vagrant for your system following [official
54
54
  instructions](http://docs.vagrantup.com/v2/installation/index.html).
55
- Then, install two plugins: `sahara` and `vagrant-foodtaster-server`:
55
+ Then, install two Vagrant plugins:
56
+ [sahara](http://github.com/jedi4ever/sahara) and
57
+ [vagrant-foodtaster-server](http://github.com/mlapshin/vagrant-foodtaster-server):
56
58
 
57
59
  vagrant plugin install sahara
58
60
  vagrant plugin install vagrant-foodtaster-server
@@ -61,6 +63,10 @@ That's all, you are ready to go.
61
63
 
62
64
  ## Usage
63
65
 
66
+ Check out [foodtaster-example
67
+ repository](http://github.com/mlapshin/foodtaster-example) or
68
+ follow this instructions.
69
+
64
70
  In your Chef repository, create a basic Gemfile:
65
71
 
66
72
  source 'https://rubygems.org/'
@@ -101,4 +107,4 @@ You are ready to write cookbook specs. Run them as usual with command:
101
107
  ## License
102
108
 
103
109
  Foodtaster is distributed under [MIT
104
- License](http://raw.github.com/mlapshin/foodtaster/master/LICENSE).
110
+ License](http://raw.github.com/mlapshin/foodtaster/master/LICENSE).
data/foodtaster.gemspec CHANGED
@@ -11,6 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.description = %q{RSpec for Chef cookbooks run on Vagrant}
12
12
  gem.summary = %q{Foodtaster is a library for testing your Chef code with RSpec.}
13
13
  gem.homepage = "http://github.com/mlapshin/foodtaster"
14
+ gem.license = 'MIT'
14
15
 
15
16
  gem.files = `git ls-files`.split($/)
16
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -2,29 +2,69 @@ require 'drb'
2
2
 
3
3
  module Foodtaster
4
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}")
5
+ MAX_ATTEMPTS = 20
10
6
 
11
- init
12
- end
7
+ def self.connect(drb_port, server_process = nil)
8
+ attempt_index = 1
9
+ begin
10
+ sleep 0.2
11
+ client = Foodtaster::Client.new(drb_port)
12
+ rescue DRb::DRbConnError => e
13
+ Foodtaster.logger.debug "DRb connection failed (attempt #{attempt_index}/#{MAX_ATTEMPTS}): #{e.message}"
14
+ attempt_index += 1
15
+ retry if attempt_index <= MAX_ATTEMPTS && (server_process.nil? || server_process.alive?)
16
+ end
13
17
 
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
+ if client
19
+ Foodtaster.logger.debug "DRb connection established"
20
+ else
21
+ Foodtaster.logger.debug "Can't connect to Foodtaster DRb Server"
18
22
  end
23
+
24
+ client
19
25
  end
20
26
 
21
- private
27
+ [:vm_defined?, :prepare_vm, :rollback_vm,
28
+ :run_chef_on_vm, :execute_command_on_vm,
29
+ :shutdown_vm].each do |method_name|
30
+ define_method method_name do |*args|
31
+ begin
32
+ @v.send(method_name, *args)
33
+ rescue DRb::DRbUnknownError => e
34
+ message = "Folowing exception was raised on server:\n#{e.unknown.buf}"
35
+ Foodtaster.logger.fatal(message)
36
+ raise e
37
+ end
38
+ end
39
+ end
22
40
 
23
- def init
24
- $stdout.extend DRbUndumped
25
- $stderr.extend DRbUndumped
41
+ private
26
42
 
27
- @v.redirect_stdstreams($stdout, $stderr)
28
- end
43
+ def initialize(drb_port)
44
+ # start local service to be able to redirect stdout & stderr
45
+ # to client
46
+ DRb.start_service("druby://localhost:0")
47
+ @v = DRbObject.new_with_uri("druby://localhost:#{drb_port}")
48
+
49
+ init
50
+ end
51
+
52
+ private
53
+
54
+ def init
55
+ $stdout.extend DRbUndumped
56
+ $stderr.extend DRbUndumped
57
+
58
+ @v.redirect_stdstreams($stdout, $stderr)
59
+ check_version
60
+ end
61
+
62
+ def check_version
63
+ server_version = @v.version
64
+
65
+ if server_version != Foodtaster::VERSION
66
+ Foodtaster.logger.warn "Warning: Foodtaster DRb Server version doesn't match Foodtaster Gem version.\n\nDRb Server version: #{server_version}\nFoodtaster Gem version: #{Foodtaster::VERSION}\n"
67
+ end
68
+ end
29
69
  end
30
70
  end
@@ -1,6 +1,7 @@
1
1
  module Foodtaster
2
2
  class Config
3
- %w(log_level drb_port vagrant_binary).each do |attr|
3
+ %w(log_level drb_port vagrant_binary shutdown_vms
4
+ skip_rollback start_server).each do |attr|
4
5
  attr_accessor attr.to_sym
5
6
  end
6
7
 
@@ -8,6 +9,9 @@ module Foodtaster
8
9
  @log_level = :info
9
10
  @drb_port = 35672
10
11
  @vagrant_binary = 'vagrant'
12
+ @shutdown_vms = false
13
+ @skip_rollback = false
14
+ @start_server = true
11
15
  end
12
16
 
13
17
  def self.default
@@ -1,18 +1,22 @@
1
1
  module Foodtaster
2
2
  module RSpec
3
3
  module DslMethods
4
- def run_chef_on(vm_name, &block)
4
+ def require_vm(vm_name)
5
5
  Foodtaster::RSpecRun.current.require_vm(vm_name)
6
6
 
7
- skip_rollback = true
7
+ let(vm_name) { get_vm(vm_name) }
8
+ end
9
+
10
+ def run_chef_on(vm_name, options = {}, &block)
11
+ require_vm(vm_name)
12
+ skip_rollback = Foodtaster.config.skip_rollback || options[:skip_rollback]
8
13
 
9
14
  before(:all) do
10
15
  vm = get_vm(vm_name)
11
16
  vm.rollback unless skip_rollback
17
+
12
18
  run_chef_on(vm_name, &block)
13
19
  end
14
-
15
- let(vm_name) { get_vm(vm_name) }
16
20
  end
17
21
  end
18
22
  end
@@ -7,10 +7,20 @@ module Foodtaster
7
7
 
8
8
  def run_chef_on(vm_name, &block)
9
9
  chef_config = ChefConfig.new.tap{ |conf| block.call(conf) }.to_hash
10
+ @previous_chef_config = chef_config
11
+
10
12
  vm = get_vm(vm_name)
11
13
  vm.run_chef(chef_config)
12
14
  end
13
15
 
16
+ def rerun_chef_on(vm_name)
17
+ raise RuntimeError, "No previous Chef run was made" unless @previous_chef_config
18
+ vm = get_vm(vm_name)
19
+ vm.run_chef(@previous_chef_config)
20
+ end
21
+
22
+ alias :repeat_chef_run :rerun_chef_on
23
+
14
24
  private
15
25
 
16
26
  class ChefConfig
@@ -28,6 +28,12 @@ module Foodtaster
28
28
  @results[:owner] = (@actual_owner.to_s == @owner.to_s)
29
29
  end
30
30
 
31
+ if @mode
32
+ @actual_mode = vm.execute("sudo stat #{@path} -c \"%a\"").stdout.chomp
33
+
34
+ @results[:mode] = (@actual_mode == @mode.to_s(8))
35
+ end
36
+
31
37
  @results.values.all?
32
38
  end
33
39
 
@@ -43,10 +49,17 @@ module Foodtaster
43
49
  self
44
50
  end
45
51
 
52
+ def with_mode(mode)
53
+ @mode = mode
54
+
55
+ self
56
+ end
57
+
46
58
  def failure_message_for_should
47
59
  ["expected that #{@vm.name} should have file '#{@path}'",
48
60
  @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(" ")
61
+ @owner && !@results[:owner] && "with owner #{@owner}, but actual owner is #{@actual_owner}",
62
+ @mode && !@results[:mode] && "with mode #{@mode.to_s(8)}(octal), but actual mode is #{@actual_mode}(octal)"].delete_if { |a| !a }.join(" ")
50
63
  end
51
64
 
52
65
  def failure_message_for_should_not
@@ -56,7 +69,8 @@ module Foodtaster
56
69
  def description
57
70
  ["have file '#{@path}'",
58
71
  @content && "with content #{@content.inspect}",
59
- @owner && "with owner #{@owner}"].delete_if { |a| !a }.join(" ")
72
+ @owner && "with owner #{@owner}",
73
+ @mode && "with mode #{@mode}"].delete_if { |a| !a }.join(" ")
60
74
  end
61
75
  end
62
76
 
@@ -5,7 +5,8 @@ module Foodtaster
5
5
  def initialize
6
6
  @required_vm_names = Set.new
7
7
  @client = nil
8
- @server_pid = nil
8
+ @server_process = nil
9
+ @stopped = false
9
10
  end
10
11
 
11
12
  def require_vm(vm_name)
@@ -21,15 +22,28 @@ module Foodtaster
21
22
  end
22
23
 
23
24
  def start
24
- at_exit { self.stop }
25
-
26
- Foodtaster.logger.debug "Starting Foodtaster specs run"
25
+ setup_signal_handlers
27
26
  start_server_and_connect_client
28
- prepare_required_vms
27
+
28
+ if (@server_process.nil? || @server_process.alive?) && @client
29
+ prepare_required_vms
30
+ else
31
+ if @server_process
32
+ Foodtaster.logger.fatal "Failed to start Foodtaster DRb Server:\n\n#{@server_process.output}"
33
+ else
34
+ Foodtaster.logger.fatal "Failed to connect to Foodtaster DRb Server"
35
+ end
36
+
37
+ exit 1
38
+ end
29
39
  end
30
40
 
31
41
  def stop
42
+ return if @stopped
43
+
44
+ @stopped = true
32
45
  puts "" # newline after rspec output
46
+ shutdown_required_vms if Foodtaster.config.shutdown_vms
33
47
  terminate_server
34
48
  end
35
49
 
@@ -47,51 +61,45 @@ module Foodtaster
47
61
 
48
62
  private
49
63
 
64
+ def setup_signal_handlers
65
+ terminator = proc {
66
+ self.stop
67
+ exit 1
68
+ }
69
+
70
+ trap("INT", &terminator)
71
+ trap("TERM", &terminator)
72
+
73
+ at_exit do
74
+ self.stop
75
+ end
76
+ end
77
+
50
78
  def prepare_required_vms
51
79
  self.required_vm_names.each { |vm_name| get_vm(vm_name).prepare }
52
80
  end
53
81
 
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"
82
+ def shutdown_required_vms
83
+ self.required_vm_names.each { |vm_name| get_vm(vm_name).shutdown }
84
+ end
57
85
 
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}"
86
+ def start_server_and_connect_client
87
+ drb_port = Foodtaster.config.drb_port
60
88
 
89
+ start_server(drb_port) if Foodtaster.config.start_server
61
90
  connect_client(drb_port)
62
91
  end
63
92
 
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
93
+ def start_server(drb_port)
94
+ @server_process = Foodtaster::ServerProcess.new(drb_port)
85
95
  end
86
96
 
87
97
  def terminate_server
88
- pgid = Process.getpgid(@server_pid) rescue 0
98
+ @server_process && @server_process.terminate
99
+ end
89
100
 
90
- if pgid > 0
91
- Process.kill("INT", -pgid)
92
- Process.wait(-pgid)
93
- Foodtaster.logger.debug "Terminated foodtaster-server process"
94
- end
101
+ def connect_client(drb_port)
102
+ @client = Foodtaster::Client.connect(drb_port, @server_process)
95
103
  end
96
104
  end
97
105
  end
@@ -0,0 +1,39 @@
1
+ require 'open3'
2
+
3
+ module Foodtaster
4
+ class ServerProcess
5
+ def initialize(drb_port)
6
+ Foodtaster.logger.debug "Starting Foodtaster specs run"
7
+
8
+ vagrant_binary = Foodtaster.config.vagrant_binary
9
+
10
+ _, @pipe_out, thread = Open3.popen2("#{vagrant_binary} foodtaster-server #{drb_port}",
11
+ pgroup: true, err: [:child, :out])
12
+
13
+ @pid = thread.pid
14
+ @pgid = Process.getpgid(@pid)
15
+
16
+ Foodtaster.logger.debug "Started foodtaster-server on port #{drb_port} with PID #{@pid}"
17
+ end
18
+
19
+ def output
20
+ @pipe_out.read
21
+ end
22
+
23
+ def alive?
24
+ Process.kill(0, @pid) == 1 rescue false
25
+ end
26
+
27
+ def terminate
28
+ if alive?
29
+ @pipe_out.close
30
+
31
+ if @pgid > 0
32
+ Process.kill("TERM", -@pgid)
33
+ Process.waitpid(-@pgid) rescue nil
34
+ Foodtaster.logger.debug "Terminated Foodtaster DRb Server process"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module Foodtaster
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/foodtaster/vm.rb CHANGED
@@ -6,8 +6,8 @@ module Foodtaster
6
6
  attr_reader :exit_status
7
7
 
8
8
  def initialize(hash)
9
- @stderr = hash[:stderr]
10
- @stdout = hash[:stdout]
9
+ @stderr = hash[:stderr].to_s
10
+ @stdout = hash[:stdout].to_s
11
11
  @exit_status = hash[:exit_status]
12
12
  end
13
13
 
@@ -32,8 +32,13 @@ module Foodtaster
32
32
  @client.prepare_vm(name)
33
33
  end
34
34
 
35
+ def shutdown
36
+ Foodtaster.logger.debug "#{name}: Shutting down VM"
37
+ @client.shutdown_vm(name)
38
+ end
39
+
35
40
  def rollback
36
- Foodtaster.logger.info "#{name}: Rollbacking VM"
41
+ Foodtaster.logger.info "#{name}: Rolling back VM"
37
42
  @client.rollback_vm(name)
38
43
  end
39
44
 
@@ -42,12 +47,17 @@ module Foodtaster
42
47
  exec_result_hash = @client.execute_command_on_vm(name, command)
43
48
 
44
49
  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}"
50
+ Foodtaster.logger.debug "#{name}: STDOUT: #{exec_result_hash[:stdout].to_s.chomp}"
51
+ Foodtaster.logger.debug "#{name}: STDERR: #{exec_result_hash[:stderr].to_s.chomp}"
47
52
 
48
53
  ExecResult.new(exec_result_hash)
49
54
  end
50
55
 
56
+ def execute_as(user, command)
57
+ cmd = %Q[sudo su -l #{user} -c "#{command}"]
58
+ self.execute cmd
59
+ end
60
+
51
61
  def run_chef(config)
52
62
  Foodtaster.logger.info "#{name}: Running Chef with Run List #{config[:run_list].join(', ')}"
53
63
  Foodtaster.logger.debug "#{name}: with JSON: #{config[:json].inspect}"
data/lib/foodtaster.rb CHANGED
@@ -4,9 +4,10 @@ require 'foodtaster/rspec'
4
4
  require 'logger'
5
5
 
6
6
  module Foodtaster
7
- autoload :Client, 'foodtaster/client'
8
- autoload :Vm, 'foodtaster/vm'
9
- autoload :RSpecRun, 'foodtaster/rspec_run'
7
+ autoload :Client, 'foodtaster/client'
8
+ autoload :ServerProcess, 'foodtaster/server_process'
9
+ autoload :Vm, 'foodtaster/vm'
10
+ autoload :RSpecRun, 'foodtaster/rspec_run'
10
11
 
11
12
  class << self
12
13
  def logger
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foodtaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
4
  prerelease:
5
+ version: 0.0.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mike Lapshin
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-24 00:00:00.000000000 Z
12
+ date: 2013-11-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
+ prerelease: false
16
+ type: :runtime
15
17
  name: rspec
16
- requirement: !ruby/object:Gem::Requirement
18
+ version_requirements: !ruby/object:Gem::Requirement
17
19
  none: false
18
20
  requirements:
19
21
  - - ! '>='
20
22
  - !ruby/object:Gem::Version
21
23
  version: 2.10.0
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
24
+ requirement: !ruby/object:Gem::Requirement
25
25
  none: false
26
26
  requirements:
27
27
  - - ! '>='
@@ -51,10 +51,12 @@ files:
51
51
  - lib/foodtaster/rspec/matchers/simple_matchers.rb
52
52
  - lib/foodtaster/rspec/matchers/user_matcher.rb
53
53
  - lib/foodtaster/rspec_run.rb
54
+ - lib/foodtaster/server_process.rb
54
55
  - lib/foodtaster/version.rb
55
56
  - lib/foodtaster/vm.rb
56
57
  homepage: http://github.com/mlapshin/foodtaster
57
- licenses: []
58
+ licenses:
59
+ - MIT
58
60
  post_install_message:
59
61
  rdoc_options: []
60
62
  require_paths:
@@ -73,8 +75,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
75
  version: '0'
74
76
  requirements: []
75
77
  rubyforge_project:
76
- rubygems_version: 1.8.24
78
+ rubygems_version: 1.8.25
77
79
  signing_key:
78
80
  specification_version: 3
79
81
  summary: Foodtaster is a library for testing your Chef code with RSpec.
80
82
  test_files: []
83
+ has_rdoc: