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.
@@ -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