cukeforker 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.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.mdown +26 -0
- data/Rakefile +5 -0
- data/bin/cukeforker +9 -0
- data/cukeforker.gemspec +25 -0
- data/lib/cukeforker.rb +20 -0
- data/lib/cukeforker/abstract_listener.rb +39 -0
- data/lib/cukeforker/logging_listener.rb +65 -0
- data/lib/cukeforker/runner.rb +104 -0
- data/lib/cukeforker/version.rb +3 -0
- data/lib/cukeforker/vnc_listener.rb +21 -0
- data/lib/cukeforker/vnc_server.rb +62 -0
- data/lib/cukeforker/vnc_server_pool.rb +68 -0
- data/lib/cukeforker/worker.rb +87 -0
- data/lib/cukeforker/worker_queue.rb +113 -0
- data/spec/cukeforker/logging_listener_spec.rb +45 -0
- data/spec/cukeforker/runner_spec.rb +84 -0
- data/spec/cukeforker/vnc_listener_spec.rb +32 -0
- data/spec/cukeforker/vnc_server_pool_spec.rb +62 -0
- data/spec/cukeforker/vnc_server_spec.rb +48 -0
- data/spec/cukeforker/worker_queue_spec.rb +117 -0
- data/spec/cukeforker/worker_spec.rb +102 -0
- data/spec/spec_helper.rb +28 -0
- metadata +135 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
module CukeForker
|
2
|
+
class Worker
|
3
|
+
class << self
|
4
|
+
attr_writer :id
|
5
|
+
def id; @id ||= -1; end
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :status, :feature, :pid, :format, :out, :id
|
9
|
+
attr_accessor :vnc
|
10
|
+
|
11
|
+
def initialize(feature, format, out, extra_args = [])
|
12
|
+
@feature = feature
|
13
|
+
@format = format
|
14
|
+
@extra_args = extra_args
|
15
|
+
@out = out
|
16
|
+
@status, @vnc = nil
|
17
|
+
|
18
|
+
@id = self.class.id += 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def finished?
|
22
|
+
wait_pid, @status = Process.waitpid2(pid, Process::WNOHANG)
|
23
|
+
!!wait_pid
|
24
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def failed?
|
29
|
+
status.nil? || status.exitstatus != 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def start
|
33
|
+
@pid = Process.fork { execute_cucumber }
|
34
|
+
end
|
35
|
+
|
36
|
+
def args
|
37
|
+
args = %W[--format #{format} --out #{output}]
|
38
|
+
args += @extra_args
|
39
|
+
args << feature
|
40
|
+
|
41
|
+
args
|
42
|
+
end
|
43
|
+
|
44
|
+
def text
|
45
|
+
"[
|
46
|
+
#{pid}
|
47
|
+
#{feature}
|
48
|
+
#{status.inspect}
|
49
|
+
#{out}
|
50
|
+
#{vnc && vnc.display}
|
51
|
+
]"
|
52
|
+
end
|
53
|
+
|
54
|
+
def output
|
55
|
+
File.join out, "#{basename}.#{format}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def stdout
|
59
|
+
File.join out, "#{basename}.stdout"
|
60
|
+
end
|
61
|
+
|
62
|
+
def stderr
|
63
|
+
File.join out, "#{basename}.stderr"
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def execute_cucumber
|
69
|
+
FileUtils.mkdir_p(out) unless File.exist? out
|
70
|
+
|
71
|
+
$stdout.reopen stdout
|
72
|
+
$stderr.reopen stderr
|
73
|
+
|
74
|
+
if @vnc
|
75
|
+
ENV['DISPLAY'] = @vnc.display
|
76
|
+
end
|
77
|
+
|
78
|
+
failed = Cucumber::Cli::Main.execute args
|
79
|
+
exit failed ? 1 : 0
|
80
|
+
end
|
81
|
+
|
82
|
+
def basename
|
83
|
+
@basename ||= feature.gsub(/\W/, '_')
|
84
|
+
end
|
85
|
+
|
86
|
+
end # Worker
|
87
|
+
end # CukeForker
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module CukeForker
|
2
|
+
class WorkerQueue
|
3
|
+
include Observable
|
4
|
+
|
5
|
+
def initialize(max)
|
6
|
+
@max = max
|
7
|
+
|
8
|
+
@pending = []
|
9
|
+
@running = []
|
10
|
+
@finished = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def backed_up?
|
14
|
+
@pending.any?
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(worker)
|
18
|
+
@pending << worker
|
19
|
+
end
|
20
|
+
|
21
|
+
def process(poll_interval = nil)
|
22
|
+
@start_time = Time.now
|
23
|
+
|
24
|
+
while backed_up?
|
25
|
+
fill
|
26
|
+
eta
|
27
|
+
poll poll_interval while full?
|
28
|
+
end
|
29
|
+
|
30
|
+
# yay, no more pending workers
|
31
|
+
end
|
32
|
+
|
33
|
+
def wait_until_finished(poll_interval = nil)
|
34
|
+
until empty?
|
35
|
+
poll poll_interval
|
36
|
+
eta
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def fill
|
41
|
+
while backed_up? and not full?
|
42
|
+
worker = @pending.shift
|
43
|
+
start worker
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def poll(seconds = nil)
|
48
|
+
finished = @running.select { |w| w.finished? }
|
49
|
+
|
50
|
+
if finished.empty?
|
51
|
+
sleep seconds if seconds
|
52
|
+
else
|
53
|
+
finished.each { |w| finish w }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def size
|
58
|
+
@running.size
|
59
|
+
end
|
60
|
+
|
61
|
+
def full?
|
62
|
+
size == @max
|
63
|
+
end
|
64
|
+
|
65
|
+
def empty?
|
66
|
+
@running.empty?
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_failures?
|
70
|
+
@finished.any? { |w| w.failed? }
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def start(worker)
|
76
|
+
fire :on_worker_starting, worker
|
77
|
+
|
78
|
+
worker.start
|
79
|
+
@running << worker
|
80
|
+
end
|
81
|
+
|
82
|
+
def finish(worker)
|
83
|
+
@running.delete worker
|
84
|
+
@finished << worker
|
85
|
+
|
86
|
+
fire :on_worker_finished, worker
|
87
|
+
end
|
88
|
+
|
89
|
+
def eta
|
90
|
+
return Time.now if @finished.empty?
|
91
|
+
|
92
|
+
pending = @pending.size
|
93
|
+
finished = @finished.size
|
94
|
+
|
95
|
+
seconds_per_child = (Time.now - start_time) / finished
|
96
|
+
eta = Time.now + (seconds_per_child * pending)
|
97
|
+
|
98
|
+
fire :on_eta, eta, pending + size, finished
|
99
|
+
end
|
100
|
+
|
101
|
+
def fire(*args)
|
102
|
+
changed
|
103
|
+
notify_observers(*args)
|
104
|
+
end
|
105
|
+
|
106
|
+
def start_time
|
107
|
+
@start_time or raise NotStartedError
|
108
|
+
end
|
109
|
+
|
110
|
+
class NotStartedError < StandardError; end
|
111
|
+
|
112
|
+
end # WorkerQueue
|
113
|
+
end # CukeForker
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module CukeForker
|
4
|
+
describe LoggingListener do
|
5
|
+
let(:stdout) { StringIO.new }
|
6
|
+
let(:listener) { LoggingListener.new stdout }
|
7
|
+
|
8
|
+
it "logs all events" do
|
9
|
+
Time.stub(:now => Time.now)
|
10
|
+
|
11
|
+
mock_worker = mock(Worker, :id => "1", :feature => "foo/bar")
|
12
|
+
mock_display = mock(VncServer)
|
13
|
+
mock_display.stub(:display).and_return(nil, ":5")
|
14
|
+
|
15
|
+
listener.on_run_starting
|
16
|
+
listener.on_display_starting mock_display
|
17
|
+
listener.on_display_fetched mock_display
|
18
|
+
listener.on_worker_starting mock_worker
|
19
|
+
listener.on_eta Time.now, 10, 255
|
20
|
+
listener.on_worker_finished mock_worker
|
21
|
+
listener.on_display_released mock_display
|
22
|
+
listener.on_run_interrupted
|
23
|
+
listener.on_run_finished false
|
24
|
+
listener.on_display_stopping mock_display
|
25
|
+
|
26
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S##{Process.pid}")
|
27
|
+
|
28
|
+
stdout.string.should == <<-OUTPUT
|
29
|
+
I, [#{timestamp}] INFO -- : [ run ] starting
|
30
|
+
I, [#{timestamp}] INFO -- : [ display( ) ] starting
|
31
|
+
I, [#{timestamp}] INFO -- : [ display(:5) ] fetched
|
32
|
+
I, [#{timestamp}] INFO -- : [ worker(1) ] starting: foo/bar
|
33
|
+
I, [#{timestamp}] INFO -- : [ eta(10/255) ] #{Time.now.strftime "%Y-%m-%d %H:%M:%S"}
|
34
|
+
I, [#{timestamp}] INFO -- : [ worker(1) ] finished: foo/bar
|
35
|
+
I, [#{timestamp}] INFO -- : [ display(:5) ] released
|
36
|
+
I, [#{timestamp}] INFO -- : [ run ] interrupted - please wait
|
37
|
+
I, [#{timestamp}] INFO -- : [ run ] finished, passed
|
38
|
+
I, [#{timestamp}] INFO -- : [ display(:5) ] stopping
|
39
|
+
OUTPUT
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
end # Worker
|
45
|
+
end # CukeForker
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module CukeForker
|
4
|
+
describe Runner do
|
5
|
+
|
6
|
+
context "creating" do
|
7
|
+
it "sets up a new instance" do
|
8
|
+
# sigh.
|
9
|
+
|
10
|
+
max = 4
|
11
|
+
format = :json
|
12
|
+
out = "/tmp"
|
13
|
+
listeners = [mock(AbstractListener, :update => nil)]
|
14
|
+
log = false
|
15
|
+
features = %w[a b]
|
16
|
+
|
17
|
+
mock_queue = mock(WorkerQueue)
|
18
|
+
mock_workers = Array.new(2) { |n| mock("Worker-#{n}") }
|
19
|
+
|
20
|
+
Process.stub(:pid => 1234)
|
21
|
+
|
22
|
+
WorkerQueue.should_receive(:new).with(max).and_return mock_queue
|
23
|
+
Worker.should_receive(:new).with("a", :json, "/tmp/1234", []).and_return mock_workers[0]
|
24
|
+
Worker.should_receive(:new).with("b", :json, "/tmp/1234", []).and_return mock_workers[1]
|
25
|
+
|
26
|
+
mock_queue.should_receive(:add_observer).once.with listeners.first
|
27
|
+
mock_queue.should_receive(:add).with mock_workers[0]
|
28
|
+
mock_queue.should_receive(:add).with mock_workers[1]
|
29
|
+
|
30
|
+
Runner.create(features,
|
31
|
+
:max => max,
|
32
|
+
:notify => listeners,
|
33
|
+
:format => format,
|
34
|
+
:log => false,
|
35
|
+
:out => out
|
36
|
+
).should be_kind_of(Runner)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "sets up the VNC pool if :vnc => true" do
|
40
|
+
mock_pool = mock(VncServerPool, :add_observer => nil)
|
41
|
+
VncServerPool.should_receive(:new).with(2).and_return mock_pool
|
42
|
+
VncListener.should_receive(:new).with(mock_pool).and_return mock(:update => nil)
|
43
|
+
|
44
|
+
Runner.create([], :max => 2, :vnc => true)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "creates and runs a new runner" do
|
48
|
+
r = mock(Runner)
|
49
|
+
Runner.should_receive(:create).with(%w[a b], {}).and_return(r)
|
50
|
+
r.should_receive(:run)
|
51
|
+
|
52
|
+
Runner.run(%w[a b])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "running" do
|
57
|
+
let(:listener) { mock(AbstractListener, :update => nil) }
|
58
|
+
let(:queue) { mock(Queue, :has_failures? => false) }
|
59
|
+
let(:runner) { Runner.new(queue) }
|
60
|
+
|
61
|
+
it "processes the queue" do
|
62
|
+
runner.add_observer listener
|
63
|
+
|
64
|
+
listener.should_receive(:update).with(:on_run_starting)
|
65
|
+
queue.should_receive(:process).with 0.2 # poll interval
|
66
|
+
queue.should_receive(:wait_until_finished)
|
67
|
+
listener.should_receive(:update).with(:on_run_finished, false)
|
68
|
+
|
69
|
+
runner.run
|
70
|
+
end
|
71
|
+
|
72
|
+
it "fires on_run_interrupted and shuts down if the run is interrupted" do
|
73
|
+
runner.add_observer listener
|
74
|
+
|
75
|
+
queue.stub(:process).and_raise(Interrupt)
|
76
|
+
runner.stub(:stop)
|
77
|
+
listener.should_receive(:update).with(:on_run_interrupted)
|
78
|
+
|
79
|
+
runner.run
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end # Runner
|
84
|
+
end # CukeForker
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module CukeForker
|
4
|
+
describe VncListener do
|
5
|
+
let(:server) { mock(VncServer) }
|
6
|
+
let(:pool) { mock(VncServerPool) }
|
7
|
+
let(:worker) { mock(Worker) }
|
8
|
+
let(:listener) { VncListener.new pool }
|
9
|
+
|
10
|
+
it "fetches a display from the pool and assings it to the worker" do
|
11
|
+
pool.should_receive(:get).and_return(server)
|
12
|
+
worker.should_receive(:vnc=).with server
|
13
|
+
|
14
|
+
listener.on_worker_starting worker
|
15
|
+
end
|
16
|
+
|
17
|
+
it "releases the display and removes it from the worker" do
|
18
|
+
worker.should_receive(:vnc).and_return server
|
19
|
+
pool.should_receive(:release).with server
|
20
|
+
worker.should_receive(:vnc=).with(nil)
|
21
|
+
|
22
|
+
listener.on_worker_finished worker
|
23
|
+
end
|
24
|
+
|
25
|
+
it "stops the pool when the run finishes" do
|
26
|
+
pool.should_receive(:stop)
|
27
|
+
|
28
|
+
listener.on_run_finished(true)
|
29
|
+
end
|
30
|
+
|
31
|
+
end # VncListenerServer
|
32
|
+
end # CukeForker
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module CukeForker
|
4
|
+
describe VncServerPool do
|
5
|
+
let(:pool) { VncServerPool.new(3, SpecHelper::FakeVnc) }
|
6
|
+
|
7
|
+
it "creates 3 instances of the given display class" do
|
8
|
+
SpecHelper::FakeVnc.should_receive(:new).exactly(3).times
|
9
|
+
|
10
|
+
pool = VncServerPool.new(3, SpecHelper::FakeVnc)
|
11
|
+
pool.size.should == 3
|
12
|
+
end
|
13
|
+
|
14
|
+
it "can fetch a server from the pool" do
|
15
|
+
pool.get.should be_kind_of(SpecHelper::FakeVnc)
|
16
|
+
pool.size.should == 2
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can release a server" do
|
20
|
+
obj = pool.get
|
21
|
+
pool.size.should == 2
|
22
|
+
|
23
|
+
pool.release obj
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can stop the pool" do
|
27
|
+
mock_server = mock(VncServer)
|
28
|
+
|
29
|
+
pool.stub(:running => [mock_server])
|
30
|
+
mock_server.should_receive(:stop)
|
31
|
+
|
32
|
+
pool.stop
|
33
|
+
end
|
34
|
+
|
35
|
+
it "raises a TooManyDisplaysError if the pool is over capacity" do
|
36
|
+
lambda { pool.release "foo" }.should raise_error(VncServerPool::TooManyDisplaysError)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "raises a OutOfDisplaysError if the pool is empty" do
|
40
|
+
3.times { pool.get }
|
41
|
+
lambda { pool.get }.should raise_error(VncServerPool::OutOfDisplaysError)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "notifies observers" do
|
45
|
+
server = mock(VncServer, :start => nil, :stop => nil)
|
46
|
+
observer = mock(AbstractListener)
|
47
|
+
|
48
|
+
SpecHelper::FakeVnc.stub :new => server
|
49
|
+
|
50
|
+
observer.should_receive(:update).with :on_display_fetched , server
|
51
|
+
observer.should_receive(:update).with :on_display_released, server
|
52
|
+
observer.should_receive(:update).with :on_display_starting, server
|
53
|
+
observer.should_receive(:update).with :on_display_stopping , server
|
54
|
+
|
55
|
+
pool.add_observer observer
|
56
|
+
|
57
|
+
pool.release pool.get
|
58
|
+
pool.stop
|
59
|
+
end
|
60
|
+
|
61
|
+
end # VncServerPool
|
62
|
+
end # CukeForker
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module CukeForker
|
4
|
+
describe VncServer do
|
5
|
+
|
6
|
+
context "managing new displays" do
|
7
|
+
let(:server) { VncServer.new }
|
8
|
+
|
9
|
+
it "starts a new server" do
|
10
|
+
server.should_receive(:`).with("tightvncserver 2>&1").and_return("desktop is #{Socket.gethostname}:1")
|
11
|
+
server.start
|
12
|
+
server.display.should == ":1"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "stops the server" do
|
16
|
+
server.should_receive(:`).with("tightvncserver -kill :5 2>&1")
|
17
|
+
server.stub :display => ":5"
|
18
|
+
server.stop
|
19
|
+
end
|
20
|
+
|
21
|
+
it "raises VncServer::Error if the server could not be started" do
|
22
|
+
server.should_receive(:`).and_return("oops")
|
23
|
+
server.stub :last_status => mock(:success? => false)
|
24
|
+
|
25
|
+
lambda { server.start }.should raise_error(VncServer::Error, /oops/)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "controlling an existing display" do
|
30
|
+
let(:server) { VncServer.new ":5" }
|
31
|
+
|
32
|
+
it "starts the server on the given display" do
|
33
|
+
server.should_receive(:`).with("tightvncserver :5 2>&1").and_return("desktop is #{Socket.gethostname}:5")
|
34
|
+
server.start
|
35
|
+
server.display.should == ":5"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns an instance for all existing displays" do
|
40
|
+
Dir.stub(:[]).and_return [".vnc/qa1:1.pid", ".vnc/qa1:2.pid", ".vnc/qa1:3.pid"]
|
41
|
+
|
42
|
+
all = VncServer.all
|
43
|
+
all.size.should == 3
|
44
|
+
all.map { |e| e.display }.should == [":1", ":2", ":3"]
|
45
|
+
end
|
46
|
+
|
47
|
+
end # VncServer
|
48
|
+
end # CukeForker
|