JonathanTron-specjour 0.2.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.dev +3 -0
- data/.document +5 -0
- data/.gitignore +23 -0
- data/History.markdown +55 -0
- data/JonathanTron-specjour.gemspec +102 -0
- data/MIT_LICENSE +20 -0
- data/README.markdown +110 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/specjour +51 -0
- data/lib/specjour.rb +43 -0
- data/lib/specjour/connection.rb +85 -0
- data/lib/specjour/cpu.rb +19 -0
- data/lib/specjour/cucumber.rb +14 -0
- data/lib/specjour/cucumber/dispatcher.rb +18 -0
- data/lib/specjour/cucumber/distributed_formatter.rb +84 -0
- data/lib/specjour/cucumber/final_report.rb +79 -0
- data/lib/specjour/cucumber/printer.rb +9 -0
- data/lib/specjour/db_scrub.rb +47 -0
- data/lib/specjour/dispatcher.rb +119 -0
- data/lib/specjour/manager.rb +101 -0
- data/lib/specjour/printer.rb +102 -0
- data/lib/specjour/protocol.rb +14 -0
- data/lib/specjour/rspec.rb +9 -0
- data/lib/specjour/rspec/distributed_formatter.rb +83 -0
- data/lib/specjour/rspec/final_report.rb +65 -0
- data/lib/specjour/rspec/marshalable_rspec_failure.rb +35 -0
- data/lib/specjour/rsync_daemon.rb +108 -0
- data/lib/specjour/socket_helpers.rb +11 -0
- data/lib/specjour/tasks/dispatch.rake +21 -0
- data/lib/specjour/tasks/specjour.rb +1 -0
- data/lib/specjour/worker.rb +86 -0
- data/rails/init.rb +6 -0
- data/spec/cpu_spec.rb +28 -0
- data/spec/lib/specjour/worker_spec.rb +14 -0
- data/spec/manager_spec.rb +20 -0
- data/spec/rsync_daemon_spec.rb +88 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/specjour_spec.rb +5 -0
- data/specjour.gemspec +101 -0
- metadata +175 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Connection
|
3
|
+
include Protocol
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :uri
|
7
|
+
attr_writer :socket
|
8
|
+
|
9
|
+
def_delegators :socket, :flush, :closed?, :gets, :each
|
10
|
+
|
11
|
+
def self.wrap(established_connection)
|
12
|
+
host, port = established_connection.peeraddr.values_at(3,1)
|
13
|
+
connection = new URI::Generic.build(:host => host, :port => port)
|
14
|
+
connection.socket = established_connection
|
15
|
+
connection
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(uri)
|
19
|
+
@uri = uri
|
20
|
+
end
|
21
|
+
|
22
|
+
alias to_str to_s
|
23
|
+
|
24
|
+
def connect
|
25
|
+
timeout { connect_socket }
|
26
|
+
end
|
27
|
+
|
28
|
+
def disconnect
|
29
|
+
socket.close
|
30
|
+
end
|
31
|
+
|
32
|
+
def socket
|
33
|
+
@socket ||= connect
|
34
|
+
end
|
35
|
+
|
36
|
+
def timeout(&block)
|
37
|
+
Timeout.timeout(2, &block)
|
38
|
+
rescue Timeout::Error
|
39
|
+
raise Error, "Connection to dispatcher timed out"
|
40
|
+
end
|
41
|
+
|
42
|
+
def next_test
|
43
|
+
will_reconnect do
|
44
|
+
send_message(:ready)
|
45
|
+
load_object socket.gets(TERMINATOR)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def print(arg)
|
50
|
+
will_reconnect do
|
51
|
+
socket.print dump_object(arg)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def puts(arg)
|
56
|
+
print(arg << "\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def send_message(method_name, *args)
|
60
|
+
print([method_name, *args])
|
61
|
+
flush
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def connect_socket
|
67
|
+
@socket = TCPSocket.open(uri.host, uri.port)
|
68
|
+
rescue Errno::ECONNREFUSED => error
|
69
|
+
Specjour.logger.debug "Could not connect to #{uri.to_s}\n#{error.inspect}"
|
70
|
+
retry
|
71
|
+
end
|
72
|
+
|
73
|
+
def reconnect
|
74
|
+
socket.close unless socket.closed?
|
75
|
+
connect
|
76
|
+
end
|
77
|
+
|
78
|
+
def will_reconnect(&block)
|
79
|
+
block.call
|
80
|
+
rescue SystemCallError => error
|
81
|
+
reconnect
|
82
|
+
retry
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/specjour/cpu.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Specjour
|
2
|
+
module CPU
|
3
|
+
def self.cores
|
4
|
+
case RUBY_PLATFORM
|
5
|
+
when /darwin/
|
6
|
+
command('hostinfo') =~ /^(\d+).+logically/
|
7
|
+
$1.to_i
|
8
|
+
when /linux/
|
9
|
+
command('grep --count processor /proc/cpuinfo').to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def self.command(cmd)
|
16
|
+
%x(#{cmd})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Cucumber
|
3
|
+
begin
|
4
|
+
require 'cucumber'
|
5
|
+
require 'cucumber/formatter/progress'
|
6
|
+
|
7
|
+
require 'specjour/cucumber/dispatcher'
|
8
|
+
require 'specjour/cucumber/distributed_formatter'
|
9
|
+
require 'specjour/cucumber/final_report'
|
10
|
+
require 'specjour/cucumber/printer'
|
11
|
+
rescue LoadError
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Cucumber
|
3
|
+
class Dispatcher < ::Specjour::Dispatcher
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def all_specs
|
8
|
+
@all_specs ||= Dir.chdir(project_path) do
|
9
|
+
Dir["features/**/*.feature"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def printer
|
14
|
+
@printer ||= Printer.start(all_specs)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Specjour::Cucumber
|
2
|
+
class DistributedFormatter < ::Cucumber::Formatter::Progress
|
3
|
+
class << self
|
4
|
+
attr_accessor :batch_size
|
5
|
+
end
|
6
|
+
@batch_size = 1
|
7
|
+
|
8
|
+
def initialize(step_mother, io, options)
|
9
|
+
@step_mother = step_mother
|
10
|
+
@io = io
|
11
|
+
@options = options
|
12
|
+
@failing_scenarios = []
|
13
|
+
@step_summary = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def after_features(features)
|
17
|
+
print_summary
|
18
|
+
step_mother.scenarios.clear
|
19
|
+
step_mother.steps.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
def prepare_failures
|
23
|
+
@failures = step_mother.scenarios(:failed).select { |s| s.is_a?(Cucumber::Ast::Scenario) }
|
24
|
+
|
25
|
+
if !@failures.empty?
|
26
|
+
@failures.each do |failure|
|
27
|
+
failure_message = ''
|
28
|
+
failure_message += format_string("cucumber " + failure.file_colon_line, :failed) +
|
29
|
+
failure_message += format_string(" # Scenario: " + failure.name, :comment)
|
30
|
+
@failing_scenarios << failure_message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def prepare_elements(elements, status, kind)
|
36
|
+
output = ''
|
37
|
+
if elements.any?
|
38
|
+
output += format_string("\n(::) #{status} #{kind} (::)\n", status)
|
39
|
+
output += "\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
elements.each_with_index do |element, i|
|
43
|
+
if status == :failed
|
44
|
+
output += print_exception(element.exception, status, 0)
|
45
|
+
else
|
46
|
+
output += format_string(element.backtrace_line, status)
|
47
|
+
output += "\n"
|
48
|
+
end
|
49
|
+
@step_summary << output unless output.blank?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def prepare_steps(type)
|
54
|
+
prepare_elements(step_mother.scenarios(type), type, 'steps')
|
55
|
+
end
|
56
|
+
|
57
|
+
def print_exception(e, status, indent)
|
58
|
+
format_string("#{e.message} (#{e.class})\n#{e.backtrace.join("\n")}".indent(indent), status)
|
59
|
+
end
|
60
|
+
|
61
|
+
def print_summary
|
62
|
+
prepare_failures
|
63
|
+
prepare_steps(:failed)
|
64
|
+
prepare_steps(:undefined)
|
65
|
+
|
66
|
+
@io.send_message(:worker_summary=, to_hash)
|
67
|
+
end
|
68
|
+
|
69
|
+
OUTCOMES = [:failed, :skipped, :undefined, :pending, :passed]
|
70
|
+
|
71
|
+
def to_hash
|
72
|
+
hash = {}
|
73
|
+
[:scenarios, :steps].each do |type|
|
74
|
+
hash[type] = {}
|
75
|
+
OUTCOMES.each do |outcome|
|
76
|
+
hash[type][outcome] = step_mother.send(type, outcome).size
|
77
|
+
end
|
78
|
+
end
|
79
|
+
hash.merge!(:failing_scenarios => @failing_scenarios, :step_summary => @step_summary)
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Cucumber
|
3
|
+
class Summarizer
|
4
|
+
attr_reader :duration, :failing_scenarios, :step_summary
|
5
|
+
def initialize
|
6
|
+
@duration = 0.0
|
7
|
+
@failing_scenarios = []
|
8
|
+
@step_summary = []
|
9
|
+
@scenarios = Hash.new(0)
|
10
|
+
@steps = Hash.new(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
def increment(category, type, count)
|
14
|
+
current = instance_variable_get("@#{category}")
|
15
|
+
current[type] += count
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(stats)
|
19
|
+
stats.each do |category, hash|
|
20
|
+
if category == :failing_scenarios
|
21
|
+
@failing_scenarios += hash
|
22
|
+
elsif category == :step_summary
|
23
|
+
@step_summary += hash
|
24
|
+
elsif category == :duration
|
25
|
+
@duration = hash.to_f if duration < hash.to_f
|
26
|
+
else
|
27
|
+
hash.each do |type, count|
|
28
|
+
increment(category, type, count)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def scenarios(status=nil)
|
35
|
+
length = status ? @scenarios[status] : @scenarios.inject(0) {|h,(k,v)| h += v}
|
36
|
+
any = @scenarios[status] > 0 if status
|
37
|
+
OpenStruct.new(:length => length , :any? => any)
|
38
|
+
end
|
39
|
+
|
40
|
+
def steps(status=nil)
|
41
|
+
length = status ? @steps[status] : @steps.inject(0) {|h,(k,v)| h += v}
|
42
|
+
any = @steps[status] > 0 if status
|
43
|
+
OpenStruct.new(:length => length , :any? => any)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class FinalReport
|
48
|
+
include ::Cucumber::Formatter::Console
|
49
|
+
def initialize
|
50
|
+
@features = []
|
51
|
+
@summarizer = Summarizer.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def add(stats)
|
55
|
+
@summarizer.add(stats)
|
56
|
+
end
|
57
|
+
|
58
|
+
def exit_status
|
59
|
+
@summarizer.failing_scenarios.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
def summarize
|
63
|
+
if @summarizer.failing_scenarios.any?
|
64
|
+
puts "\n\n"
|
65
|
+
@summarizer.step_summary.each {|f| puts f }
|
66
|
+
puts "\n\n"
|
67
|
+
puts format_string("Failing Scenarios:", :failed)
|
68
|
+
@summarizer.failing_scenarios.each {|f| puts f }
|
69
|
+
end
|
70
|
+
|
71
|
+
default_format = lambda {|status_count, status| format_string(status_count, status)}
|
72
|
+
puts
|
73
|
+
puts scenario_summary(@summarizer, &default_format)
|
74
|
+
puts step_summary(@summarizer, &default_format)
|
75
|
+
puts format_duration(@summarizer.duration) if @summarizer.duration
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Specjour
|
2
|
+
module DbScrub
|
3
|
+
require 'rake'
|
4
|
+
load 'tasks/misc.rake'
|
5
|
+
load 'tasks/databases.rake'
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def scrub
|
10
|
+
connect_to_database
|
11
|
+
if pending_migrations?
|
12
|
+
puts "Migrating schema for database #{ENV['TEST_ENV_NUMBER']}..."
|
13
|
+
Rake::Task['db:test:load'].invoke
|
14
|
+
else
|
15
|
+
purge_tables
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def connect_to_database
|
22
|
+
connection
|
23
|
+
rescue # assume the database doesn't exist
|
24
|
+
Rake::Task['db:create'].invoke
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection
|
28
|
+
ActiveRecord::Base.connection
|
29
|
+
end
|
30
|
+
|
31
|
+
def purge_tables
|
32
|
+
connection.disable_referential_integrity do
|
33
|
+
tables_to_purge.each do |table|
|
34
|
+
connection.delete "delete from #{table}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def pending_migrations?
|
40
|
+
ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
def tables_to_purge
|
44
|
+
connection.tables - ['schema_migrations']
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Dispatcher
|
3
|
+
require 'dnssd'
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
include SocketHelpers
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :interrupted
|
9
|
+
alias interrupted? interrupted
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :project_path, :managers, :manager_threads, :hosts
|
13
|
+
attr_accessor :worker_size
|
14
|
+
|
15
|
+
def initialize(project_path)
|
16
|
+
@project_path = project_path
|
17
|
+
@managers = []
|
18
|
+
@worker_size = 0
|
19
|
+
reset_manager_threads
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
rsync_daemon.start
|
24
|
+
gather_managers
|
25
|
+
dispatch_work
|
26
|
+
printer.join
|
27
|
+
exit printer.exit_status
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def all_specs
|
33
|
+
@all_specs ||= Dir.chdir(project_path) do
|
34
|
+
Dir["spec/**/**/*_spec.rb"].sort
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def command_managers(async = false, &block)
|
39
|
+
managers.each do |manager|
|
40
|
+
manager_threads << Thread.new(manager, &block)
|
41
|
+
end
|
42
|
+
wait_on_managers unless async
|
43
|
+
end
|
44
|
+
|
45
|
+
def dispatch_work
|
46
|
+
command_managers(true) { |m| m.dispatch }
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_manager(uri)
|
50
|
+
Timeout.timeout(8) do
|
51
|
+
manager = DRbObject.new_with_uri(uri.to_s)
|
52
|
+
if !managers.include?(manager) && manager.available_for?(project_name)
|
53
|
+
set_up_manager(manager, uri)
|
54
|
+
managers << manager
|
55
|
+
self.worker_size += manager.worker_size
|
56
|
+
end
|
57
|
+
end
|
58
|
+
rescue Timeout::Error
|
59
|
+
Specjour.logger.debug "Couldn't work with manager at #{uri}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def gather_managers
|
63
|
+
puts "Waiting for managers"
|
64
|
+
Signal.trap('INT') { self.class.interrupted = true; exit }
|
65
|
+
browser = DNSSD::Service.new
|
66
|
+
begin
|
67
|
+
Timeout.timeout(10) do
|
68
|
+
browser.browse '_druby._tcp' do |reply|
|
69
|
+
if reply.flags.add?
|
70
|
+
resolve_reply(reply)
|
71
|
+
end
|
72
|
+
browser.stop unless reply.flags.more_coming?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
rescue Timeout::Error
|
76
|
+
end
|
77
|
+
puts "Managers found: #{managers.size}"
|
78
|
+
abort unless managers.size > 0
|
79
|
+
puts "Workers found: #{worker_size}"
|
80
|
+
printer.worker_size = worker_size
|
81
|
+
end
|
82
|
+
|
83
|
+
def printer
|
84
|
+
@printer ||= Printer.start(all_specs)
|
85
|
+
end
|
86
|
+
|
87
|
+
def project_name
|
88
|
+
@project_name ||= (ENV["SPECJOUR_PROJECT_NAME"] || File.basename(project_path))
|
89
|
+
end
|
90
|
+
|
91
|
+
def reset_manager_threads
|
92
|
+
@manager_threads = []
|
93
|
+
end
|
94
|
+
|
95
|
+
def resolve_reply(reply)
|
96
|
+
DNSSD.resolve!(reply) do |resolved|
|
97
|
+
resolved_ip = ip_from_hostname(resolved.target)
|
98
|
+
uri = URI::Generic.build :scheme => reply.service_name, :host => resolved_ip, :port => resolved.port
|
99
|
+
fetch_manager(uri)
|
100
|
+
resolved.service.stop if resolved.service.started?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def rsync_daemon
|
105
|
+
@rsync_daemon ||= RsyncDaemon.new(project_path, project_name)
|
106
|
+
end
|
107
|
+
|
108
|
+
def set_up_manager(manager, uri)
|
109
|
+
manager.project_name = project_name
|
110
|
+
manager.dispatcher_uri = URI::Generic.build :scheme => "specjour", :host => hostname, :port => printer.port
|
111
|
+
at_exit { manager.kill_worker_processes }
|
112
|
+
end
|
113
|
+
|
114
|
+
def wait_on_managers
|
115
|
+
manager_threads.each {|t| t.join; t.exit}
|
116
|
+
reset_manager_threads
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|