Skip to content

Commit

Permalink
Merge branch 'slave'
Browse files Browse the repository at this point in the history
  • Loading branch information
francois committed Nov 17, 2009
2 parents 1667722 + 3ec56b0 commit 7b1e36a
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 44 deletions.
6 changes: 6 additions & 0 deletions README.rdoc
Expand Up @@ -18,6 +18,12 @@ At this early stage, Nestor is pretty verbose regarding it's operations. You ca

Yes, I mean server in the sense that Nestor will load +test/test_helper.rb+ and run your tests by forking. Changing +test/test_helper.rb+ or anything in +config/+ will abort Nestor. With some more work, Nestor will be able to restart itself. Nestor also knows about +db/schema.rb+ and will run +rake db:test:prepare+ in the advent your schema changes.

=== Caveats / Warnings

Nestor internally uses the [http://codeforpeople.com/lib/ruby/slave/ Slave] gem to process results in a slave process. The processes communicate using DRb. Behavior is undefined if your own tests make use of DRb.

Also, all the top-level constants that Nestor's dependencies declare will pollute your application's namespace: Watchr, Slave, StateMachine and Thor. Should this be a problem, it would be possible to run your tests in a sub-process fashion, similar to the original Autotest. Running in this mode, none of Nestor's constants would impact your process, except DRb, which Slave uses.

== Note on Patches/Pull Requests

* Fork the project.
Expand Down
9 changes: 9 additions & 0 deletions Rakefile
Expand Up @@ -69,3 +69,12 @@ rescue LoadError
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
end
end

namespace :state_machine do
task :draw do
$:.unshift(File.dirname(__FILE__) + "/lib")
require "state_machine"
require "nestor/machine"
StateMachine::Machine.draw("Nestor::Machine", {})
end
end
7 changes: 6 additions & 1 deletion lib/nestor/cli.rb
Expand Up @@ -16,7 +16,7 @@ class Cli < Thor # :nodoc:
Use --quick to boot without running the full test suite on startup.
--debug writes extra Watchr debug messages to STDOUT.
EODESC
method_options :framework => "rails", :testlib => "test/unit", :script => nil, :debug => false, :quick => false
method_options :framework => "rails", :testlib => "test/unit", :script => nil, :debug => false, :quick => false, :require => []
def start
Watchr.options.debug = options[:debug]

Expand All @@ -33,6 +33,11 @@ def start
script_path = options[:script] ? Pathname.new(options[:script]) : nil
script = Nestor::Script.new(script_path || mapper.class.default_script_path)

options[:require].each do |path|
puts "Loading #{path.inspect} plugin"
require path
end

script.nestor_machine = machine
Watchr::Controller.new(script, Watchr.handler.new).run
end
Expand Down
37 changes: 19 additions & 18 deletions lib/nestor/machine.rb
Expand Up @@ -109,19 +109,6 @@ def log(*args)
@mapper.log(*args)
end

# Indicates the run was succesful: a green build. This does not indicate that the
# whole build was successful: only that the files that ran last were successful.
def run_successful!(files, tests)
successful!
end

# Indicates there were one or more failures. +files+ lists the actual files
# that failed, while +tests+ indicates the test names or examples that failed.
def run_failed!(files, tests)
@focused_files, @focuses = files, tests
failed!
end

# Notifies the Machine that a file changed. This might trigger a state change and schedule a build.
def changed!(file)
mapped_files = mapper.map(file)
Expand All @@ -133,35 +120,49 @@ def changed!(file)
else
mapped_files.each do |mapped_file|
mapper.log "#{file} => #{mapped_file}"
@changed_file = mapped_file
self.changed_file = mapped_file
file_changed!
end
end
end

def changed_file=(file)
raise ArgumentError, "Only accepts a single file at a time, got #{file.inspect}" unless String === file
@changed_file = file
end

private

def run_all_tests
reset_focused_files
reset_focuses
@mapper.run_all
process!(@mapper.run_all)
end

def run_multi_tests
reset_focuses
@mapper.run(focused_files)
process!(@mapper.run(focused_files))
end

def run_focused_tests
@mapper.run(focused_files, focuses)
process!(@mapper.run(focused_files, focuses))
end

def process!(info)
@mapper.log(info.inspect)
return successful! if info[:passed]

files, tests = info[:failures].values.uniq, info[:failures].keys
@focused_files, @focuses = files, tests
failed!
end

def reset_focused_files
@focused_files.clear
end

def add_changed_file_to_focused_files
@focused_files << @changed_file unless @focused_files.include?(@changed_file)
@focused_files << changed_file unless @focused_files.include?(changed_file)
end

def changed_file_in_focused_files?
Expand Down
13 changes: 1 addition & 12 deletions lib/nestor/mappers/rails/test/rails_test_unit.rb
Expand Up @@ -49,18 +49,7 @@ def changed!(filename) #:nodoc:
changed! md[0]
end

# the next 2 blocks are for receiving results from the child process
watch 'tmp/nestor-results.yml' do |md|
# Since we received the results, we must receive our child process' status, or
# else we'll leave zombie processes lying around
Thread.start { Process.wait }

info = YAML.load_file(md[0])
log "New results in: #{info.inspect}"
failures = info["failures"]
@machine.send("run_#{info["status"]}!", failures.values.flatten.uniq, failures.keys)
end

# This is only to trigger the tests after a slight delay, but from the main thread.
watch 'tmp/nestor-sendoff' do |_|
log "Sendoff"
@machine.run!
Expand Down
85 changes: 72 additions & 13 deletions lib/nestor/mappers/rails/test/unit.rb
Expand Up @@ -54,27 +54,27 @@ def log(message)

# Runs absolutely all tests as found by walking test/.
def run_all
fork do
receive_results do
log "Run all tests"
test_files = Dir["test/**/*_test.rb"]
test_files.each {|f| log(f); load f}
test_files = load_test_files(["test"])

ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
test_runner = ::Nestor::Mappers::Rails::Test::TestRunner.new(nil)
result = ::Test::Unit::AutoRunner.run(false, nil, []) do |autorunner|
autorunner.runner = lambda { test_runner }
end

# Returns a Hash which the parent process will retrieve
report(test_runner, test_files)
end
end

# Runs only the named files, and optionally focuses on only a couple of tests
# within the loaded test cases.
def run(test_files, focuses=[])
fork do
receive_results do
log "Running #{focuses.length} focused tests"
test_files.each {|f| log(f); load f}
load_test_files(test_files)

ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
test_runner = ::Nestor::Mappers::Rails::Test::TestRunner.new(nil)
Expand All @@ -83,6 +83,7 @@ def run(test_files, focuses=[])
autorunner.filters << proc{|t| focuses.include?(t.method_name)} unless focuses.empty?
end

# Returns a Hash the parent process will retrieve
report(test_runner, test_files)
end
end
Expand Down Expand Up @@ -155,12 +156,71 @@ def map(path)

private

# Since we forked, we can't call into the Machine from the child process. Upstream
# communications is implemented by writing new files to the filesystem and letting
# the parent process catch the changes.
def report(test_runner, test_files)
info = {"status" => test_runner.passed? ? "successful" : "failed", "failures" => {}}
failures = info["failures"]
def setup_lifeline
ppid = Process.ppid
log "Setting up lifeline on #{Process.pid} for #{Process.ppid}"

Thread.start do
sleep 0.5
next if ppid == Process.ppid

# Parent must have died because we don't have the same parent PID
# Die ourselves
log "Dying because parent changed"
exit!
end
end

def receive_results
rd, wr = IO.pipe
fork do
log "Setting up lifeline"
setup_lifeline

log "Closing read-end of the pipe"
rd.close

log "Doing whatever..."
info = yield

log "Returning YAML info to parent process"
wr.write info.to_yaml
end

log "Closing write-end of the pipe"
wr.close

log "Waiting for child process"
Process.wait

info = YAML.load(rd.read)
end

def load_test_files(test_files)
test_files.inject([]) do |memo, f|
case
when File.directory?(f)
Dir["#{f}/**/*_test.rb"].each do |f1|
log(f1)
load f1
memo << f1
end
when File.file?(f)
log(f)
load f
memo << f
else
# Ignore
end

memo
end
end

# Print to STDOUT the results of the run. The parent's listening on the pipe to get the data.
def report(test_runner, test_files, io=STDOUT)
info = {:passed => test_runner.passed?, :failures => {}}
failures = info[:failures]
test_runner.faults.each do |failure|
filename, test_name = self.class.parse_failure(failure, test_files)
if filename.nil? then
Expand All @@ -172,8 +232,7 @@ def report(test_runner, test_files)
end
end

File.open("tmp/nestor-results.yml", "w") {|io| io.write(info.to_yaml) }
log "Wrote #{failures.length} failure(s) to tmp/nestor-results.yml"
info
end
end

Expand Down

0 comments on commit 7b1e36a

Please sign in to comment.