jcukeforker 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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