paraspec 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +23 -0
- data/README.md +97 -0
- data/bin/paraspec +53 -0
- data/lib/paraspec.rb +21 -0
- data/lib/paraspec/drb_helpers.rb +65 -0
- data/lib/paraspec/http_client.rb +43 -0
- data/lib/paraspec/http_server.rb +24 -0
- data/lib/paraspec/ipc.rb +11 -0
- data/lib/paraspec/logger.rb +44 -0
- data/lib/paraspec/master.rb +219 -0
- data/lib/paraspec/master_runner.rb +7 -0
- data/lib/paraspec/msgpack_client.rb +53 -0
- data/lib/paraspec/msgpack_helpers.rb +46 -0
- data/lib/paraspec/msgpack_server.rb +58 -0
- data/lib/paraspec/process_helpers.rb +22 -0
- data/lib/paraspec/rspec_facade.rb +38 -0
- data/lib/paraspec/rspec_patches.rb +23 -0
- data/lib/paraspec/supervisor.rb +127 -0
- data/lib/paraspec/version.rb +3 -0
- data/lib/paraspec/worker.rb +70 -0
- data/lib/paraspec/worker_formatter.rb +82 -0
- data/lib/paraspec/worker_runner.rb +67 -0
- metadata +123 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Paraspec
|
5
|
+
class MsgpackClient
|
6
|
+
include MsgpackHelpers
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
@terminal = options[:terminal]
|
10
|
+
|
11
|
+
connect
|
12
|
+
end
|
13
|
+
|
14
|
+
def request(action, payload=nil)
|
15
|
+
req = {action: action, payload: payload, id: request_id}
|
16
|
+
Paraspec.logger.debug_ipc("CliReq:#{req[:id]} #{req}")
|
17
|
+
pk = packer(@socket)
|
18
|
+
pk.write(req)
|
19
|
+
pk.flush
|
20
|
+
response = unpacker(@socket).unpack
|
21
|
+
Paraspec.logger.debug_ipc("CliRes:#{req[:id]} #{response}")
|
22
|
+
response = IpcHash.new.merge(response)
|
23
|
+
response[:result]
|
24
|
+
end
|
25
|
+
|
26
|
+
def request_id
|
27
|
+
@request_num ||= 0
|
28
|
+
"#{$$}:#{@request_num += 1}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# The socket doesn't stay operational after a fork even if the child
|
32
|
+
# process never uses it. Parent should reconnect after forking
|
33
|
+
# any children
|
34
|
+
def reconnect!
|
35
|
+
@socket.close
|
36
|
+
connect
|
37
|
+
end
|
38
|
+
|
39
|
+
private def connect
|
40
|
+
start_time = Time.now
|
41
|
+
begin
|
42
|
+
@socket = TCPSocket.new('127.0.0.1', MASTER_APP_PORT)
|
43
|
+
rescue Errno::ECONNREFUSED
|
44
|
+
if !@terminal && Time.now - start_time > DrbHelpers::WAIT_TIME
|
45
|
+
raise
|
46
|
+
else
|
47
|
+
sleep 0.1
|
48
|
+
retry
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Paraspec
|
4
|
+
module MsgpackHelpers
|
5
|
+
|
6
|
+
def packer(io)
|
7
|
+
pk = MessagePack::Packer.new(io)
|
8
|
+
pk.register_type(1, Time) do |time|
|
9
|
+
time.to_s
|
10
|
+
end
|
11
|
+
pk.register_type(2, RSpec::Core::Example::ExecutionResult) do |er|
|
12
|
+
serialized = {}
|
13
|
+
%w(started_at finished_at run_time status).each do |field|
|
14
|
+
serialized[field] = er.send(field)
|
15
|
+
end
|
16
|
+
%w(exception pending_exception).each do |field|
|
17
|
+
serialized[field] = Marshal.dump(er.send(field))
|
18
|
+
end
|
19
|
+
Marshal.dump(serialized)
|
20
|
+
end
|
21
|
+
pk
|
22
|
+
end
|
23
|
+
|
24
|
+
def unpacker(io)
|
25
|
+
uk = MessagePack::Unpacker.new(io)
|
26
|
+
uk.register_type(1) do |serialized|
|
27
|
+
Time.parse(serialized)
|
28
|
+
end
|
29
|
+
uk.register_type(2) do |serialized|
|
30
|
+
serialized = Marshal.load(serialized)
|
31
|
+
er = RSpec::Core::Example::ExecutionResult.new
|
32
|
+
serialized.each do |k, v|
|
33
|
+
if k == 'status'
|
34
|
+
v = v.to_sym
|
35
|
+
end
|
36
|
+
if %w(exception pending_exception).include?(k)
|
37
|
+
v = Marshal.load(v)
|
38
|
+
end
|
39
|
+
er.send("#{k}=", v)
|
40
|
+
end
|
41
|
+
er
|
42
|
+
end
|
43
|
+
uk
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'msgpack'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Paraspec
|
6
|
+
class MsgpackServer
|
7
|
+
include MsgpackHelpers
|
8
|
+
|
9
|
+
def initialize(master)
|
10
|
+
@master = master
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
@socket = ::TCPServer.new('127.0.0.1', MASTER_APP_PORT)
|
15
|
+
begin
|
16
|
+
while true
|
17
|
+
s = @socket.accept_nonblock
|
18
|
+
run_processing_thread(s)
|
19
|
+
end
|
20
|
+
rescue Errno::EAGAIN
|
21
|
+
unless @master.stop?
|
22
|
+
sleep 0.2
|
23
|
+
retry
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_processing_thread(s)
|
29
|
+
Thread.new do
|
30
|
+
u = unpacker(s)
|
31
|
+
u.each do |obj|
|
32
|
+
result = nil
|
33
|
+
time = Benchmark.realtime do
|
34
|
+
action = obj['action'].gsub('-', '_')
|
35
|
+
payload = obj['payload']
|
36
|
+
if payload
|
37
|
+
payload = IpcHash.new.merge(payload)
|
38
|
+
args = [payload]
|
39
|
+
else
|
40
|
+
args = []
|
41
|
+
end
|
42
|
+
|
43
|
+
Paraspec.logger.debug_ipc("SrvReq:#{obj['id']} #{obj}")
|
44
|
+
result = @master.send(action, *args)
|
45
|
+
|
46
|
+
pk = packer(s)
|
47
|
+
resp = {result: result}
|
48
|
+
Paraspec.logger.debug_ipc("SrvRes:#{obj['id']} #{resp}")
|
49
|
+
pk.write(resp)
|
50
|
+
pk.flush
|
51
|
+
s.flush
|
52
|
+
end
|
53
|
+
Paraspec.logger.debug_perf("SrvReq:#{obj['id']} #{obj['action']}: #{result} #{'%.3f msec' % (time*1000)}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Paraspec
|
2
|
+
module ProcessHelpers
|
3
|
+
def kill_child_processes
|
4
|
+
# Only kill if we are in supervisor
|
5
|
+
return unless Process.pid == Process.getpgrp
|
6
|
+
|
7
|
+
child_pids = `pgrep -g #{$$}`
|
8
|
+
if $?.exitstatus != 0
|
9
|
+
warn "Failed to run pgrep (#{$?.exitstatus})"
|
10
|
+
end
|
11
|
+
child_pids = child_pids.strip.split(/\n/).map { |pid| pid.to_i }
|
12
|
+
child_pids.delete_if do |pid|
|
13
|
+
pid == Process.pid
|
14
|
+
end
|
15
|
+
child_pids.each do |pid|
|
16
|
+
begin
|
17
|
+
Process.kill('TERM', pid)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module Paraspec
|
5
|
+
class RSpecFacade
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
class << self
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :instance, :all_example_groups, :all_examples
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_example_groups
|
14
|
+
@all_example_groups ||= begin
|
15
|
+
groups = [] + RSpec.world.example_groups
|
16
|
+
all_groups = []
|
17
|
+
until groups.empty?
|
18
|
+
new_groups = []
|
19
|
+
groups.each do |group|
|
20
|
+
all_groups << group
|
21
|
+
new_groups += group.children
|
22
|
+
end
|
23
|
+
groups = new_groups
|
24
|
+
end
|
25
|
+
all_groups
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_examples
|
30
|
+
@all_examples ||= begin
|
31
|
+
filter_manager = RSpec.configuration.filter_manager
|
32
|
+
all_example_groups.map do |group|
|
33
|
+
filter_manager.prune(group.examples)
|
34
|
+
end.flatten
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
|
3
|
+
class RSpec::Core::Configuration
|
4
|
+
# https://github.com/rspec/rspec-core/commit/e7bb36342b8a3aca2512a0335ea9836780a60605
|
5
|
+
# Will probably add a flag to configuration to load default_path
|
6
|
+
# regardless of $0
|
7
|
+
def command
|
8
|
+
'rspec'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class RSpec::Core::World
|
13
|
+
# https://github.com/rspec/rspec-core/pull/2552
|
14
|
+
def filter_examples
|
15
|
+
@filtered_examples = Hash.new do |hash, group|
|
16
|
+
hash[group] = filter_manager.prune(group.examples)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class RSpec::Core::Reporter
|
22
|
+
attr_reader :non_example_exception_count
|
23
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Paraspec
|
2
|
+
# Supervisor is the process that spawns all other processes.
|
3
|
+
# Its primary responsibility is to be a "clean slate", specifically
|
4
|
+
# the supervisor should not ever have any of the tests loaded in its
|
5
|
+
# address space.
|
6
|
+
class Supervisor
|
7
|
+
include DrbHelpers
|
8
|
+
include ProcessHelpers
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
@original_process_title = $0
|
12
|
+
$0 = "#{@original_process_title} [supervisor]"
|
13
|
+
Paraspec.logger.ident = '[s]'
|
14
|
+
@concurrency = options[:concurrency] || 1
|
15
|
+
@terminal = options[:terminal]
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
unless @terminal
|
21
|
+
Process.setpgrp
|
22
|
+
end
|
23
|
+
|
24
|
+
supervisor_pid = $$
|
25
|
+
at_exit do
|
26
|
+
# We fork, therefore this handler will be run in master and
|
27
|
+
# workers as well but it should only run in supervisor.
|
28
|
+
# Guard accordingly
|
29
|
+
if $$ == supervisor_pid
|
30
|
+
# first kill workers, then master
|
31
|
+
((@worker_pids || []) + [@master_pid]).compact.each do |pid|
|
32
|
+
begin
|
33
|
+
Process.kill('TERM', pid)
|
34
|
+
rescue SystemCallError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# then kill our process group
|
38
|
+
unless @terminal
|
39
|
+
kill_child_processes
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
rd, wr = IO.pipe
|
45
|
+
if @master_pid = fork
|
46
|
+
# parent
|
47
|
+
wr.close
|
48
|
+
@master_pipe = rd
|
49
|
+
run_supervisor
|
50
|
+
else
|
51
|
+
# child - master
|
52
|
+
$0 = "#{@original_process_title} [master]"
|
53
|
+
if @options[:master_is_1]
|
54
|
+
ENV['TEST_ENV_NUMBER'] = '1'
|
55
|
+
end
|
56
|
+
Paraspec.logger.ident = '[m]'
|
57
|
+
rd.close
|
58
|
+
master = Master.new(:supervisor_pipe => wr)
|
59
|
+
master.run
|
60
|
+
exit(0)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def run_supervisor
|
65
|
+
start_time = Time.now
|
66
|
+
|
67
|
+
if master_client.request('non-example-exception-count').to_i == 0
|
68
|
+
master_client.request('suite-started')
|
69
|
+
|
70
|
+
@worker_pipes = []
|
71
|
+
@worker_pids = []
|
72
|
+
|
73
|
+
1.upto(@concurrency) do |i|
|
74
|
+
rd, wr = IO.pipe
|
75
|
+
if worker_pid = fork
|
76
|
+
# parent
|
77
|
+
wr.close
|
78
|
+
@worker_pipes << rd
|
79
|
+
@worker_pids << worker_pid
|
80
|
+
else
|
81
|
+
# child - worker
|
82
|
+
$0 = "#{@original_process_title} [worker-#{i}]"
|
83
|
+
Paraspec.logger.ident = "[w#{i}]"
|
84
|
+
rd.close
|
85
|
+
if RSpec.world.example_groups.count > 0
|
86
|
+
raise 'Example groups loaded too early/spilled across processes'
|
87
|
+
end
|
88
|
+
Worker.new(:number => i, :supervisor_pipe => wr).run
|
89
|
+
exit(0)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
Paraspec.logger.debug_state("Waiting for workers")
|
94
|
+
@worker_pids.each_with_index do |pid, i|
|
95
|
+
Paraspec.logger.debug_state("Waiting for worker #{i+1} at #{pid}")
|
96
|
+
wait_for_process(pid)
|
97
|
+
end
|
98
|
+
status = 0
|
99
|
+
else
|
100
|
+
status = 1
|
101
|
+
end
|
102
|
+
|
103
|
+
master_client.reconnect!
|
104
|
+
puts "dumping summary"
|
105
|
+
master_client.request('dump-summary')
|
106
|
+
if status == 0
|
107
|
+
status = master_client.request('status')
|
108
|
+
end
|
109
|
+
Paraspec.logger.debug_state("Asking master to stop")
|
110
|
+
master_client.request('stop')
|
111
|
+
wait_for_process(@master_pid)
|
112
|
+
exit status
|
113
|
+
end
|
114
|
+
|
115
|
+
def wait_for_process(pid)
|
116
|
+
begin
|
117
|
+
Process.wait(pid)
|
118
|
+
rescue Errno::ECHILD
|
119
|
+
# already dead
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def ident
|
124
|
+
"[s]"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Paraspec
|
2
|
+
# A worker process obtains a test to run from the master, runs the
|
3
|
+
# test and reports the results, as well as any output, back to the master,
|
4
|
+
# then obtains the next test to run and so on.
|
5
|
+
# There can be one or more workers participating in a test run.
|
6
|
+
# A worker generally loads all of the tests but runs a subset of them.
|
7
|
+
class Worker
|
8
|
+
include DrbHelpers
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
@number = options[:number]
|
12
|
+
ENV['TEST_ENV_NUMBER'] = @number.to_s
|
13
|
+
@supervisor_pipe = options[:supervisor_pipe]
|
14
|
+
if RSpec.world.example_groups.count > 0
|
15
|
+
raise 'Example groups loaded too early/spilled across processes'
|
16
|
+
end
|
17
|
+
@terminal = options[:terminal]
|
18
|
+
|
19
|
+
#RSpec.configuration.load_spec_files
|
20
|
+
# possibly need to signal to supervisor when we are ready to
|
21
|
+
# start running tests - there is a race otherwise I think
|
22
|
+
#puts "#{RSpecFacade.all_example_groups.count} example groups known"
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
#puts "worker: #{Process.pid} #{Process.getpgrp}"
|
27
|
+
#@master = drb_connect(MASTER_DRB_URI, timeout: !@terminal)
|
28
|
+
|
29
|
+
runner = WorkerRunner.new(master_client: master_client)
|
30
|
+
|
31
|
+
# fill cache when pruning is not set up
|
32
|
+
RSpecFacade.all_example_groups
|
33
|
+
RSpecFacade.all_examples
|
34
|
+
|
35
|
+
master_example_count = master_client.request('example-count')
|
36
|
+
if master_example_count != RSpecFacade.all_examples.count
|
37
|
+
# Workers and master should have the same examples defined.
|
38
|
+
# If a test suite conditionally defines examples, it needs to
|
39
|
+
# ensure that master and worker use the same settings.
|
40
|
+
# If worker and master sets of examples differ, when the worker
|
41
|
+
# requests an example from master it may receive an example
|
42
|
+
# that it can't run.
|
43
|
+
# We just check the count for now but may take a digest of
|
44
|
+
# defined examples in the future.
|
45
|
+
# A mismatch here usually indicates an issue with the test suite
|
46
|
+
# being run.
|
47
|
+
puts "Worker #{@number} has #{RSpecFacade.all_examples.count} examples, master has #{master_example_count}"
|
48
|
+
#byebug
|
49
|
+
raise "Worker #{@number} has #{RSpecFacade.all_examples.count} examples, master has #{master_example_count}"
|
50
|
+
end
|
51
|
+
|
52
|
+
while true
|
53
|
+
Paraspec.logger.debug_state("Requesting a spec")
|
54
|
+
spec = master_client.request('get-spec')
|
55
|
+
Paraspec.logger.debug_state("Got spec #{spec || 'nil'}")
|
56
|
+
# HTTP transport returns no spec as an empty hash,
|
57
|
+
# msgpack transport returns as nil
|
58
|
+
break if spec.nil? || spec.empty?
|
59
|
+
spec = IpcHash.new.merge(spec)
|
60
|
+
Paraspec.logger.debug_state("Running spec #{spec}")
|
61
|
+
runner.run(spec)
|
62
|
+
Paraspec.logger.debug_state("Finished running spec #{spec}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def ident
|
67
|
+
"[w#{@number}]"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|