paraspec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Paraspec
2
+ # An RSpec test runner - in a master process.
3
+ # This runner queues the tests to run instead of running them.
4
+ # Worker processes then run the tests.
5
+ class MasterRunner
6
+ end
7
+ end
@@ -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,3 @@
1
+ module Paraspec
2
+ VERSION = '0.0.1'
3
+ 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