Permalink
Browse files

Initial version.

  • Loading branch information...
0 parents commit 8d4459a4df6dc6c5d809f8fbf77f693e56d054e7 @wisq committed Aug 5, 2011
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in rails_parallel.gemspec
+gemspec
@@ -0,0 +1,33 @@
+rails_parallel
+==============
+
+rails_parallel makes your Rails tests scale with the number of CPU cores available.
+
+It also speeds up the testing process in general, by making heavy use of forking to only have to load the Rails environment once.
+
+Installation
+------------
+
+To load rails_parallel, require "rails_parallel/rake" early in your Rakefile. One possibility is to load it conditionally based on an environment variable:
+
+ require 'rails_parallel/rake' if ENV['PARALLEL']
+
+You'll want to add a lib/tasks/rails_parallel.rake with at least the following:
+
+ # RailsParallel handles the DB schema.
+ Rake::Task['test:prepare'].clear_prerequisites if Object.const_get(:RailsParallel)
+
+ namespace :parallel do
+ # Run this task if you have non-test tasks to run first and you want the
+ # RailsParallel worker to start loading your environment earlier.
+ task :launch do
+ RailsParallel::Rake.launch
+ end
+
+ # RailsParallel runs this if it needs to reload the DB.
+ namespace :db do
+ task :setup => ['db:drop', 'db:create', 'db:schema:load']
+ end
+ end
+
+This gem was designed as an internal project and currently makes certain assumptions about your project setup, such as the use of MySQL and a separate versioned schema (rather than db/schema.rb). These will become more generic in future versions.
@@ -0,0 +1,2 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
@@ -0,0 +1,23 @@
+#!/usr/bin/env ruby
+
+ENV['RAILS_ENV'] = 'test'
+
+begin
+ puts 'RP: Loading RailsParallel.'
+ $LOAD_PATH << 'lib'
+ require 'rails_parallel/runner'
+ require 'rails_parallel/object_socket'
+
+ socket = ObjectSocket.new(IO.for_fd(ARGV.first.to_i))
+ socket << :started
+
+ puts 'RP: Loading Rails.'
+ require "#{ENV['RAILS_PARALLEL_ROOT']}/config/environment"
+
+ puts 'RP: Ready for testing.'
+ RailsParallel::Runner.launch(socket)
+ puts 'RP: Shutting down.'
+ Kernel.exit!(0)
+rescue Interrupt, SignalException
+ Kernel.exit!(1)
+end
@@ -0,0 +1,3 @@
+module RailsParallel
+ # Nothing here. Require 'rails_parallel/rake' in your Rakefile if you want RP.
+end
@@ -0,0 +1,32 @@
+require 'test/unit/collector'
+
+module RailsParallel
+ class Collector
+ include Test::Unit::Collector
+
+ NAME = 'collected from the ObjectSpace'
+
+ def prepare(timings, test_name)
+ @suites = {}
+ ::ObjectSpace.each_object(Class) do |klass|
+ @suites[klass.name] = klass.suite if Test::Unit::TestCase > klass
+ end
+
+ @pending = @suites.keys.sort_by do |name|
+ [
+ 0 - timings.fetch(test_name, name), # runtime, descending
+ 0 - @suites[name].size, # no. of tests, descending
+ name
+ ]
+ end
+ end
+
+ def next_suite
+ @pending.shift
+ end
+
+ def suite_for(name)
+ @suites[name]
+ end
+ end
+end
@@ -0,0 +1,39 @@
+module RailsParallel
+ module Forks
+ def fork_and_run
+ ActiveRecord::Base.connection.disconnect! if ActiveRecord::Base.connected?
+
+ fork do
+ begin
+ yield
+ Kernel.exit!(0)
+ rescue Interrupt, SignalException
+ Kernel.exit!(1)
+ rescue Exception => e
+ puts "Error: #{e}"
+ puts(*e.backtrace.map {|t| "\t#{t}"})
+ before_exit
+ Kernel.exit!(1)
+ end
+ end
+ end
+
+ def wait_for(pid, nonblock = false)
+ pid = Process.waitpid(pid, nonblock ? Process::WNOHANG : 0)
+ check_status($?) if pid
+ pid
+ end
+
+ def wait_any(nonblock = false)
+ wait_for(-1, nonblock)
+ end
+
+ def check_status(stat)
+ raise "error: #{stat.inspect}" unless stat.success?
+ end
+
+ def before_exit
+ # cleanup here (in children)
+ end
+ end
+end
@@ -0,0 +1,76 @@
+require 'rubygems'
+require 'socket'
+
+class ObjectSocket
+ BLOCK_SIZE = 4096
+
+ attr_reader :socket
+
+ def self.pair
+ Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0).map { |s| new(s) }
+ end
+
+ def initialize(socket)
+ @socket = socket
+ @buffer = ''
+ end
+
+ def nonblock=(val)
+ @nonblock = val
+ end
+
+ def close
+ @socket.close
+ end
+
+ def nonblocking(&block)
+ with_nonblock(true, &block)
+ end
+ def blocking(&block)
+ with_nonblock(false, &block)
+ end
+
+ def each_object(&block)
+ first = true
+ loop do
+ process_buffer(&block) if first
+ first = false
+
+ @buffer += @nonblock ? @socket.read_nonblock(BLOCK_SIZE) : @socket.readpartial(BLOCK_SIZE)
+ process_buffer(&block)
+ end
+ rescue Errno::EAGAIN
+ # end of nonblocking data
+ end
+
+ def next_object
+ each_object { |o| return o }
+ nil # no pending data in nonblock mode
+ end
+
+ def <<(obj)
+ data = Marshal.dump(obj)
+ @socket.syswrite [data.size, data].pack('Na*')
+ self # chainable
+ end
+
+ private
+
+ def process_buffer
+ while @buffer.size >= 4
+ size = 4 + @buffer.unpack('N').first
+ break unless @buffer.size >= size
+
+ packet = @buffer.slice!(0, size)
+ yield Marshal.load(packet[4..-1])
+ end
+ end
+
+ def with_nonblock(value)
+ old_value = @nonblock
+ @nonblock = value
+ return yield
+ ensure
+ @nonblock = old_value
+ end
+end
Oops, something went wrong.

0 comments on commit 8d4459a

Please sign in to comment.