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