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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.mdown +45 -0
- data/Rakefile +7 -0
- data/bin/cukeforker +9 -0
- data/jcukeforker.gemspec +29 -0
- data/lib/jcukeforker/abstract_listener.rb +57 -0
- data/lib/jcukeforker/configurable_vnc_server.rb +16 -0
- data/lib/jcukeforker/formatters/junit_scenario_formatter.rb +40 -0
- data/lib/jcukeforker/formatters/scenario_line_logger.rb +28 -0
- data/lib/jcukeforker/logging_listener.rb +59 -0
- data/lib/jcukeforker/recording_vnc_listener.rb +60 -0
- data/lib/jcukeforker/runner.rb +155 -0
- data/lib/jcukeforker/scenarios.rb +43 -0
- data/lib/jcukeforker/status_server.rb +37 -0
- data/lib/jcukeforker/task_manager.rb +45 -0
- data/lib/jcukeforker/version.rb +3 -0
- data/lib/jcukeforker/worker.rb +122 -0
- data/lib/jcukeforker/worker_script.rb +6 -0
- data/lib/jcukeforker.rb +29 -0
- data/spec/jcukeforker/formatters/scenario_line_logger_spec.rb +44 -0
- data/spec/jcukeforker/logging_listener_spec.rb +43 -0
- data/spec/jcukeforker/recording_vnc_listener_spec.rb +81 -0
- data/spec/jcukeforker/runner_spec.rb +94 -0
- data/spec/jcukeforker/scenarios_spec.rb +67 -0
- data/spec/jcukeforker/status_server_spec.rb +36 -0
- data/spec/jcukeforker/task_manager_spec.rb +48 -0
- data/spec/jcukeforker/worker_spec.rb +27 -0
- data/spec/spec_helper.rb +6 -0
- metadata +183 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'cucumber/runtime/features_loader'
|
2
|
+
|
3
|
+
module JCukeForker
|
4
|
+
|
5
|
+
#
|
6
|
+
# CukeForker::Scenarios.by_args(args)
|
7
|
+
#
|
8
|
+
# where 'args' is a String of cucumber options
|
9
|
+
#
|
10
|
+
# For example:
|
11
|
+
# CukeForker::Scenarios.by_args(%W[-p my_profile -t @edition])
|
12
|
+
# will return an array of scenarios and their line numbers that match
|
13
|
+
# the tags specified in the cucumber profile 'my_profile' AND have the '@edition' tag
|
14
|
+
#
|
15
|
+
|
16
|
+
class Scenarios
|
17
|
+
def self.by_args(args)
|
18
|
+
options = Cucumber::Cli::Options.new(STDOUT, STDERR, :default_profile => 'default')
|
19
|
+
tagged(options.parse!(args)[:tag_expressions])
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.all
|
23
|
+
any_tag = []
|
24
|
+
tagged any_tag
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.tagged(tags)
|
28
|
+
tag_expression = Gherkin::TagExpression.new(tags)
|
29
|
+
scenario_line_logger = JCukeForker::Formatters::ScenarioLineLogger.new(tag_expression)
|
30
|
+
loader = Cucumber::Runtime::FeaturesLoader.new(feature_files, [], tag_expression)
|
31
|
+
|
32
|
+
loader.features.each do |feature|
|
33
|
+
feature.accept(scenario_line_logger)
|
34
|
+
end
|
35
|
+
|
36
|
+
scenario_line_logger.scenarios
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.feature_files
|
40
|
+
Dir.glob('**/**.feature')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
module JCukeForker
|
3
|
+
class StatusServer
|
4
|
+
include Observable
|
5
|
+
include Celluloid::IO
|
6
|
+
|
7
|
+
finalizer :shutdown
|
8
|
+
|
9
|
+
def initialize(port = '6333')
|
10
|
+
@server = TCPServer.new 'localhost', port
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
loop { async.handle_connection @server.accept }
|
15
|
+
end
|
16
|
+
|
17
|
+
def shutdown
|
18
|
+
@server.close if @server
|
19
|
+
end
|
20
|
+
|
21
|
+
def handle_connection(socket)
|
22
|
+
until socket.eof? do
|
23
|
+
raw_message = socket.gets
|
24
|
+
json_obj = JSON.parse raw_message
|
25
|
+
fire json_obj.first, *json_obj[1..-1]
|
26
|
+
end
|
27
|
+
socket.close
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def fire(*args)
|
33
|
+
changed
|
34
|
+
notify_observers *args
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
module JCukeForker
|
3
|
+
class TaskManager < AbstractListener
|
4
|
+
|
5
|
+
def initialize()
|
6
|
+
@tasks = []
|
7
|
+
@worker_sockets = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(task)
|
11
|
+
@tasks << task
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_worker_register(worker_path)
|
15
|
+
@worker_sockets[worker_path] = UNIXSocket.open worker_path
|
16
|
+
pop_task worker_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_task_finished(worker_path, feature, status)
|
20
|
+
pop_task worker_path
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_worker_dead(worker_path)
|
24
|
+
socket = @worker_sockets.delete worker_path
|
25
|
+
socket.close
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
@worker_sockets.each {|k, v| v.close}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def pop_task(worker_path)
|
35
|
+
task = @tasks.shift || '__KILL__'
|
36
|
+
task = task.to_json if task.is_a? Hash
|
37
|
+
|
38
|
+
if task.empty?
|
39
|
+
puts "-----#{worker_path}...EMPTY TASK!"
|
40
|
+
end
|
41
|
+
|
42
|
+
@worker_sockets[worker_path].puts(task)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'cucumber/cli/main'
|
6
|
+
require 'observer'
|
7
|
+
require 'childprocess'
|
8
|
+
require_relative './abstract_listener'
|
9
|
+
require_relative './recording_vnc_listener'
|
10
|
+
|
11
|
+
module JCukeForker
|
12
|
+
class Worker
|
13
|
+
include Observable
|
14
|
+
|
15
|
+
attr_reader :feature, :format, :out
|
16
|
+
|
17
|
+
def initialize(status_path, task_path, recorder = nil)
|
18
|
+
@status_path = status_path
|
19
|
+
@task_path = task_path
|
20
|
+
if ENV['DISPLAY'] && recorder
|
21
|
+
config = recorder ? {} : JSON.parse(recorder)
|
22
|
+
add_observer JCukeForker::RecordingVncListener.new(self, config)
|
23
|
+
end
|
24
|
+
@status_socket = TCPSocket.new 'localhost', status_path
|
25
|
+
@status = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def register
|
29
|
+
@worker_server = UNIXServer.new @task_path
|
30
|
+
update_status :on_worker_register
|
31
|
+
end
|
32
|
+
|
33
|
+
def close
|
34
|
+
@worker_server.close
|
35
|
+
@status_socket.close
|
36
|
+
end
|
37
|
+
|
38
|
+
def run
|
39
|
+
worker_socket = @worker_server.accept
|
40
|
+
loop do
|
41
|
+
raw_message = worker_socket.gets
|
42
|
+
if raw_message.nil? then
|
43
|
+
sleep 0.3
|
44
|
+
next
|
45
|
+
end
|
46
|
+
if raw_message.strip == '__KILL__'
|
47
|
+
update_status :on_worker_dead
|
48
|
+
break
|
49
|
+
end
|
50
|
+
set_state raw_message
|
51
|
+
update_status :on_task_starting, feature
|
52
|
+
status = execute_cucumber
|
53
|
+
update_status :on_task_finished, feature, status
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def update_status(meth, *args)
|
58
|
+
message = [meth, @task_path]
|
59
|
+
message += args
|
60
|
+
|
61
|
+
changed
|
62
|
+
notify_observers *message
|
63
|
+
@status_socket.puts(message.to_json)
|
64
|
+
end
|
65
|
+
|
66
|
+
def failed?
|
67
|
+
@status.nil? || @status
|
68
|
+
end
|
69
|
+
|
70
|
+
def output
|
71
|
+
File.join out, "#{basename}.#{format}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def stdout
|
75
|
+
File.join out, "#{basename}.stdout"
|
76
|
+
end
|
77
|
+
|
78
|
+
def stderr
|
79
|
+
File.join out, "#{basename}.stderr"
|
80
|
+
end
|
81
|
+
|
82
|
+
def basename
|
83
|
+
@basename ||= feature.gsub(/\W/, '_')
|
84
|
+
end
|
85
|
+
|
86
|
+
def args
|
87
|
+
args = %W[--format #{format} --out #{output}]
|
88
|
+
args += @extra_args
|
89
|
+
args << feature
|
90
|
+
|
91
|
+
args
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def set_state(raw_message)
|
97
|
+
json_obj = JSON.parse raw_message
|
98
|
+
@format = json_obj['format']
|
99
|
+
@feature = json_obj['feature']
|
100
|
+
@extra_args = json_obj['extra_args']
|
101
|
+
@out = json_obj['out']
|
102
|
+
end
|
103
|
+
|
104
|
+
def execute_cucumber
|
105
|
+
FileUtils.mkdir_p(out) unless File.exist? out
|
106
|
+
|
107
|
+
$stdout.reopen stdout
|
108
|
+
$stderr.reopen stderr
|
109
|
+
|
110
|
+
begin
|
111
|
+
Cucumber::Cli::Main.execute args
|
112
|
+
rescue SystemExit => e
|
113
|
+
@status = e.success?
|
114
|
+
end
|
115
|
+
|
116
|
+
$stdout.flush
|
117
|
+
$stderr.flush
|
118
|
+
|
119
|
+
@status
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/jcukeforker.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
unless RUBY_PLATFORM =~ /darwin|linux|java/
|
2
|
+
raise "CukeForker only supported on *nix"
|
3
|
+
end
|
4
|
+
|
5
|
+
|
6
|
+
require "cucumber/cli/main"
|
7
|
+
require "vnctools"
|
8
|
+
require "fileutils"
|
9
|
+
require "observer"
|
10
|
+
require "forwardable"
|
11
|
+
require "ostruct"
|
12
|
+
require "json"
|
13
|
+
require "securerandom"
|
14
|
+
require "celluloid/io"
|
15
|
+
require "celluloid/autostart"
|
16
|
+
|
17
|
+
module JCukeForker
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'jcukeforker/abstract_listener'
|
21
|
+
require 'jcukeforker/logging_listener'
|
22
|
+
require 'jcukeforker/runner'
|
23
|
+
require 'jcukeforker/scenarios'
|
24
|
+
require 'jcukeforker/status_server'
|
25
|
+
require 'jcukeforker/task_manager'
|
26
|
+
require 'jcukeforker/configurable_vnc_server'
|
27
|
+
|
28
|
+
require 'jcukeforker/formatters/scenario_line_logger'
|
29
|
+
require 'jcukeforker/formatters/junit_scenario_formatter'
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path("../../../spec_helper", __FILE__)
|
2
|
+
require 'cucumber/ast/scenario_outline'
|
3
|
+
|
4
|
+
module JCukeForker::Formatters
|
5
|
+
describe ScenarioLineLogger do
|
6
|
+
it "returns scenario names and line numbers for a scenario" do
|
7
|
+
logger = ScenarioLineLogger.new
|
8
|
+
|
9
|
+
feature = double("Cucumber::Ast::Feature")
|
10
|
+
feature_element = double("Cucumber::Ast::Scenario")
|
11
|
+
|
12
|
+
feature.should_receive(:file).twice.and_return('features/test1.feature')
|
13
|
+
feature_element.should_receive(:source_tags).twice.and_return('')
|
14
|
+
feature_element.should_receive(:feature).twice.and_return(feature)
|
15
|
+
feature_element.should_receive(:line).and_return(3)
|
16
|
+
feature_element.should_receive(:line).and_return(6)
|
17
|
+
|
18
|
+
logger.visit_feature_element(feature_element)
|
19
|
+
logger.visit_feature_element(feature_element)
|
20
|
+
|
21
|
+
logger.scenarios.length.should == 2
|
22
|
+
logger.scenarios[0].should == "features/test1.feature:3"
|
23
|
+
logger.scenarios[1].should == "features/test1.feature:6"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns scenario names and line numbers for a scenario outline" do
|
27
|
+
logger = ScenarioLineLogger.new
|
28
|
+
|
29
|
+
feature = double("Cucumber::Ast::Feature")
|
30
|
+
location = double("Cucumber::Ast::Location", :line => 4)
|
31
|
+
feature_element = Cucumber::Ast::ScenarioOutline.new(*Array.new(11) {|a| double(a, :each => true) })
|
32
|
+
feature_element.stub(:location => location)
|
33
|
+
|
34
|
+
feature.should_receive(:file).and_return('features/test1.feature')
|
35
|
+
feature_element.should_receive(:source_tags).and_return('')
|
36
|
+
feature_element.should_receive(:feature).and_return(feature)
|
37
|
+
|
38
|
+
logger.visit_feature_element(feature_element)
|
39
|
+
|
40
|
+
logger.scenarios.length.should == 1
|
41
|
+
logger.scenarios[0].should == "features/test1.feature:4"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module JCukeForker
|
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 = {:path => '/tmp/12sd3-1', :feature => 'foo/bar', :failed? => 'true'}
|
12
|
+
mock_worker2 = {:path => '/tmp/12sd3-15', :feature => 'foo/baz', :failed? => 'false'}
|
13
|
+
|
14
|
+
listener.on_run_starting
|
15
|
+
listener.on_worker_register mock_worker[:path]
|
16
|
+
listener.on_task_starting mock_worker[:path], mock_worker[:feature]
|
17
|
+
listener.on_worker_register mock_worker2[:path]
|
18
|
+
listener.on_task_starting mock_worker2[:path], mock_worker2[:feature]
|
19
|
+
listener.on_task_finished mock_worker[:path], mock_worker[:feature], mock_worker[:failed?]
|
20
|
+
listener.on_task_finished mock_worker2[:path], mock_worker2[:feature], mock_worker2[:failed?]
|
21
|
+
listener.on_worker_dead mock_worker[:path]
|
22
|
+
listener.on_worker_dead mock_worker2[:path]
|
23
|
+
listener.on_run_interrupted
|
24
|
+
listener.on_run_finished false
|
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 -- : [ worker 1 ] register: /tmp/12sd3-1
|
31
|
+
I, [#{timestamp}] INFO -- : [ worker 1 ] starting: foo/bar
|
32
|
+
I, [#{timestamp}] INFO -- : [ worker 15 ] register: /tmp/12sd3-15
|
33
|
+
I, [#{timestamp}] INFO -- : [ worker 15 ] starting: foo/baz
|
34
|
+
I, [#{timestamp}] INFO -- : [ worker 1 ] passed : foo/bar
|
35
|
+
I, [#{timestamp}] INFO -- : [ worker 15 ] failed : foo/baz
|
36
|
+
I, [#{timestamp}] INFO -- : [ worker 1 ] dead : /tmp/12sd3-1
|
37
|
+
I, [#{timestamp}] INFO -- : [ worker 15 ] dead : /tmp/12sd3-15
|
38
|
+
I, [#{timestamp}] INFO -- : [ run ] interrupted - please wait
|
39
|
+
I, [#{timestamp}] INFO -- : [ run ] finished, passed
|
40
|
+
OUTPUT
|
41
|
+
end
|
42
|
+
end # Worker
|
43
|
+
end # CukeForker
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
module JCukeForker
|
4
|
+
describe RecordingVncListener do
|
5
|
+
let(:worker) { double(Worker, :out => ".", :basename => "foo", :failed? => true) }
|
6
|
+
let(:recorder) { double(ChildProcess, :start => nil, :stop => nil, :io => double('io').as_null_object, :crashed? => false) }
|
7
|
+
let(:listener) { RecordingVncListener.new worker }
|
8
|
+
|
9
|
+
it "starts recording when the task is started" do
|
10
|
+
worker_path = 'worker_path'
|
11
|
+
feature = 'feature'
|
12
|
+
|
13
|
+
env = ENV['DISPLAY']
|
14
|
+
ENV['DISPLAY']= ':1'
|
15
|
+
ChildProcess.should_receive(:build).with(
|
16
|
+
'ffmpeg',
|
17
|
+
'-an',
|
18
|
+
'-y',
|
19
|
+
'-f', 'x11grab',
|
20
|
+
'-r', '5',
|
21
|
+
'-s', '1024x768',
|
22
|
+
'-i', ':1',
|
23
|
+
'-vcodec', 'vp8',
|
24
|
+
'./feature.webm'
|
25
|
+
).and_return(recorder)
|
26
|
+
|
27
|
+
recorder.should_receive(:start)
|
28
|
+
|
29
|
+
listener.on_task_starting worker, feature
|
30
|
+
ENV['DISPLAY'] = env
|
31
|
+
end
|
32
|
+
|
33
|
+
it "stops recording when the task is finished" do
|
34
|
+
recorder.should_receive(:stop)
|
35
|
+
listener.instance_variable_set(:@recorder, recorder)
|
36
|
+
|
37
|
+
listener.on_task_finished worker, nil, nil
|
38
|
+
|
39
|
+
listener.instance_variable_get(:@recorder).should be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "stops recording when worker dies" do
|
43
|
+
listener.instance_variable_set(:@recorder, recorder)
|
44
|
+
recorder.should_receive(:stop)
|
45
|
+
|
46
|
+
listener.on_worker_dead(nil)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "deletes the output file if the worker succeeded" do
|
50
|
+
recorder.stub(:stop)
|
51
|
+
listener.instance_variable_set(:@recorder, recorder)
|
52
|
+
|
53
|
+
worker.should_receive(:failed?).and_return(false)
|
54
|
+
listener.should_receive(:output).and_return("./foo.mp4")
|
55
|
+
FileUtils.should_receive(:rm_rf).with("./foo.mp4")
|
56
|
+
|
57
|
+
listener.on_task_finished worker, nil, nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it "passes along options to recorder" do
|
61
|
+
listener = RecordingVncListener.new worker, :codec => "flv"
|
62
|
+
env = ENV['DISPLAY']
|
63
|
+
ENV['DISPLAY']= ':1'
|
64
|
+
ChildProcess.should_receive(:build).with(
|
65
|
+
'ffmpeg',
|
66
|
+
'-an',
|
67
|
+
'-y',
|
68
|
+
'-f', 'x11grab',
|
69
|
+
'-r', '5',
|
70
|
+
'-s', '1024x768',
|
71
|
+
'-i', ':1',
|
72
|
+
'-vcodec', 'flv',
|
73
|
+
'./feature.flv'
|
74
|
+
).and_return(recorder)
|
75
|
+
|
76
|
+
listener.on_task_starting worker, 'feature'
|
77
|
+
env = ENV['DISPLAY']
|
78
|
+
end
|
79
|
+
|
80
|
+
end # RecordingVncListener
|
81
|
+
end # CukeForker
|