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