minimal-buffet 0.6

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/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
+