foodtaster 0.0.1 → 0.0.3

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 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: