specjour 0.1.1
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/.document +5 -0
- data/.gitignore +21 -0
- data/MIT_LICENSE +20 -0
- data/README.markdown +47 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/bin/specjour +41 -0
- data/lib/specjour/core_ext/array.rb +15 -0
- data/lib/specjour/db_scrub.rb +23 -0
- data/lib/specjour/dispatcher.rb +113 -0
- data/lib/specjour/distributed_formatter.rb +84 -0
- data/lib/specjour/final_report.rb +60 -0
- data/lib/specjour/manager.rb +67 -0
- data/lib/specjour/marshalable_failure_formatter.rb +11 -0
- data/lib/specjour/marshalable_rspec_failure.rb +33 -0
- data/lib/specjour/printer.rb +65 -0
- data/lib/specjour/protocol.rb +21 -0
- data/lib/specjour/rsync_daemon.rb +60 -0
- data/lib/specjour/tasks/dispatch.rake +13 -0
- data/lib/specjour/tasks/specjour.rb +1 -0
- data/lib/specjour/worker.rb +55 -0
- data/lib/specjour.rb +22 -0
- data/rails/init.rb +4 -0
- data/spec/lib/specjour/core_ext/array_spec.rb +31 -0
- data/spec/lib/specjour/worker_spec.rb +14 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/specjour_spec.rb +5 -0
- metadata +145 -0
data/.document
ADDED
data/.gitignore
ADDED
data/MIT_LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Sandro Turriate
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Specjour
|
2
|
+
|
3
|
+
_Distribute your spec suite amongst your LAN via Bonjour._
|
4
|
+
|
5
|
+
1. Start a dispatcher in your project directory.
|
6
|
+
2. Spin up a manager on each remote machine.
|
7
|
+
3. Say "goodbye" to your long coffee breaks.
|
8
|
+
|
9
|
+
## Requirements
|
10
|
+
|
11
|
+
* Bonjour or DNSSD (the capability and the gem)
|
12
|
+
* Rsync (system command used)
|
13
|
+
* Rspec (officially v1.3.0)
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
gem install specjour
|
17
|
+
|
18
|
+
## Start a manager
|
19
|
+
Running `specjour` on the command-line will start a manager which advertises that it's ready to run tests. By default, the manager will only use one worker to run the tests. If you had 4 cores however, you could use `specjour --workers 4` to run 4 sets of tests at once.
|
20
|
+
|
21
|
+
$ specjour
|
22
|
+
|
23
|
+
## Setup the dispatcher
|
24
|
+
Add the rake task to the `Rakefile` in your project's directory.
|
25
|
+
|
26
|
+
require 'specjour/tasks/specjour'
|
27
|
+
|
28
|
+
## Distribute the tests
|
29
|
+
Run the rake task in your project directory to start the test suite.
|
30
|
+
|
31
|
+
$ rake specjour
|
32
|
+
|
33
|
+
The worker reports passes/failures in batches of 25 so you won't get immediate feedback, override the batch size via `specjour --batch-size 1`
|
34
|
+
|
35
|
+
## Note on Patches/Pull Requests
|
36
|
+
|
37
|
+
* Fork the project.
|
38
|
+
* Make your feature addition or bug fix.
|
39
|
+
* Add tests for it. This is important so I don't break it in a
|
40
|
+
future version unintentionally.
|
41
|
+
* Commit, do not mess with rakefile, version, or history.
|
42
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
43
|
+
* Send me a pull request. Bonus points for topic branches.
|
44
|
+
|
45
|
+
## Copyright
|
46
|
+
|
47
|
+
Copyright (c) 2010 Sandro Turriate. See MIT_LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "specjour"
|
8
|
+
gem.summary = %Q{Distribute your spec suite amongst your LAN via Bonjour.}
|
9
|
+
gem.description = %Q{Distribute your spec suite amongst your LAN via Bonjour.}
|
10
|
+
gem.email = "sandro.turriate@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/sandro/specjour"
|
12
|
+
gem.authors = ["Sandro Turriate"]
|
13
|
+
gem.add_dependency "dnssd", "1.3.1"
|
14
|
+
gem.add_dependency "rspec"
|
15
|
+
gem.add_development_dependency "rspec", "1.3.0"
|
16
|
+
gem.add_development_dependency "yard", "0.5.3"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
begin
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
43
|
+
rescue LoadError
|
44
|
+
task :yardoc do
|
45
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
$:.unshift(File.dirname(__FILE__) + "/lib")
|
50
|
+
require 'specjour/tasks/specjour'
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/bin/specjour
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'specjour'
|
4
|
+
|
5
|
+
options = {:worker_size => 1, :batch_size => 25}
|
6
|
+
|
7
|
+
optparse = OptionParser.new do |opts|
|
8
|
+
opts.banner = "Usage: specjour [options]"
|
9
|
+
|
10
|
+
opts.on('-w', '--workers [WORKERS]', Numeric, "Number of WORKERS to spin up, defaults to #{options[:worker_size]}") do |n|
|
11
|
+
options[:worker_size] = n
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on('-b', '--batch-size [SIZE]', Integer, "Number of specs to run before reporting back to the dispatcher, defaults to #{options[:batch_size]}") do |n|
|
15
|
+
options[:batch_size] = n
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on('--do-work OPTIONS', Array, 'INTERNAL USE ONLY') do |args|
|
19
|
+
specs_to_run = args[3..-1]
|
20
|
+
options[:worker_args] = args[0], args[1], args[2], specs_to_run
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on_tail('-v', '--version', 'Show the version of specjour') do
|
24
|
+
abort Specjour::VERSION
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
28
|
+
summary = opts.to_a
|
29
|
+
summary.first << "\n"
|
30
|
+
abort summary.reject {|s| s =~ /INTERNAL/}.join
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
optparse.parse!
|
35
|
+
|
36
|
+
if options[:worker_args]
|
37
|
+
options[:worker_args] << options[:batch_size]
|
38
|
+
Specjour::Worker.new(*options[:worker_args]).run
|
39
|
+
else
|
40
|
+
Specjour::Manager.new(options[:worker_size], options[:batch_size]).start
|
41
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Among
|
3
|
+
def among(group_size)
|
4
|
+
group_size = 1 if group_size.zero?
|
5
|
+
groups = Array.new(group_size) { [] }
|
6
|
+
offset = 0
|
7
|
+
each do |item|
|
8
|
+
groups[offset] << item
|
9
|
+
offset = (offset == group_size - 1) ? 0 : offset + 1
|
10
|
+
end
|
11
|
+
groups
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
::Array.send(:include, Specjour::Among)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Specjour
|
2
|
+
module DbScrub
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def scrub
|
6
|
+
begin
|
7
|
+
ActiveRecord::Base.connection
|
8
|
+
rescue # assume the database doesn't exist
|
9
|
+
create_db_and_schema
|
10
|
+
else
|
11
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
12
|
+
ActiveRecord::Base.connection.delete "delete from #{table}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_db_and_schema
|
18
|
+
load 'Rakefile'
|
19
|
+
Rake::Task['db:create'].invoke
|
20
|
+
Rake::Task['db:schema:load'].invoke
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Dispatcher
|
3
|
+
require 'dnssd'
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
|
6
|
+
attr_reader :project_path, :managers, :manager_threads, :hosts
|
7
|
+
attr_accessor :worker_size
|
8
|
+
|
9
|
+
def initialize(project_path)
|
10
|
+
@project_path = project_path
|
11
|
+
@managers = []
|
12
|
+
@worker_size = 0
|
13
|
+
reset_manager_threads
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
rsync_daemon.start
|
18
|
+
gather_managers
|
19
|
+
sync_managers
|
20
|
+
dispatch_work
|
21
|
+
printer.join
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def all_specs
|
27
|
+
@all_specs ||= Dir.chdir(project_path) do
|
28
|
+
Dir["spec/**/**/*_spec.rb"].partition {|f| f =~ /integration/}.flatten
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def dispatch_work
|
33
|
+
distributable_specs = all_specs.among(worker_size)
|
34
|
+
last_index = 0
|
35
|
+
managers.each_with_index do |manager, index|
|
36
|
+
manager.specs_to_run = Array.new(manager.worker_size) do |i|
|
37
|
+
distributable_specs[last_index + i]
|
38
|
+
end
|
39
|
+
last_index += manager.worker_size
|
40
|
+
manager_threads << Thread.new(manager) {|m| m.dispatch}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def drb_start
|
45
|
+
DRb.start_service nil, self
|
46
|
+
at_exit { puts 'shutting down DRb client'; DRb.stop_service }
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_manager(uri)
|
50
|
+
manager = DRbObject.new_with_uri(uri.to_s)
|
51
|
+
unless managers.include?(manager)
|
52
|
+
set_up_manager(manager, uri)
|
53
|
+
managers << manager
|
54
|
+
self.worker_size += manager.worker_size
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def gather_managers
|
59
|
+
puts "Waiting for managers"
|
60
|
+
Signal.trap('INT') { exit }
|
61
|
+
browser = DNSSD::Service.new
|
62
|
+
browser.browse '_druby._tcp' do |reply|
|
63
|
+
if reply.flags.add?
|
64
|
+
resolve_reply(reply)
|
65
|
+
end
|
66
|
+
browser.stop unless reply.flags.more_coming?
|
67
|
+
end
|
68
|
+
puts "Managers found: #{managers.size}"
|
69
|
+
printer.worker_size = worker_size
|
70
|
+
end
|
71
|
+
|
72
|
+
def printer
|
73
|
+
@printer ||= Printer.new.start
|
74
|
+
end
|
75
|
+
|
76
|
+
def project_name
|
77
|
+
@project_name ||= File.basename(project_path)
|
78
|
+
end
|
79
|
+
|
80
|
+
def reset_manager_threads
|
81
|
+
@manager_threads = []
|
82
|
+
end
|
83
|
+
|
84
|
+
def resolve_reply(reply)
|
85
|
+
DNSSD.resolve!(reply) do |resolved|
|
86
|
+
uri = URI::Generic.build :scheme => reply.service_name, :host => resolved.target, :port => resolved.port
|
87
|
+
fetch_manager(uri)
|
88
|
+
resolved.service.stop if resolved.service.started?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def rsync_daemon
|
93
|
+
@rsync_daemon ||= RsyncDaemon.new(project_path, project_name)
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_up_manager(manager, uri)
|
97
|
+
manager.project_name = project_name
|
98
|
+
manager.dispatcher_uri = URI::Generic.build :scheme => "specjour", :host => printer.host, :port => printer.port
|
99
|
+
end
|
100
|
+
|
101
|
+
def sync_managers
|
102
|
+
managers.each do |manager|
|
103
|
+
manager_threads << Thread.new(manager) { |manager| manager.sync }
|
104
|
+
end
|
105
|
+
wait_on_managers
|
106
|
+
end
|
107
|
+
|
108
|
+
def wait_on_managers
|
109
|
+
manager_threads.each {|t| t.join; t.exit}
|
110
|
+
reset_manager_threads
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Specjour
|
2
|
+
class DistributedFormatter < Spec::Runner::Formatter::BaseTextFormatter
|
3
|
+
require 'specjour/marshalable_rspec_failure'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :batch_size
|
7
|
+
end
|
8
|
+
@batch_size = 1
|
9
|
+
|
10
|
+
attr_reader :failing_messages, :passing_messages, :pending_messages, :output
|
11
|
+
attr_reader :duration, :example_count, :failure_count, :pending_count, :pending_examples, :failing_examples
|
12
|
+
|
13
|
+
def initialize(options, output)
|
14
|
+
@options = options
|
15
|
+
@output = output.extend Specjour::Protocol
|
16
|
+
@failing_messages = []
|
17
|
+
@passing_messages = []
|
18
|
+
@pending_messages = []
|
19
|
+
@pending_examples = []
|
20
|
+
@failing_examples = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def example_failed(example, counter, failure)
|
24
|
+
failing_messages << colorize_failure('F', failure)
|
25
|
+
batch_print(failing_messages)
|
26
|
+
end
|
27
|
+
|
28
|
+
def example_passed(example)
|
29
|
+
passing_messages << green('.')
|
30
|
+
batch_print(passing_messages)
|
31
|
+
end
|
32
|
+
|
33
|
+
def example_pending(example, message, deprecated_pending_location=nil)
|
34
|
+
super
|
35
|
+
pending_messages << yellow('*')
|
36
|
+
batch_print(pending_messages)
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump_summary(duration, example_count, failure_count, pending_count)
|
40
|
+
@duration = duration
|
41
|
+
@example_count = example_count
|
42
|
+
@failure_count = failure_count
|
43
|
+
@pending_count = pending_count
|
44
|
+
output.puts [:worker_summary=, to_hash]
|
45
|
+
output.flush
|
46
|
+
end
|
47
|
+
|
48
|
+
def dump_pending
|
49
|
+
#noop
|
50
|
+
end
|
51
|
+
|
52
|
+
def dump_failure(counter, failure)
|
53
|
+
failing_examples << failure
|
54
|
+
end
|
55
|
+
|
56
|
+
def start_dump
|
57
|
+
print_and_flush failing_messages
|
58
|
+
print_and_flush passing_messages
|
59
|
+
print_and_flush pending_messages
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_hash
|
63
|
+
h = {}
|
64
|
+
[:duration, :example_count, :failure_count, :pending_count, :pending_examples, :failing_examples].each do |key|
|
65
|
+
h[key] = send(key)
|
66
|
+
end
|
67
|
+
h
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def batch_print(messages)
|
73
|
+
if messages.size == self.class.batch_size
|
74
|
+
print_and_flush(messages)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def print_and_flush(messages)
|
79
|
+
output.print messages.to_s
|
80
|
+
output.flush
|
81
|
+
messages.replace []
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Specjour
|
2
|
+
class FinalReport
|
3
|
+
require 'specjour/marshalable_rspec_failure'
|
4
|
+
attr_reader :duration, :example_count, :failure_count, :pending_count, :pending_examples, :failing_examples
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@duration = 0.0
|
8
|
+
@example_count = 0
|
9
|
+
@failure_count = 0
|
10
|
+
@pending_count = 0
|
11
|
+
@pending_examples = []
|
12
|
+
@failing_examples = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(stats)
|
16
|
+
stats.each do |key, value|
|
17
|
+
if key == :duration
|
18
|
+
@duration = value.to_f if duration < value.to_f
|
19
|
+
else
|
20
|
+
increment(key, value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def increment(key, value)
|
26
|
+
current = instance_variable_get("@#{key}")
|
27
|
+
instance_variable_set("@#{key}", current + value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def formatter_options
|
31
|
+
@formatter_options ||= OpenStruct.new(
|
32
|
+
:colour => true,
|
33
|
+
:autospec => false,
|
34
|
+
:dry_run => false
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatter
|
39
|
+
@formatter ||= begin
|
40
|
+
f = MarshalableFailureFormatter.new(formatter_options, $stdout)
|
41
|
+
f.instance_variable_set(:@pending_examples, pending_examples)
|
42
|
+
f
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def summarize
|
47
|
+
if example_count > 0
|
48
|
+
formatter.dump_pending
|
49
|
+
dump_failures
|
50
|
+
formatter.dump_summary(duration, example_count, failure_count, pending_count)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def dump_failures
|
55
|
+
failing_examples.each_with_index do |failure, index|
|
56
|
+
formatter.dump_failure index + 1, failure
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Manager
|
3
|
+
require 'dnssd'
|
4
|
+
include DRbUndumped
|
5
|
+
|
6
|
+
attr_accessor :project_name, :specs_to_run, :dispatcher_uri, :worker_size, :bonjour_service, :batch_size
|
7
|
+
|
8
|
+
def initialize(worker_size = 1, batch_size = 25)
|
9
|
+
@worker_size = worker_size
|
10
|
+
@batch_size = 25
|
11
|
+
end
|
12
|
+
|
13
|
+
def project_path=(name)
|
14
|
+
@project_path = name
|
15
|
+
end
|
16
|
+
|
17
|
+
def project_path
|
18
|
+
@project_path ||= File.join("/tmp", project_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def dispatch
|
22
|
+
bonjour_service.stop
|
23
|
+
pids = []
|
24
|
+
(1..worker_size).each do |index|
|
25
|
+
pids << fork do
|
26
|
+
exec("specjour --batch-size #{batch_size} --do-work #{project_path},#{dispatcher_uri},#{index},#{specs_to_run[index - 1].join(',')}")
|
27
|
+
Kernel.exit!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
at_exit { Process.kill('KILL', *pids) rescue nil }
|
31
|
+
Process.waitall
|
32
|
+
bonjour_announce
|
33
|
+
end
|
34
|
+
|
35
|
+
def start
|
36
|
+
drb_start
|
37
|
+
bonjour_announce
|
38
|
+
Signal.trap('INT') { puts; puts "Shutting down manager..."; exit }
|
39
|
+
DRb.thread.join
|
40
|
+
end
|
41
|
+
|
42
|
+
def drb_start
|
43
|
+
DRb.start_service nil, self
|
44
|
+
Kernel.puts "Manager started at #{drb_uri}"
|
45
|
+
at_exit { DRb.stop_service }
|
46
|
+
end
|
47
|
+
|
48
|
+
def sync
|
49
|
+
cmd "rsync -a --port=8989 #{dispatcher_uri.host}::#{project_name} #{project_path}"
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def cmd(command)
|
55
|
+
Kernel.puts command
|
56
|
+
system command
|
57
|
+
end
|
58
|
+
|
59
|
+
def drb_uri
|
60
|
+
@drb_uri ||= URI.parse(DRb.uri)
|
61
|
+
end
|
62
|
+
|
63
|
+
def bonjour_announce
|
64
|
+
@bonjour_service = DNSSD.register! "specjour_manager_#{object_id}", "_#{drb_uri.scheme}._tcp", nil, drb_uri.port
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Specjour
|
2
|
+
class MarshalableFailureFormatter < Spec::Runner::Formatter::BaseTextFormatter
|
3
|
+
def dump_failure(counter, failure)
|
4
|
+
@output.puts
|
5
|
+
@output.puts "#{counter.to_s})"
|
6
|
+
@output.puts colorize_failure("#{failure.header}\n#{failure.message}", failure)
|
7
|
+
@output.puts format_backtrace(failure.backtrace)
|
8
|
+
@output.flush
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Spec::Runner::Reporter::Failure
|
3
|
+
attr_reader :backtrace, :message, :header, :exception_class_name
|
4
|
+
|
5
|
+
def initialize(group_description, example_description, exception)
|
6
|
+
@example_name = "#{group_description} #{example_description}"
|
7
|
+
@message = exception.message
|
8
|
+
@backtrace = exception.backtrace
|
9
|
+
@exception_class_name = exception.class.name
|
10
|
+
@pending_fixed = exception.is_a?(Spec::Example::PendingExampleFixedError)
|
11
|
+
@exception_not_met = exception.is_a?(Spec::Expectations::ExpectationNotMetError)
|
12
|
+
set_header
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_header
|
16
|
+
if expectation_not_met?
|
17
|
+
@header = "'#{@example_name}' FAILED"
|
18
|
+
elsif pending_fixed?
|
19
|
+
@header = "'#{@example_name}' FIXED"
|
20
|
+
else
|
21
|
+
@header = "#{exception_class_name} in '#{@example_name}'"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def pending_fixed?
|
26
|
+
@pending_fixed
|
27
|
+
end
|
28
|
+
|
29
|
+
def expectation_not_met?
|
30
|
+
@exception_not_met
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Printer < GServer
|
3
|
+
include Protocol
|
4
|
+
RANDOM_PORT = 0
|
5
|
+
|
6
|
+
attr_reader :completed_workers
|
7
|
+
attr_accessor :worker_size
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(
|
11
|
+
port = RANDOM_PORT,
|
12
|
+
host = hostname,
|
13
|
+
max_connections = 100,
|
14
|
+
stdlog = $stderr,
|
15
|
+
audit = true,
|
16
|
+
debug = true
|
17
|
+
)
|
18
|
+
@completed_workers = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def serve(client)
|
22
|
+
client.each(TERMINATOR) do |data|
|
23
|
+
process load_object(data)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def worker_summary=(summary)
|
28
|
+
report.add(summary)
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def disconnecting(client_port)
|
34
|
+
@completed_workers += 1
|
35
|
+
if completed_workers == worker_size
|
36
|
+
stop
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def hostname
|
41
|
+
@hostname ||= Socket.gethostname
|
42
|
+
end
|
43
|
+
|
44
|
+
def log(msg)
|
45
|
+
#noop
|
46
|
+
end
|
47
|
+
|
48
|
+
def process(message)
|
49
|
+
if message.is_a?(String)
|
50
|
+
$stdout.print message
|
51
|
+
$stdout.flush
|
52
|
+
elsif message.is_a?(Array)
|
53
|
+
send(message.first, message[1])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def report
|
58
|
+
@report ||= FinalReport.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def stopping
|
62
|
+
report.summarize
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Protocol
|
3
|
+
TERMINATOR = "|ruojceps|"
|
4
|
+
|
5
|
+
def puts(arg)
|
6
|
+
print(arg << "\n")
|
7
|
+
end
|
8
|
+
|
9
|
+
def print(arg)
|
10
|
+
super dump_object(arg)
|
11
|
+
end
|
12
|
+
|
13
|
+
def dump_object(data)
|
14
|
+
Marshal.dump(data) << TERMINATOR
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_object(data)
|
18
|
+
Marshal.load(data.sub(/#{TERMINATOR}$/, ''))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Specjour
|
2
|
+
class RsyncDaemon
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
attr_reader :project_path, :project_name
|
6
|
+
def initialize(project_path, project_name)
|
7
|
+
@project_path = project_path
|
8
|
+
@project_name = project_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def config_file
|
12
|
+
File.join("/tmp", "rsyncd.conf")
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
write_config
|
17
|
+
system("rsync", "--daemon", "--config=#{config_file}", "--port=8989")
|
18
|
+
at_exit { puts 'shutting down rsync'; stop }
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop
|
22
|
+
if pid
|
23
|
+
Process.kill("TERM", pid)
|
24
|
+
FileUtils.rm(pid_file)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def write_config
|
31
|
+
File.open(config_file, 'w') do |f|
|
32
|
+
f.write config
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def pid
|
37
|
+
if File.exists?(pid_file)
|
38
|
+
File.read(pid_file).strip.to_i
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def pid_file
|
43
|
+
File.join("/tmp", "#{project_name}_rsync_daemon.pid")
|
44
|
+
end
|
45
|
+
|
46
|
+
def config
|
47
|
+
<<-CONFIG
|
48
|
+
# global configuration
|
49
|
+
use chroot = no
|
50
|
+
timeout = 60
|
51
|
+
read only = yes
|
52
|
+
pid file = #{pid_file}
|
53
|
+
|
54
|
+
[#{project_name}]
|
55
|
+
path = #{project_path}
|
56
|
+
exclude = .git* doc tmp/* public log script
|
57
|
+
CONFIG
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'specjour'
|
2
|
+
|
3
|
+
namespace :specjour do
|
4
|
+
task :dispatch, [:project_path] do |task, args|
|
5
|
+
args.with_defaults :project_path => Rake.original_dir
|
6
|
+
Specjour::Dispatcher.new(args.project_path).start
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Dispatch the [project_path] to listening managers"
|
11
|
+
task :specjour, [:project_path] do |task, args|
|
12
|
+
Rake::Task['specjour:dispatch'].invoke(args[:project_path])
|
13
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
load File.join(File.dirname(__FILE__), "dispatch.rake")
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Worker
|
3
|
+
attr_accessor :dispatcher_uri
|
4
|
+
attr_reader :project_path, :specs_to_run, :number, :batch_size
|
5
|
+
|
6
|
+
def initialize(project_path, dispatcher_uri, number, specs_to_run, batch_size)
|
7
|
+
@project_path = project_path
|
8
|
+
@specs_to_run = specs_to_run
|
9
|
+
@number = number.to_i
|
10
|
+
@batch_size = batch_size.to_i
|
11
|
+
self.dispatcher_uri = dispatcher_uri
|
12
|
+
end
|
13
|
+
|
14
|
+
def dispatcher_uri=(val)
|
15
|
+
@dispatcher_uri = URI.parse(val)
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
puts "Running #{specs_to_run.size} spec files..."
|
20
|
+
GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
|
21
|
+
DistributedFormatter.batch_size = batch_size
|
22
|
+
Dir.chdir(project_path) do
|
23
|
+
set_env_variables
|
24
|
+
options = Spec::Runner::OptionParser.parse(
|
25
|
+
rspec_options,
|
26
|
+
STDERR,
|
27
|
+
dispatcher
|
28
|
+
)
|
29
|
+
Spec::Runner.use options
|
30
|
+
options.run_examples
|
31
|
+
Spec::Runner.options.instance_variable_set(:@examples_run, true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def dispatcher
|
38
|
+
@dispatcher ||= TCPSocket.open dispatcher_uri.host, dispatcher_uri.port
|
39
|
+
end
|
40
|
+
|
41
|
+
def rspec_options
|
42
|
+
%w(--format=Specjour::DistributedFormatter) + specs_to_run
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_env_variables
|
46
|
+
ENV['PREPARE_DB'] = 'true'
|
47
|
+
ENV['RSPEC_COLOR'] = 'true'
|
48
|
+
if number > 1
|
49
|
+
ENV['TEST_ENV_NUMBER'] = number.to_s
|
50
|
+
else
|
51
|
+
ENV['TEST_ENV_NUMBER'] = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/specjour.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec'
|
2
|
+
require 'spec/runner/formatter/base_text_formatter'
|
3
|
+
require 'specjour/protocol'
|
4
|
+
require 'specjour/core_ext/array'
|
5
|
+
|
6
|
+
autoload :URI, 'uri'
|
7
|
+
autoload :DRb, 'drb'
|
8
|
+
autoload :Forwardable, 'forwardable'
|
9
|
+
autoload :GServer, 'gserver'
|
10
|
+
|
11
|
+
module Specjour
|
12
|
+
autoload :Dispatcher, 'specjour/dispatcher'
|
13
|
+
autoload :DistributedFormatter, 'specjour/distributed_formatter'
|
14
|
+
autoload :FinalReport, 'specjour/final_report'
|
15
|
+
autoload :Manager, 'specjour/manager'
|
16
|
+
autoload :MarshalableFailureFormatter, 'specjour/marshalable_failure_formatter'
|
17
|
+
autoload :Printer, 'specjour/printer'
|
18
|
+
autoload :RsyncDaemon, 'specjour/rsync_daemon'
|
19
|
+
autoload :Worker, 'specjour/worker'
|
20
|
+
|
21
|
+
VERSION = "0.1.1".freeze
|
22
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Array splitting among many" do
|
4
|
+
describe "#among" do
|
5
|
+
let(:array) { [1,2,3,4,5] }
|
6
|
+
|
7
|
+
it "splits among 0" do
|
8
|
+
array.among(0).should == [[1,2,3,4,5]]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "splits among by 1" do
|
12
|
+
array.among(1).should == [[1,2,3,4,5]]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "splits among by 2" do
|
16
|
+
array.among(2).should == [[1,3,5],[2,4]]
|
17
|
+
end
|
18
|
+
|
19
|
+
it "splits among by 3" do
|
20
|
+
array.among(3).should == [[1,4],[2,5],[3]]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "splits among by 4" do
|
24
|
+
array.among(4).should == [[1,5],[2],[3],[4]]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "splits among by 5" do
|
28
|
+
array.among(5).should == [[1],[2],[3],[4],[5]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: specjour
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Sandro Turriate
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-20 00:00:00 -04:00
|
18
|
+
default_executable: specjour
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: dnssd
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 3
|
30
|
+
- 1
|
31
|
+
version: 1.3.1
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 1
|
55
|
+
- 3
|
56
|
+
- 0
|
57
|
+
version: 1.3.0
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id003
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: yard
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
- 5
|
70
|
+
- 3
|
71
|
+
version: 0.5.3
|
72
|
+
type: :development
|
73
|
+
version_requirements: *id004
|
74
|
+
description: Distribute your spec suite amongst your LAN via Bonjour.
|
75
|
+
email: sandro.turriate@gmail.com
|
76
|
+
executables:
|
77
|
+
- specjour
|
78
|
+
extensions: []
|
79
|
+
|
80
|
+
extra_rdoc_files:
|
81
|
+
- README.markdown
|
82
|
+
files:
|
83
|
+
- .document
|
84
|
+
- .gitignore
|
85
|
+
- MIT_LICENSE
|
86
|
+
- README.markdown
|
87
|
+
- Rakefile
|
88
|
+
- VERSION
|
89
|
+
- bin/specjour
|
90
|
+
- lib/specjour.rb
|
91
|
+
- lib/specjour/core_ext/array.rb
|
92
|
+
- lib/specjour/db_scrub.rb
|
93
|
+
- lib/specjour/dispatcher.rb
|
94
|
+
- lib/specjour/distributed_formatter.rb
|
95
|
+
- lib/specjour/final_report.rb
|
96
|
+
- lib/specjour/manager.rb
|
97
|
+
- lib/specjour/marshalable_failure_formatter.rb
|
98
|
+
- lib/specjour/marshalable_rspec_failure.rb
|
99
|
+
- lib/specjour/printer.rb
|
100
|
+
- lib/specjour/protocol.rb
|
101
|
+
- lib/specjour/rsync_daemon.rb
|
102
|
+
- lib/specjour/tasks/dispatch.rake
|
103
|
+
- lib/specjour/tasks/specjour.rb
|
104
|
+
- lib/specjour/worker.rb
|
105
|
+
- rails/init.rb
|
106
|
+
- spec/lib/specjour/core_ext/array_spec.rb
|
107
|
+
- spec/lib/specjour/worker_spec.rb
|
108
|
+
- spec/spec.opts
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/specjour_spec.rb
|
111
|
+
has_rdoc: true
|
112
|
+
homepage: http://github.com/sandro/specjour
|
113
|
+
licenses: []
|
114
|
+
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options:
|
117
|
+
- --charset=UTF-8
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
segments:
|
132
|
+
- 0
|
133
|
+
version: "0"
|
134
|
+
requirements: []
|
135
|
+
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 1.3.6
|
138
|
+
signing_key:
|
139
|
+
specification_version: 3
|
140
|
+
summary: Distribute your spec suite amongst your LAN via Bonjour.
|
141
|
+
test_files:
|
142
|
+
- spec/lib/specjour/core_ext/array_spec.rb
|
143
|
+
- spec/lib/specjour/worker_spec.rb
|
144
|
+
- spec/spec_helper.rb
|
145
|
+
- spec/specjour_spec.rb
|