minimal-buffet 0.6

Sign up to get free protection for your applications and to get access to all the features.
data/bin/buffet ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'buffet/cli'
4
+
5
+ Buffet::CLI.new ARGV
data/lib/buffet/cli.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'buffet'
2
+ require 'optparse'
3
+
4
+ module Buffet
5
+ class CLI
6
+ def initialize args
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: buffet [options] [spec-files]"
9
+
10
+ opts.on('-c', '--config CONFIG',
11
+ 'Use the specified CONFIG file') do |config_file|
12
+ Settings.load_file File.expand_path(config_file)
13
+ end
14
+
15
+ opts.on('-p', '--project PROJECT',
16
+ 'Use the specified PROJECT name') do |project_name|
17
+ Settings.project_name = project_name
18
+ end
19
+ end.parse!(args)
20
+
21
+ specs = Buffet.extract_specs_from(opts.empty? ? 'spec' : opts)
22
+
23
+ runner = Runner.new
24
+ runner.run specs
25
+
26
+ exit 1 if runner.failures?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ require 'logger'
2
+ require 'wopen3'
3
+
4
+ module Buffet
5
+ class CommandRunner
6
+ def initialize logger = Logger.new(STDOUT)
7
+ @logger = logger
8
+ end
9
+
10
+ def run *command
11
+ start_time = Time.now
12
+ result = Wopen3.system *command
13
+ end_time = Time.now
14
+ @logger.info "\n" +
15
+ "command: #{command.join ' '}\n" +
16
+ "time: #{end_time - start_time}\n" +
17
+ "status: #{result.status}\n" +
18
+ "stdout:\n#{result.stdout}\n" +
19
+ "stderr:\n#{result.stderr}"
20
+ result
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,129 @@
1
+ require 'benchmark'
2
+ require 'drb'
3
+ require 'thread'
4
+ require 'socket'
5
+
6
+ module Buffet
7
+ class Master
8
+ attr_reader :failures, :stats
9
+
10
+ def initialize project, slaves, specs, listener
11
+ @project = project
12
+ @slaves = slaves
13
+ @stats = {:examples => 0, :failures => 0, :pending => 0}
14
+ @slaves_stats = Hash[@slaves.map do |slave|
15
+ [slave.user_at_host, stats.dup.merge!(:slave => slave)]
16
+ end]
17
+ @stats[:slaves] = @slaves_stats
18
+ @lock = Mutex.new
19
+ @failures = []
20
+ @specs = specs.shuffle # Never have the same test distribution
21
+ @listener = listener
22
+ end
23
+
24
+ def run
25
+ start_service
26
+
27
+ @stats[:total_time] = Benchmark.measure do
28
+ threads = @slaves.map do |slave|
29
+ Thread.new do
30
+ time = Benchmark.measure do
31
+ prepare_slave slave
32
+ run_slave slave
33
+ end.real
34
+ @lock.synchronize { @slaves_stats[slave.name][:total_time] = time }
35
+ end
36
+ end
37
+
38
+ threads.each { |t| t.join }
39
+ end.real
40
+
41
+ stop_service
42
+ end
43
+
44
+ def next_file_for slave_name
45
+ file = @lock.synchronize { @specs.shift }
46
+ if file
47
+ slave = @slaves_stats[slave_name][:slave]
48
+ @listener.spec_taken slave, file if file
49
+ end
50
+ file
51
+ end
52
+
53
+ def example_passed slave_name, details
54
+ @lock.synchronize do
55
+ @stats[:examples] += 1
56
+ @slaves_stats[slave_name][:examples] += 1
57
+ end
58
+
59
+ @listener.example_passed
60
+ end
61
+
62
+ def example_failed slave_name, details
63
+ @lock.synchronize do
64
+ @stats[:examples] += 1
65
+ @stats[:failures] += 1
66
+ @slaves_stats[slave_name][:failures] += 1
67
+ @failures << details
68
+ end
69
+
70
+ @listener.example_failed
71
+ end
72
+
73
+ def example_pending slave_name, details
74
+ @lock.synchronize do
75
+ @stats[:examples] += 1
76
+ @stats[:pending] += 1
77
+ @slaves_stats[slave_name][:pending] += 1
78
+ end
79
+
80
+ @listener.example_pending
81
+ end
82
+
83
+ private
84
+
85
+ def server_uri
86
+ @drb_server.uri
87
+ end
88
+
89
+ def start_service
90
+ @drb_server = DRb.start_service("druby://#{ip}:0", self)
91
+ end
92
+
93
+ def stop_service
94
+ DRb.stop_service
95
+ end
96
+
97
+ def ip
98
+ result = Buffet.run! 'host `hostname -s`'
99
+ result.stdout.chomp.match(/((\d+\.){3}\d+)/)[1]
100
+ end
101
+
102
+ def prepare_slave slave
103
+ time = Benchmark.measure do
104
+ @project.sync_to slave
105
+
106
+ if Settings.has_prepare_script?
107
+ slave.execute_in_project "#{Settings.prepare_script} #{Buffet.user} #{@project.name}"
108
+ end
109
+
110
+ # Copy support files so they can be run on the remote machine
111
+ slave.scp File.dirname(__FILE__) + '/../../support',
112
+ @project.support_dir_on_slave, :recurse => true
113
+ end.real
114
+ @lock.synchronize { @slaves_stats[slave.name][:prepare_time] = time }
115
+
116
+ @listener.slave_prepared slave
117
+ end
118
+
119
+ def run_slave slave
120
+ time = Benchmark.measure do
121
+ slave.execute_in_project(
122
+ ".buffet/buffet-worker #{server_uri} #{slave.user_at_host} #{Settings.framework}")
123
+ end.real
124
+ @lock.synchronize { @slaves_stats[slave.name][:test_time] = time }
125
+
126
+ @listener.slave_finished slave
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,24 @@
1
+ module Buffet
2
+ class Project
3
+ attr_accessor :name
4
+ attr_reader :directory
5
+
6
+ def initialize directory
7
+ @name = File.basename directory
8
+ @directory = File.expand_path directory
9
+ end
10
+
11
+ def directory_on_slave
12
+ "#{Buffet.workspace_dir}/#{name}"
13
+ end
14
+
15
+ def support_dir_on_slave
16
+ "#{directory_on_slave}/.buffet"
17
+ end
18
+
19
+ def sync_to slave
20
+ slave.execute "mkdir -p #{directory_on_slave}"
21
+ slave.rsync directory + '/', directory_on_slave
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,93 @@
1
+ require 'buffet'
2
+ require 'colorize'
3
+
4
+ module Buffet
5
+ class Runner
6
+ def initialize
7
+ @project = Settings.project
8
+ end
9
+
10
+ def run specs = nil
11
+ @specs = specs
12
+ raise 'No specs found' if @specs.empty?
13
+
14
+ @slaves = Settings.slaves
15
+ raise 'No slaves defined in settings.yml' if @slaves.empty?
16
+
17
+ Buffet.logger.info "Starting Buffet test run"
18
+ puts "Running Buffet..."
19
+
20
+ run_tests
21
+ display_results
22
+ end
23
+
24
+ def slave_prepared slave
25
+ Buffet.logger.info "#{slave.name} prepared"
26
+ end
27
+
28
+ def spec_taken slave, spec_file
29
+ Buffet.logger.info "#{slave.name} took #{spec_file}"
30
+ end
31
+
32
+ def example_passed
33
+ print '.'.green
34
+ STDOUT.flush
35
+ end
36
+
37
+ def example_failed
38
+ print 'F'.red
39
+ STDOUT.flush
40
+ end
41
+
42
+ def example_pending
43
+ print '*'.yellow
44
+ STDOUT.flush
45
+ end
46
+
47
+ def slave_finished slave
48
+ Buffet.logger.info "#{slave.name} finished"
49
+ end
50
+
51
+ def failures?
52
+ @master.failures.any?
53
+ end
54
+
55
+ private
56
+
57
+ def run_tests
58
+ @master = Master.new @project, @slaves, @specs, self
59
+ @master.run
60
+ end
61
+
62
+ def display_results
63
+ results = []
64
+ results << "\n"
65
+
66
+ @master.stats[:slaves].each do |slave_name, slave_stats|
67
+ results << "#{slave_name}:"
68
+ slave_stats.each do |key, value|
69
+ results << "\t#{key}: #{value}" unless key == :slave
70
+ end
71
+ end
72
+
73
+ results << "Total Examples: #{@master.stats[:examples]}"
74
+ results << "Total Pending: #{@master.stats[:pending]}"
75
+ results << "Total Failures: #{@master.stats[:failures]}"
76
+ results << ''
77
+ results << "Buffet consumed in #{@master.stats[:total_time]} seconds"
78
+
79
+ unless @master.failures.empty?
80
+ results << ''
81
+ results << @master.failures.map do |failure|
82
+ "#{failure[:description]}\n".red +
83
+ "Slave: #{failure[:slave_name]}\n" +
84
+ "Location: #{failure[:location]}\n" +
85
+ "#{failure[:message]}\n" +
86
+ "#{failure[:backtrace]}\n"
87
+ end
88
+ end
89
+
90
+ puts results
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,58 @@
1
+ require 'yaml'
2
+
3
+ module Buffet
4
+ class Settings
5
+ DEFAULT_SETTINGS_FILE = 'buffet.yml'
6
+ DEFAULT_PREPARE_SCRIPT = 'bin/before-buffet-run'
7
+ DEFAULT_EXCLUDE_FILTER_FILE = '.buffet-exclude-filter'
8
+
9
+ class << self
10
+ def [](name)
11
+ @settings ||= load_file DEFAULT_SETTINGS_FILE
12
+ @settings[name]
13
+ end
14
+
15
+ def load_file file
16
+ @settings = YAML.load_file file
17
+ end
18
+
19
+ def slaves
20
+ @slaves ||= self['slaves'].map do |slave_hash|
21
+ Slave.new slave_hash['user'], slave_hash['host'], project
22
+ end
23
+ end
24
+
25
+ def project_name=(project_name)
26
+ project.name = project_name
27
+ end
28
+
29
+ def project
30
+ @project ||= Project.new Dir.pwd
31
+ end
32
+
33
+ def framework
34
+ self['framework'].upcase || 'RSPEC1'
35
+ end
36
+
37
+ def prepare_script
38
+ self['prepare_script'] || DEFAULT_PREPARE_SCRIPT
39
+ end
40
+
41
+ def has_prepare_script?
42
+ self['prepare_script'] || File.exist?(DEFAULT_PREPARE_SCRIPT)
43
+ end
44
+
45
+ def exclude_filter_file
46
+ self['exclude_filter_file'] || DEFAULT_EXCLUDE_FILTER_FILE
47
+ end
48
+
49
+ def has_exclude_filter_file?
50
+ self['exclude_filter_file'] || File.exist?(DEFAULT_EXCLUDE_FILTER_FILE)
51
+ end
52
+
53
+ def reset!
54
+ @settings = nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ module Buffet
2
+ class Slave
3
+ attr_reader :user, :host
4
+
5
+ def initialize user, host, project
6
+ @user = user
7
+ @host = host
8
+ @project = project
9
+ end
10
+
11
+ def rsync src, dest
12
+ Buffet.run! 'rsync', '-aqz', '--delete',
13
+ '--delete-excluded', rsync_exclude_flags,
14
+ '-e', 'ssh', src, "#{user_at_host}:#{dest}"
15
+ end
16
+
17
+ def scp src, dest, options = {}
18
+ args = [src, "#{user_at_host}:#{dest}"]
19
+ args.unshift '-r' if options[:recurse]
20
+ Buffet.run! 'scp', *args
21
+ end
22
+
23
+ def execute_in_project command
24
+ execute "cd #{@project.directory_on_slave} && #{command}"
25
+ end
26
+
27
+ def execute command
28
+ Buffet.run! 'ssh', "#{user_at_host}", command
29
+ end
30
+
31
+ def name
32
+ user_at_host
33
+ end
34
+
35
+ def user_at_host
36
+ "#{@user}@#{@host}"
37
+ end
38
+
39
+ private
40
+
41
+ def rsync_exclude_flags
42
+ if Settings.has_exclude_filter_file?
43
+ exclude_flags = "--exclude-from=#{Settings.exclude_filter_file}"
44
+ end
45
+ exclude_flags || ''
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module Buffet
2
+ VERSION = '0.6'
3
+ end
data/lib/buffet.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'fileutils'
2
+ require 'find'
3
+ require 'logger'
4
+ require 'pathname'
5
+
6
+ module Buffet
7
+ autoload :CommandRunner, 'buffet/command_runner'
8
+ autoload :Master, 'buffet/master'
9
+ autoload :Project, 'buffet/project'
10
+ autoload :Runner, 'buffet/runner'
11
+ autoload :Settings, 'buffet/settings'
12
+ autoload :Slave, 'buffet/slave'
13
+
14
+ def self.logdir
15
+ @logdir ||= Pathname.new(ENV['HOME']) + '.buffet/log'
16
+ end
17
+
18
+ def self.logfile
19
+ 'buffet.log'
20
+ end
21
+
22
+ def self.logger
23
+ @logger ||= begin
24
+ FileUtils.mkdir_p logdir
25
+ Logger.new logdir + logfile
26
+ end
27
+ end
28
+
29
+ def self.runner
30
+ @runner ||= CommandRunner.new logger
31
+ end
32
+
33
+ def self.run! *command
34
+ result = runner.run *command
35
+ unless result.success?
36
+ logger.error 'exiting due to non-zero exit status'
37
+ exit result.status
38
+ end
39
+ result
40
+ end
41
+
42
+ # Given a set of files/directories, return all spec files contained
43
+ def self.extract_specs_from files
44
+ specs = []
45
+ files.each do |spec_file|
46
+ Find.find(spec_file) do |f|
47
+ specs << f if f.match /_spec\.rb$/
48
+ end
49
+ end
50
+ specs.uniq
51
+ end
52
+
53
+ def self.workspace_dir
54
+ ".buffet/workspaces/#{user}" # Relative to home directory
55
+ end
56
+
57
+ def self.user
58
+ @user ||= `whoami`.chomp
59
+ end
60
+ end
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'drb'
4
+ require 'fileutils'
5
+
6
+ if File.exist? 'Gemfile'
7
+ require 'rubygems'
8
+ require 'bundler/setup'
9
+ end
10
+
11
+ # NOTE: ARGV is used by the spec runner. If I leave the drb server address
12
+ # in ARGV, rspec will think it's an argument to the test runner.
13
+ buffet_server = DRbObject.new_with_uri(ARGV.shift)
14
+ slave_name = ARGV.shift
15
+ framework = ARGV.shift
16
+
17
+ FileUtils.mkdir_p('./tmp')
18
+
19
+ if framework == 'RSPEC1'
20
+ require 'spec'
21
+ require 'spec/runner/command_line'
22
+ require File.dirname(__FILE__) + '/rspec1_formatter.rb'
23
+
24
+ Spec::Runner::Formatter::AugmentedTextFormatter.configure buffet_server, slave_name
25
+
26
+ while file = buffet_server.next_file_for(slave_name)
27
+ # RSpec1 closes stderr/out after each run, so we reopen them each time
28
+ outlog = File.open('./tmp/buffet.out.log', 'a')
29
+ errlog = File.open('./tmp/buffet.error.log', 'a')
30
+
31
+ Spec::Runner::CommandLine.run(
32
+ Spec::Runner::OptionParser.parse(
33
+ ['--format', 'Spec::Runner::Formatter::AugmentedTextFormatter', file],
34
+ errlog,
35
+ outlog
36
+ )
37
+ )
38
+ end
39
+ else
40
+ require 'rspec'
41
+ require File.dirname(__FILE__) + '/rspec2_formatter.rb'
42
+
43
+ RSpec::Core::Formatters::AugmentedTextFormatter.configure buffet_server, slave_name
44
+
45
+ while file = buffet_server.next_file_for(slave_name)
46
+ RSpec::Core::CommandLine.new(
47
+ ['--format', 'RSpec::Core::Formatters::AugmentedTextFormatter', file]
48
+ ).run($stderr, $stdout)
49
+
50
+ RSpec.world.reset
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec/runner/formatter/base_text_formatter'
2
+
3
+ module Spec
4
+ module Runner
5
+ module Formatter
6
+ class AugmentedTextFormatter < BaseTextFormatter
7
+ def self.configure buffet_server, slave_name
8
+ @@buffet_server = buffet_server
9
+ @@slave_name = slave_name
10
+ end
11
+
12
+ def example_passed example_proxy
13
+ super
14
+ @@buffet_server.example_passed(@@slave_name, {
15
+ :description => example_proxy.description,
16
+ :location => example_proxy.location,
17
+ :slave_name => @@slave_name,
18
+ })
19
+ end
20
+
21
+ def example_failed example_proxy, counter, failure
22
+ super
23
+ @@buffet_server.example_failed(@@slave_name, {
24
+ :backtrace => failure.exception.backtrace.join("\n"),
25
+ :description => failure.header,
26
+ :location => example_proxy.location,
27
+ :message => failure.exception.message,
28
+ :slave_name => @@slave_name,
29
+ })
30
+ end
31
+
32
+ def example_pending example, message, deprecated_pending_location=nil
33
+ super
34
+ @@buffet_server.example_pending(@@slave_name, {
35
+ :description => example.description,
36
+ :location => example.location,
37
+ :message => message,
38
+ :slave_name => @@slave_name,
39
+ })
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ require 'rspec/core/formatters/base_text_formatter'
2
+
3
+ module RSpec
4
+ module Core
5
+ module Formatters
6
+ class AugmentedTextFormatter < BaseTextFormatter
7
+ def initialize(output)
8
+ super(output)
9
+ end
10
+
11
+ def self.configure buffet_server, slave_name
12
+ @@buffet_server = buffet_server
13
+ @@slave_name = slave_name
14
+ end
15
+
16
+ def example_passed example
17
+ super
18
+ @@buffet_server.example_passed(@@slave_name, {
19
+ :description => example.description,
20
+ :location => example.location,
21
+ :slave_name => slave_name,
22
+ })
23
+ end
24
+
25
+ def example_failed example
26
+ super
27
+ exception = example.metadata[:execution_result][:exception]
28
+ backtrace = format_backtrace(exception.backtrace, example).join("\n")
29
+
30
+ @@buffet_server.example_failed(@@slave_name, {
31
+ :description => description,
32
+ :backtrace => backtrace,
33
+ :message => exception.message,
34
+ :location => example.location,
35
+ :slave_name => slave_name,
36
+ })
37
+ end
38
+
39
+ def example_pending example, message, deprecated_pending_location=nil
40
+ super
41
+ @@buffet_server.example_pending(@@slave_name, {
42
+ :description => example.description,
43
+ :location => example.location,
44
+ :message => message,
45
+ :slave_name => slave_name,
46
+ })
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minimal-buffet
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 6
8
+ version: "0.6"
9
+ platform: ruby
10
+ authors:
11
+ - Causes Engineering
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2011-12-29 00:00:00 -05:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: colorize
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ segments:
27
+ - 0
28
+ version: "0"
29
+ type: :runtime
30
+ version_requirements: *id001
31
+ - !ruby/object:Gem::Dependency
32
+ name: wopen3
33
+ prerelease: false
34
+ requirement: &id002 !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ segments:
39
+ - 0
40
+ version: "0"
41
+ type: :runtime
42
+ version_requirements: *id002
43
+ - !ruby/object:Gem::Dependency
44
+ name: rspec
45
+ prerelease: false
46
+ requirement: &id003 !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ type: :development
54
+ version_requirements: *id003
55
+ - !ruby/object:Gem::Dependency
56
+ name: mkdtemp
57
+ prerelease: false
58
+ requirement: &id004 !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ type: :development
66
+ version_requirements: *id004
67
+ description: Buffet distributes RSpec test cases over multiple machines.
68
+ email:
69
+ - eng@causes.com
70
+ - grant@causes.com
71
+ - shane@causes.com
72
+ executables:
73
+ - buffet
74
+ extensions: []
75
+
76
+ extra_rdoc_files: []
77
+
78
+ files:
79
+ - lib/buffet.rb
80
+ - lib/buffet/cli.rb
81
+ - lib/buffet/command_runner.rb
82
+ - lib/buffet/master.rb
83
+ - lib/buffet/project.rb
84
+ - lib/buffet/runner.rb
85
+ - lib/buffet/settings.rb
86
+ - lib/buffet/slave.rb
87
+ - lib/buffet/version.rb
88
+ - support/buffet-worker
89
+ - support/rspec1_formatter.rb
90
+ - support/rspec2_formatter.rb
91
+ has_rdoc: true
92
+ homepage: http://github.com/causes/buffet
93
+ licenses: []
94
+
95
+ post_install_message:
96
+ rdoc_options: []
97
+
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ requirements: []
115
+
116
+ rubyforge_project:
117
+ rubygems_version: 1.3.6
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: Distributed testing framework for Ruby, Rails and RSpec
121
+ test_files: []
122
+