JonathanTron-specjour 0.2.5.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/.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
|