cukeforker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ \#*
6
+ .\#*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color -fs
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cukeforker.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 Jari Bakken
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
data/README.mdown ADDED
@@ -0,0 +1,26 @@
1
+ cukeforker
2
+ ==========
3
+
4
+ Forking cukes and VNC displays.
5
+
6
+ Usage
7
+ =============
8
+
9
+ TODO.
10
+
11
+ Note on Patches/Pull Requests
12
+ =============================
13
+
14
+ * Fork the project.
15
+ * Make your feature addition or bug fix.
16
+ * Add tests for it. This is important so I don't break it in a
17
+ future version unintentionally.
18
+ * Commit, do not mess with rakefile, version, or history.
19
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
20
+ * Send me a pull request. Bonus points for topic branches.
21
+
22
+ Copyright
23
+ =========
24
+
25
+ Copyright (c) 2011 Jari Bakken. See LICENSE for details.
26
+
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new
data/bin/cukeforker ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cukeforker'
4
+
5
+ split = ARGV.index("--")
6
+ extra_args = ARGV[0..(split-1)]
7
+ features = ARGV[(split+1)..-1]
8
+
9
+ CukeForker::Runner.run(features, :extra_args => extra_args)
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cukeforker/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cukeforker"
7
+ s.version = CukeForker::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jari Bakken"]
10
+ s.email = ["jari.bakken@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Library to maintain a forking queue of Cucumber processes}
13
+ s.description = %q{Library to maintain a forking queue of Cucumber processes, with optional VNC displays.}
14
+
15
+ s.rubyforge_project = "cukeforker"
16
+
17
+ s.add_dependency "cucumber"
18
+ s.add_development_dependency "rspec"
19
+ s.add_development_dependency "simplecov"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
data/lib/cukeforker.rb ADDED
@@ -0,0 +1,20 @@
1
+ unless RUBY_PLATFORM =~ /darwin|linux/
2
+ raise "CukeForker only supported on *nix/MRI"
3
+ end
4
+
5
+
6
+ require "cucumber/cli/main"
7
+ require "fileutils"
8
+ require "observer"
9
+
10
+ module CukeForker
11
+ end
12
+
13
+ require 'cukeforker/vnc_server_pool'
14
+ require 'cukeforker/vnc_server'
15
+ require 'cukeforker/abstract_listener'
16
+ require 'cukeforker/vnc_listener'
17
+ require 'cukeforker/logging_listener'
18
+ require 'cukeforker/worker'
19
+ require 'cukeforker/worker_queue'
20
+ require 'cukeforker/runner'
@@ -0,0 +1,39 @@
1
+ module CukeForker
2
+ class AbstractListener
3
+
4
+ def on_run_starting
5
+ end
6
+
7
+ def on_worker_starting(worker)
8
+ end
9
+
10
+ def on_worker_finished(worker)
11
+ end
12
+
13
+ def on_run_interrupted
14
+ end
15
+
16
+ def on_run_finished(failed)
17
+ end
18
+
19
+ def on_display_fetched(server)
20
+ end
21
+
22
+ def on_display_released(server)
23
+ end
24
+
25
+ def on_display_starting(server)
26
+ end
27
+
28
+ def on_display_stopping(server)
29
+ end
30
+
31
+ def on_eta(time, remaining, finished)
32
+ end
33
+
34
+ def update(meth, *args)
35
+ __send__(meth, *args)
36
+ end
37
+
38
+ end # AbstractListener
39
+ end # CukeForker
@@ -0,0 +1,65 @@
1
+ require "logger"
2
+
3
+ module CukeForker
4
+ class LoggingListener < AbstractListener
5
+ TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
6
+
7
+ def initialize(io = STDOUT)
8
+ @io = io
9
+ end
10
+
11
+ def on_run_starting
12
+ log.info "[ run ] starting"
13
+ end
14
+
15
+ def on_worker_starting(worker)
16
+ log.info "[ worker(#{worker.id}) ] starting: #{worker.feature}"
17
+ end
18
+
19
+ def on_worker_finished(worker)
20
+ log.info "[ worker(#{worker.id}) ] finished: #{worker.feature}"
21
+ end
22
+
23
+ def on_run_finished(failed)
24
+ log.info "[ run ] finished, #{failed ? 'failed' : 'passed'}"
25
+ end
26
+
27
+ def on_run_interrupted
28
+ puts "\n"
29
+ log.info "[ run ] interrupted - please wait"
30
+ end
31
+
32
+ def on_display_fetched(server)
33
+ log.info "[ display(#{server.display.to_s.ljust(2)}) ] fetched"
34
+ end
35
+
36
+ def on_display_released(server)
37
+ log.info "[ display(#{server.display.to_s.ljust(2)}) ] released"
38
+ end
39
+
40
+ def on_display_starting(server)
41
+ log.info "[ display(#{server.display.to_s.ljust(2)}) ] starting"
42
+ end
43
+
44
+ def on_display_stopping(server)
45
+ log.info "[ display(#{server.display.to_s.ljust(2)}) ] stopping"
46
+ end
47
+
48
+ def on_eta(eta, remaining, finished)
49
+ counts = "(#{remaining}/#{finished})".ljust(11)
50
+ log.info "[ eta#{counts}] #{eta.strftime TIME_FORMAT}"
51
+ end
52
+
53
+ private
54
+
55
+ def log
56
+ @log ||= (
57
+ log = Logger.new @io
58
+ log.datetime_format = TIME_FORMAT
59
+
60
+ log
61
+ )
62
+ end
63
+ end # LoggingListener
64
+
65
+ end # CukeForker
@@ -0,0 +1,104 @@
1
+ module CukeForker
2
+
3
+ #
4
+ # Runner.run(features, opts)
5
+ #
6
+ # where 'features' is an Array of file:line
7
+ # and 'opts' is a Hash of options:
8
+ #
9
+ # :max => Fixnum number of workers (default: 2)
10
+ # :vnc => true/false children are launched with DISPLAY set from a VNC server pool,
11
+ # where the size of the pool is equal to :max
12
+ # :notify => object (or array of objects) implementing the AbstractListener API
13
+ # :out => path directory to dump output to (default: current working dir)
14
+ # :log => true/false wether or not to log to stdout (default: true)
15
+ # :format => Symbol format passed to `cucumber --format` (default: html)
16
+ # :extra_args => Array extra arguments passed to cucumber
17
+ #
18
+
19
+ class Runner
20
+ include Observable
21
+
22
+ DEFAULT_OPTIONS = {
23
+ :max => 2,
24
+ :vnc => false,
25
+ :notify => [],
26
+ :out => Dir.pwd,
27
+ :log => true,
28
+ :format => :html
29
+ }
30
+
31
+ def self.run(features, opts = {})
32
+ create(features, opts).run
33
+ end
34
+
35
+ def self.create(features, opts = {})
36
+ opts = DEFAULT_OPTIONS.dup.merge(opts)
37
+
38
+ max = opts[:max]
39
+ format = opts[:format]
40
+ out = File.join opts[:out], Process.pid.to_s
41
+ listeners = Array(opts[:notify])
42
+ extra_args = Array(opts[:extra_args])
43
+
44
+ if opts[:log]
45
+ listeners << LoggingListener.new
46
+ end
47
+
48
+ if opts[:vnc]
49
+ vnc_pool = VncServerPool.new(max)
50
+ listeners << VncListener.new(vnc_pool)
51
+ end
52
+
53
+ queue = WorkerQueue.new max
54
+ features.each do |feature|
55
+ queue.add Worker.new(feature, format, out, extra_args)
56
+ end
57
+
58
+ runner = Runner.new queue
59
+
60
+ listeners.each { |listener|
61
+ queue.add_observer listener
62
+ runner.add_observer listener
63
+ vnc_pool.add_observer listener if opts[:vnc]
64
+ }
65
+
66
+ runner
67
+ end
68
+
69
+ def initialize(queue)
70
+ @queue = queue
71
+ end
72
+
73
+ def run
74
+ start
75
+ process
76
+ stop
77
+ rescue Interrupt
78
+ fire :on_run_interrupted
79
+ stop
80
+ end
81
+
82
+ private
83
+
84
+ def start
85
+ fire :on_run_starting
86
+ end
87
+
88
+ def process
89
+ @queue.process 0.2
90
+ end
91
+
92
+ def stop
93
+ @queue.wait_until_finished 0.5
94
+ ensure # catch potential second Interrupt
95
+ fire :on_run_finished, @queue.has_failures?
96
+ end
97
+
98
+ def fire(*args)
99
+ changed
100
+ notify_observers(*args)
101
+ end
102
+
103
+ end # Runner
104
+ end # CukeForker
@@ -0,0 +1,3 @@
1
+ module CukeForker
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,21 @@
1
+ module CukeForker
2
+ class VncListener < AbstractListener
3
+ def initialize(pool)
4
+ @pool = pool
5
+ end
6
+
7
+ def on_worker_starting(worker)
8
+ worker.vnc = @pool.get
9
+ end
10
+
11
+ def on_worker_finished(worker)
12
+ @pool.release worker.vnc
13
+ worker.vnc = nil
14
+ end
15
+
16
+ def on_run_finished(failed)
17
+ @pool.stop
18
+ end
19
+
20
+ end # VncListener
21
+ end # CukeForker
@@ -0,0 +1,62 @@
1
+ require "socket"
2
+
3
+ module CukeForker
4
+ class VncServer
5
+
6
+ class Error < StandardError
7
+ end
8
+
9
+ class << self
10
+ def displays
11
+ Dir[File.expand_path("~/.vnc/*.pid")].map { |e| e[/(\d+)\.pid/, 1] }.compact
12
+ end
13
+
14
+ def all
15
+ displays.map { |display| new ":#{display}" }
16
+ end
17
+ end
18
+
19
+ attr_reader :display
20
+
21
+ def initialize(display = nil)
22
+ @display = display
23
+ end
24
+
25
+ def start
26
+ if display
27
+ server display
28
+ else
29
+ output = server
30
+ @display = output[/desktop is #{host}(\S+)/, 1]
31
+ end
32
+ end
33
+
34
+ def stop
35
+ server "-kill", display.to_s
36
+ end
37
+
38
+ private
39
+
40
+ def server(*args)
41
+ cmd = ['tightvncserver', args, '2>&1'].flatten.compact
42
+ out = `#{cmd.join ' '}`
43
+
44
+ unless last_status.success?
45
+ raise Error, "could not run tightvncserver: #{out.inspect}"
46
+ end
47
+
48
+ out
49
+ end
50
+
51
+ def last_status
52
+ $?
53
+ end
54
+
55
+ def host
56
+ @host ||= Socket.gethostname
57
+ end
58
+ end # VncServer
59
+ end # CukeForker
60
+
61
+
62
+
@@ -0,0 +1,68 @@
1
+ module CukeForker
2
+ class VncServerPool
3
+ include Observable
4
+
5
+ def initialize(capacity, klass = VncServer)
6
+ @capacity = capacity
7
+ @servers = Array.new(capacity) { klass.new }
8
+ @running = []
9
+ end
10
+
11
+ def stop
12
+ running.each do |s|
13
+ fire :on_display_stopping, s
14
+ s.stop
15
+ end
16
+ end
17
+
18
+ def size
19
+ @servers.size
20
+ end
21
+
22
+ def get
23
+ raise OutOfDisplaysError if @servers.empty?
24
+
25
+ server = next_server
26
+ fire :on_display_fetched, server
27
+
28
+ server
29
+ end
30
+
31
+ def release(server)
32
+ raise TooManyDisplaysError if size == @capacity
33
+ fire :on_display_released, server
34
+
35
+ @servers.unshift server
36
+ end
37
+
38
+ private
39
+
40
+ def fire(*args)
41
+ changed
42
+ notify_observers(*args)
43
+ end
44
+
45
+ def running
46
+ @running
47
+ end
48
+
49
+ def next_server
50
+ server = @servers.shift
51
+
52
+ if server.display.nil?
53
+ fire :on_display_starting, server
54
+ server.start
55
+ @running << server
56
+ end
57
+
58
+ server
59
+ end
60
+
61
+ class TooManyDisplaysError < StandardError
62
+ end
63
+
64
+ class OutOfDisplaysError < StandardError
65
+ end
66
+
67
+ end # DisplayPool
68
+ end # CukeForker