paraspec 0.0.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.
- 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
|