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.
@@ -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