jcukeforker 0.2.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 32563d887a984beb002a2661539769d5908beeb3
4
+ data.tar.gz: 602298483da878759729ab39644dfae2058012d9
5
+ SHA512:
6
+ metadata.gz: f956f1cc6927646155a24209b2b796a058818a39ca038df79e7f4de673ea15803840a8bba9f638d6dd658c2a6c8c0f34b5a76bcd474321e50404561249d2345e
7
+ data.tar.gz: 4245453e0c2a25cc5cfe430932cf499fbd4fbeb2ff6fa312c2724b380543b6981ab8eeb7f8cbd86c6c5771fb01a044a1b816ffd73efdbfc79d19c941fbc0d0f1
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ \#*
6
+ .\#*
7
+ .idea
8
+ .ruby-version
9
+ /coverage
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-2014 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,45 @@
1
+ # jcukeforker
2
+
3
+ Forking cukes and VNC displays.
4
+
5
+ Jcukeforker is a fork of cukeforker desgined for jruby.
6
+
7
+ ### NB!
8
+
9
+ If you're using cukeforker with selenium-webdriver and Firefox, all versions prior to 2.40 has a bug where custom
10
+ Firefox profiles created in a forked process would not get cleaned up. Please make sure you're using selenium-webdriver >= 2.40
11
+ to avoid this.
12
+
13
+ ## Usage
14
+
15
+
16
+ ```ruby
17
+ # parallelize per feature
18
+ JCukeForker::Runner.run Dir['features/**/*.feature'],
19
+ :max => 4 # number of workers
20
+ :out => "/path/to/reports", # output path
21
+ :format => :html # passed to `cucumber --format`,
22
+ :extra_args => %w[--extra arguments] # passed to cucumber,
23
+ :vnc => true # manage a pool of VNC displays, assign one per worker.
24
+
25
+ # parallelize per scenario, with one JUnit XML file per scenario.
26
+ JCukeForker::Runner.run JCukeForker::Scenarios.tagged(%W[@edition ~@wip])
27
+ :extra_args => %W[-f CukeForker::Formatters::JunitScenarioFormatter --out results/junit]
28
+ ```
29
+
30
+ Note on Patches/Pull Requests
31
+ =============================
32
+
33
+ * Fork the project.
34
+ * Make your feature addition or bug fix.
35
+ * Add tests for it. This is important so I don't break it in a
36
+ future version unintentionally.
37
+ * Commit, do not mess with rakefile, version, or history.
38
+ (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)
39
+ * Send me a pull request. Bonus points for topic branches.
40
+
41
+ Copyright
42
+ =========
43
+
44
+ Copyright (c) 2011-2014 Jari Bakken. See LICENSE for details.
45
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new
6
+
7
+ task :default => :spec
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,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "jcukeforker/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "jcukeforker"
7
+ s.version = JCukeForker::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jason Gowan", "Jari Bakken"]
10
+ s.email = ["gowanjason@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 = "jcukeforker"
16
+
17
+ s.add_dependency "cucumber", ">= 1.1.5"
18
+ s.add_dependency "vnctools", ">= 0.1.1"
19
+ s.add_dependency "celluloid-io", ">= 0.15.0"
20
+ s.add_dependency "childprocess", ">= 0.5.3"
21
+ s.add_development_dependency "rspec", "~> 2.5"
22
+ s.add_development_dependency "coveralls"
23
+ s.add_development_dependency "rake", "~> 0.9.2"
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+ end
@@ -0,0 +1,57 @@
1
+ module JCukeForker
2
+ class AbstractListener
3
+
4
+ def on_run_starting
5
+ end
6
+
7
+ def on_worker_waiting(worker_path)
8
+ end
9
+
10
+ def on_worker_dead(worker_path)
11
+ end
12
+
13
+ def on_task_starting(worker_path, feature)
14
+ end
15
+
16
+ def on_task_finished(worker_path, feature, status)
17
+ end
18
+
19
+ def on_worker_forked(worker)
20
+ end
21
+
22
+ def on_worker_register(worker_path)
23
+ end
24
+
25
+ def on_worker_waiting(worker_path)
26
+ end
27
+
28
+ def on_worker_dead(worker_path)
29
+ end
30
+
31
+ def on_run_interrupted
32
+ end
33
+
34
+ def on_run_finished(failed)
35
+ end
36
+
37
+ def on_display_fetched(server)
38
+ end
39
+
40
+ def on_display_released(server)
41
+ end
42
+
43
+ def on_display_starting(worker_path, display)
44
+ end
45
+
46
+ def on_display_stopping(worker_path, display)
47
+ end
48
+
49
+ def on_eta(time, remaining, finished)
50
+ end
51
+
52
+ def update(meth, *args)
53
+ __send__(meth, *args)
54
+ end
55
+
56
+ end # AbstractListener
57
+ end # CukeForker
@@ -0,0 +1,16 @@
1
+
2
+ module JCukeForker
3
+ class ConfigurableVncServer
4
+
5
+ def self.create_class(launch_arguments)
6
+ Class.new(VncTools::Server) do
7
+
8
+ define_method :launch_arguments do
9
+ launch_arguments
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
@@ -0,0 +1,40 @@
1
+ require 'cucumber/formatter/junit'
2
+ require 'cucumber/formatter/ordered_xml_markup'
3
+ module JCukeForker
4
+ module Formatters
5
+ class JunitScenarioFormatter < Cucumber::Formatter::Junit
6
+ def feature_result_filename(feature_file)
7
+ File.join(@reportdir, "TEST-#{basename(feature_file)}.xml")
8
+ end
9
+
10
+ def after_feature(feature)
11
+ # do nothing
12
+ end
13
+
14
+ def feature_element_line_number(feature_element)
15
+ if feature_element.respond_to? :line
16
+ feature_element.line
17
+ else
18
+ feature_element.instance_variable_get(:@line)
19
+ end
20
+ end
21
+
22
+ def after_feature_element(feature_element)
23
+ @testsuite = Cucumber::Formatter::OrderedXmlMarkup.new( :indent => 2 )
24
+ @testsuite.instruct!
25
+ @testsuite.testsuite(
26
+ :failures => @failures,
27
+ :errors => @errors,
28
+ :skipped => @skipped,
29
+ :tests => @tests,
30
+ :time => "%.6f" % @time,
31
+ :name => @feature_name ) do
32
+ @testsuite << @builder.target!
33
+ end
34
+
35
+ line_number = feature_element_line_number(feature_element)
36
+ write_file(feature_result_filename(feature_element.feature.file+"-#{line_number}"), @testsuite.target!)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ require 'gherkin/tag_expression'
2
+ module JCukeForker
3
+ module Formatters
4
+ class ScenarioLineLogger
5
+ attr_reader :scenarios
6
+
7
+ def initialize(tag_expression = Gherkin::TagExpression.new([]))
8
+ @scenarios = []
9
+ @tag_expression = tag_expression
10
+ end
11
+
12
+ def visit_feature_element(feature_element)
13
+ if @tag_expression.evaluate(feature_element.source_tags)
14
+ line_number = if feature_element.respond_to?(:line)
15
+ feature_element.line
16
+ else
17
+ feature_element.location.line
18
+ end
19
+
20
+ @scenarios << [feature_element.feature.file, line_number].join(':')
21
+ end
22
+ end
23
+
24
+ def method_missing(*args)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ require "logger"
2
+
3
+ module JCukeForker
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_register(worker_path)
16
+ log.info "[ worker #{worker_id(worker_path).ljust 3} ] register: #{worker_path}"
17
+ end
18
+
19
+ def on_worker_dead(worker_path)
20
+ log.info "[ worker #{worker_id(worker_path).ljust 3} ] dead : #{worker_path}"
21
+ end
22
+
23
+ def on_task_starting(worker_path, feature)
24
+ log.info "[ worker #{worker_id(worker_path).ljust 3} ] starting: #{feature}"
25
+ end
26
+
27
+ def on_task_finished(worker_path, feature, status)
28
+ log.info "[ worker #{worker_id(worker_path).ljust 3} ] #{status_string(status).ljust(8)}: #{feature}"
29
+ end
30
+
31
+ def on_run_finished(failed)
32
+ log.info "[ run ] finished, #{status_string failed}"
33
+ end
34
+
35
+ def on_run_interrupted
36
+ puts "\n"
37
+ log.info "[ run ] interrupted - please wait"
38
+ end
39
+
40
+ private
41
+
42
+ def status_string(failed)
43
+ failed == 'false' ? 'failed' : 'passed'
44
+ end
45
+
46
+ def worker_id(worker_path)
47
+ /\-(\d+)$/.match(worker_path).captures[0]
48
+ end
49
+
50
+ def log
51
+ @log ||= (
52
+ log = Logger.new @io
53
+ log.datetime_format = TIME_FORMAT
54
+
55
+ log
56
+ )
57
+ end
58
+ end # LoggingListener
59
+ end # CukeForker
@@ -0,0 +1,60 @@
1
+
2
+ module JCukeForker
3
+ class RecordingVncListener < AbstractListener
4
+
5
+ attr_reader :output
6
+
7
+ def initialize(worker, opts = {})
8
+ @ext = opts[:codec] || "webm"
9
+ @options = opts
10
+ @worker = worker
11
+
12
+ @recorder = nil
13
+ end
14
+
15
+ def on_task_starting(worker_path, feature)
16
+
17
+ @recorder = recorder_for(feature)
18
+ @recorder.start
19
+ end
20
+
21
+ def on_task_finished(worker, feature, status)
22
+ if @recorder.crashed?
23
+ raise 'ffmpeg failed'
24
+ end
25
+
26
+ unless worker.failed?
27
+ FileUtils.rm_rf output
28
+ end
29
+
30
+ @recorder.stop
31
+
32
+ @recorder = nil
33
+ end
34
+
35
+ def on_worker_dead(worker_path)
36
+ @recorder && @recorder.stop
37
+ end
38
+
39
+ private
40
+
41
+ def recorder_for(feature)
42
+ @output = File.join(@worker.out, "#{feature.gsub(/\W/, '_')}.#{@ext}")
43
+
44
+ process = ChildProcess.build(
45
+ 'ffmpeg',
46
+ '-an',
47
+ '-y',
48
+ '-f', 'x11grab',
49
+ '-r', @options[:frame_rate] || '5',
50
+ '-s', @options[:frame_size] || '1024x768',
51
+ '-i', ENV['DISPLAY'],
52
+ '-vcodec', @options[:codec] || 'vp8',
53
+ @output
54
+ )
55
+ process.io.stdout = process.io.stderr = File.open('/dev/null', 'w')
56
+ process
57
+ end
58
+
59
+ end # RecordingVncListener
60
+ end # CukeForker
@@ -0,0 +1,155 @@
1
+ module JCukeForker
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, pass 0 for unlimited)
10
+ # :vnc => true/false,Class,Array children are launched with DISPLAY set from a VNC server pool,
11
+ # where the size of the pool is equal to :max. If passed a Class instance,
12
+ # this will be passed as the second argument to VncTools::ServerPool.
13
+ # :record => true/false,Hash whether to record a video of failed tests (requires ffmpeg)
14
+ # this will be ignored if if :vnc is not true. If passed a Hash,
15
+ # this will be passed as options to RecordingVncListener
16
+ # :notify => object (or array of objects) implementing the AbstractListener API
17
+ # :out => path directory to dump output to (default: current working dir)
18
+ # :log => true/false wether or not to log to stdout (default: true)
19
+ # :format => Symbol format passed to `cucumber --format` (default: html)
20
+ # :extra_args => Array extra arguments passed to cucumber
21
+ # :delay => Numeric seconds to sleep between each worker is started (default: 0)
22
+ #
23
+
24
+ class Runner
25
+ include Observable
26
+
27
+ DEFAULT_OPTIONS = {
28
+ :max => 2,
29
+ :vnc => false,
30
+ :record => false,
31
+ :notify => nil,
32
+ :out => Dir.pwd,
33
+ :log => true,
34
+ :format => :html,
35
+ :delay => 0
36
+ }
37
+
38
+ def self.run(features, opts = {})
39
+ create(features, opts).run
40
+ end
41
+
42
+ def self.create(features, opts = {})
43
+ opts = DEFAULT_OPTIONS.dup.merge(opts)
44
+
45
+ max = opts[:max]
46
+ format = opts[:format]
47
+ out = File.join opts[:out]
48
+ listeners = Array(opts[:notify])
49
+ extra_args = Array(opts[:extra_args])
50
+ delay = opts[:delay]
51
+
52
+ if opts[:log]
53
+ listeners << LoggingListener.new
54
+ end
55
+
56
+ task_manager = TaskManager.new
57
+ features.each do |feature|
58
+ task_manager.add({feature: feature, format: format,out: out,extra_args: extra_args})
59
+ end
60
+
61
+ listeners << task_manager
62
+ status_server = StatusServer.new '6333'
63
+ worker_dir = "/tmp/jcukeforker-#{SecureRandom.hex 4}"
64
+ FileUtils.mkdir_p worker_dir
65
+
66
+ vnc_pool = nil
67
+ if vnc = opts[:vnc]
68
+ if vnc.kind_of?(Array)
69
+ vnc_pool = VncTools::ServerPool.new(max, ConfigurableVncServer.create_class(vnc))
70
+ elsif vnc.kind_of?(Class)
71
+ vnc_pool = VncTools::ServerPool.new(max, vnc)
72
+ else
73
+ vnc_pool = VncTools::ServerPool.new(max)
74
+ end
75
+ end
76
+
77
+ processes = create_processes(max, '6333', worker_dir, vnc_pool, opts[:record])
78
+
79
+ runner = Runner.new status_server, processes, worker_dir, vnc_pool, delay
80
+
81
+ listeners.each { |l|
82
+ status_server.add_observer l
83
+ runner.add_observer l
84
+ }
85
+
86
+ runner
87
+ end
88
+
89
+ def initialize(status_server, processes, worker_dir, vnc_pool, delay)
90
+ @status_server = status_server
91
+ @processes = processes
92
+ @worker_dir = worker_dir
93
+ @vnc_pool = vnc_pool
94
+ @delay = delay
95
+ end
96
+
97
+ def run
98
+ start
99
+ process
100
+ stop
101
+ rescue Interrupt
102
+ fire :on_run_interrupted
103
+ stop
104
+ rescue StandardError
105
+ fire :on_run_interrupted
106
+ stop
107
+ raise
108
+ end
109
+
110
+ private
111
+
112
+ def self.create_processes(max, status_path, worker_dir, vnc_pool = nil, record = false)
113
+ worker_file = "#{File.expand_path File.dirname(__FILE__)}/worker_script.rb"
114
+
115
+ (1..max).inject([]) do |l, i|
116
+ process_args = %W[ruby #{worker_file} #{status_path} #{worker_dir}/worker-#{i}]
117
+ if vnc_pool && record
118
+ record = {} unless record.kind_of? Hash
119
+ process_args << record.to_json
120
+ end
121
+ process = ChildProcess.build(*process_args)
122
+ process.environment['DISPLAY'] = vnc_pool.get.display if vnc_pool
123
+ l << process
124
+ end
125
+ end
126
+
127
+ def start
128
+ @status_server.async.run
129
+ fire :on_run_starting
130
+
131
+ @processes.each do |process|
132
+ process.start
133
+ sleep @delay
134
+ end
135
+ end
136
+
137
+ def process
138
+ @processes.each &:wait
139
+ end
140
+
141
+ def stop
142
+ @status_server.shutdown
143
+ ensure # catch potential second Interrupt
144
+ @vnc_pool.stop if @vnc_pool
145
+ FileUtils.rm_r @worker_dir
146
+ #fire :on_run_finished, @queue.has_failures?
147
+ end
148
+
149
+ def fire(*args)
150
+ changed
151
+ notify_observers(*args)
152
+ end
153
+
154
+ end # Runner
155
+ end # CukeForker