jcukeforker 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module JCukeForker
2
+ VERSION = "0.2.2"
3
+ 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
@@ -0,0 +1,6 @@
1
+ require_relative './worker'
2
+
3
+ worker = JCukeForker::Worker.new *$ARGV
4
+ worker.register
5
+ worker.run
6
+ worker.close
@@ -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