cukeq 0.0.1.dev
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.
- data/.autotest +1 -0
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +73 -0
- data/Rakefile +68 -0
- data/VERSION +1 -0
- data/bin/cukeq +34 -0
- data/features/cukeq.feature +15 -0
- data/features/example1.feature +1 -0
- data/features/example2.feature +1 -0
- data/features/step_definitions/cukeq_steps.rb +22 -0
- data/features/support/cukeq_helper.rb +73 -0
- data/features/support/env.rb +13 -0
- data/features/support/report_app.rb +36 -0
- data/lib/cukeq.rb +45 -0
- data/lib/cukeq/broker.rb +65 -0
- data/lib/cukeq/em/system3.rb +53 -0
- data/lib/cukeq/job_clearer.rb +19 -0
- data/lib/cukeq/master.rb +146 -0
- data/lib/cukeq/reporter.rb +22 -0
- data/lib/cukeq/runner.rb +63 -0
- data/lib/cukeq/scenario_exploder.rb +32 -0
- data/lib/cukeq/scenario_runner.rb +134 -0
- data/lib/cukeq/scm.rb +51 -0
- data/lib/cukeq/scm/git_bridge.rb +36 -0
- data/lib/cukeq/scm/shell_svn_bridge.rb +65 -0
- data/lib/cukeq/scm/svn_bridge.rb +77 -0
- data/lib/cukeq/slave.rb +114 -0
- data/lib/cukeq/webapp.rb +38 -0
- data/spec/cukeq/broker_spec.rb +78 -0
- data/spec/cukeq/cukeq_spec.rb +10 -0
- data/spec/cukeq/master_spec.rb +153 -0
- data/spec/cukeq/reporter_spec.rb +29 -0
- data/spec/cukeq/runner_spec.rb +11 -0
- data/spec/cukeq/scenario_exploder_spec.rb +17 -0
- data/spec/cukeq/scenario_runner_spec.rb +32 -0
- data/spec/cukeq/scm/git_bridge_spec.rb +52 -0
- data/spec/cukeq/scm/svn_bridge_spec.rb +5 -0
- data/spec/cukeq/scm_spec.rb +48 -0
- data/spec/cukeq/slave_spec.rb +86 -0
- data/spec/cukeq/webapp_spec.rb +37 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- metadata +228 -0
data/lib/cukeq/scm.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module CukeQ
|
2
|
+
class Scm
|
3
|
+
attr_reader :url
|
4
|
+
|
5
|
+
autoload :GitBridge, "cukeq/scm/git_bridge"
|
6
|
+
autoload :SvnBridge, "cukeq/scm/svn_bridge"
|
7
|
+
|
8
|
+
ROOT = File.expand_path("~/.cukeq")
|
9
|
+
|
10
|
+
def initialize(url)
|
11
|
+
@url = url.kind_of?(String) ? URI.parse(url) : url
|
12
|
+
end
|
13
|
+
|
14
|
+
def working_copy
|
15
|
+
@working_copy ||= "#{ROOT}/repos/#{url.host}/#{url.path.gsub(/[^A-z]+/, '_')}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_revision
|
19
|
+
bridge.current_revision
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(&blk)
|
23
|
+
log self.class, :updating, url.to_s => working_copy
|
24
|
+
bridge.update(&blk)
|
25
|
+
log self.class, :done
|
26
|
+
end
|
27
|
+
|
28
|
+
def bridge
|
29
|
+
@bridge ||= (
|
30
|
+
case url.scheme
|
31
|
+
when "git"
|
32
|
+
GitBridge.new url, working_copy
|
33
|
+
when "svn"
|
34
|
+
SvnBridge.new url, working_copy
|
35
|
+
when "http", "https"
|
36
|
+
# TODO: fix heuristic for http scm urls
|
37
|
+
if url.to_s.include?("svn")
|
38
|
+
SvnBridge.new url, working_copy
|
39
|
+
elsif url.to_s.include?("git")
|
40
|
+
GitBridge.new url, working_copy
|
41
|
+
else
|
42
|
+
raise "unknown scm: #{url}"
|
43
|
+
end
|
44
|
+
else
|
45
|
+
raise "unknown scm: #{url}"
|
46
|
+
end
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
end # Scm
|
51
|
+
end # CukeQ
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "git"
|
2
|
+
|
3
|
+
module CukeQ
|
4
|
+
class Scm
|
5
|
+
class GitBridge
|
6
|
+
|
7
|
+
def initialize(url, working_copy)
|
8
|
+
@url = url
|
9
|
+
@working_copy = working_copy
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(&blk)
|
13
|
+
repo.reset_hard
|
14
|
+
repo.pull
|
15
|
+
|
16
|
+
# TODO: async
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_revision
|
21
|
+
repo.revparse("HEAD")
|
22
|
+
end
|
23
|
+
|
24
|
+
def repo
|
25
|
+
@repo ||= (
|
26
|
+
unless File.directory? @working_copy
|
27
|
+
Git.clone(@url, @working_copy)
|
28
|
+
end
|
29
|
+
|
30
|
+
Git.open(@working_copy)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
end # GitBridge
|
35
|
+
end # Scm
|
36
|
+
end # CukeQ
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "open3"
|
2
|
+
require "nokogiri"
|
3
|
+
|
4
|
+
module CukeQ
|
5
|
+
class Scm
|
6
|
+
class ShellSvnBridge
|
7
|
+
|
8
|
+
def initialize(url, working_copy)
|
9
|
+
@url = url
|
10
|
+
@working_copy = working_copy
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(&blk)
|
14
|
+
ensure_working_copy
|
15
|
+
Dir.chdir(@working_copy) { execute "svn update --non-interactive" }
|
16
|
+
|
17
|
+
# TODO: async
|
18
|
+
yield
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_revision
|
22
|
+
ensure_working_copy
|
23
|
+
info[:revision]
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def info
|
29
|
+
data = {}
|
30
|
+
|
31
|
+
xml = Dir.chdir(@working_copy) { execute "svn --xml info" }
|
32
|
+
doc = Nokogiri::XML(xml)
|
33
|
+
|
34
|
+
data[:revision] = doc.css("info entry commit").first['revision'].to_i
|
35
|
+
data[:url] = doc.css("url").text
|
36
|
+
|
37
|
+
data
|
38
|
+
end
|
39
|
+
|
40
|
+
def ensure_working_copy
|
41
|
+
return if File.directory? @working_copy
|
42
|
+
|
43
|
+
log self.class, :checkout, @url.to_s => @working_copy
|
44
|
+
execute "svn checkout #{@url} #{@working_copy}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def execute(cmd)
|
48
|
+
out, err = nil
|
49
|
+
|
50
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr|
|
51
|
+
out = stdout.read
|
52
|
+
err = stderr.read
|
53
|
+
end
|
54
|
+
|
55
|
+
unless $?.success?
|
56
|
+
raise SystemCallError, "#{cmd.inspect}, stdout: #{out.inspect}, stderr: #{err.inspect}"
|
57
|
+
end
|
58
|
+
|
59
|
+
out
|
60
|
+
end
|
61
|
+
end # ShellSvnBridge
|
62
|
+
|
63
|
+
SvnBridge = ShellSvnBridge
|
64
|
+
end # Scm
|
65
|
+
end # CukeQ
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "svn/client" # apt-get install libsvn-ruby
|
2
|
+
|
3
|
+
module CukeQ
|
4
|
+
class Scm
|
5
|
+
class SvnBridge
|
6
|
+
|
7
|
+
def initialize(url, working_copy)
|
8
|
+
@url = url
|
9
|
+
@working_copy = working_copy
|
10
|
+
|
11
|
+
@simple_auth = Hash.new do |hash, realm|
|
12
|
+
hash[realm] = simple_auth_for(realm) || raise_auth_error
|
13
|
+
end
|
14
|
+
|
15
|
+
setup_auth
|
16
|
+
end
|
17
|
+
|
18
|
+
def update(&blk)
|
19
|
+
ensure_working_copy
|
20
|
+
ctx.update(@working_copy).to_s
|
21
|
+
|
22
|
+
# TODO: async
|
23
|
+
yield
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_revision
|
27
|
+
ensure_working_copy
|
28
|
+
|
29
|
+
Dir.chdir(@working_copy) do
|
30
|
+
ctx.status(@working_copy, "BASE").to_s
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def ctx
|
37
|
+
@ctx ||= Svn::Client::Context.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_auth
|
41
|
+
ctx.add_simple_prompt_provider(0) do |cred, realm, username, save|
|
42
|
+
cred.username = ENV['CUKEQ_SVN_USERNAME'] || @url.user || @simple_auth[realm]["username"]
|
43
|
+
cred.password = ENV['CUKEQ_SVN_PASSWORD'] || @url.password || @simple_auth[realm]["password"]
|
44
|
+
cred
|
45
|
+
end
|
46
|
+
|
47
|
+
if ENV['CUKEQ_SVN_INSECURE_SSL']
|
48
|
+
ctx.add_ssl_server_trust_prompt_provider do |cred, host, failures, info, was|
|
49
|
+
cred.accepted_failures = failures
|
50
|
+
cred
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def simple_auth_for(realm)
|
56
|
+
Svn::Core::Config.read_auth_data(Svn::Core::AUTH_CRED_SIMPLE, realm)
|
57
|
+
end
|
58
|
+
|
59
|
+
def raise_auth_error
|
60
|
+
raise <<-END
|
61
|
+
No SVN credentials provided. Either of these will do:
|
62
|
+
* set CUKEQ_SVN_USERNAME and CUKEQ_SVN_PASSWORD
|
63
|
+
* add username and password to the repo URL: https://foo:bar@svn.example.com/
|
64
|
+
* make sure your credentials are saved to disk (~/.subversion/auth)
|
65
|
+
END
|
66
|
+
end
|
67
|
+
|
68
|
+
def ensure_working_copy
|
69
|
+
return if File.directory? @working_copy
|
70
|
+
|
71
|
+
log self.class, :checkout, @url.to_s => @working_copy
|
72
|
+
ctx.checkout(@url.to_s, @working_copy)
|
73
|
+
end
|
74
|
+
|
75
|
+
end # SvnBridge
|
76
|
+
end # Scm
|
77
|
+
end # CukeQ
|
data/lib/cukeq/slave.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CukeQ
|
4
|
+
class Slave
|
5
|
+
DEFAULT_BROKER_URI = URI.parse("amqp://cukeq-slave:cukeq123@localhost:5672/cukeq")
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def execute(argv = [])
|
9
|
+
configured_instance(argv).start
|
10
|
+
end
|
11
|
+
|
12
|
+
def configured_instance(argv = [])
|
13
|
+
opts = parse(argv)
|
14
|
+
|
15
|
+
if b = opts[:broker]
|
16
|
+
b.user ||= DEFAULT_BROKER_URI.user
|
17
|
+
b.password ||= DEFAULT_BROKER_URI.password
|
18
|
+
b.host ||= DEFAULT_BROKER_URI.host
|
19
|
+
b.port ||= DEFAULT_BROKER_URI.port
|
20
|
+
b.path = b.path.empty? ? DEFAULT_BROKER_URI.path : b.path
|
21
|
+
else
|
22
|
+
opts[:broker] = DEFAULT_BROKER_URI
|
23
|
+
end
|
24
|
+
|
25
|
+
new(
|
26
|
+
Broker.new(opts[:broker]),
|
27
|
+
ScenarioRunner.new
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse(argv)
|
32
|
+
options = {}
|
33
|
+
|
34
|
+
argv.extend(OptionParser::Arguable)
|
35
|
+
argv.options do |opts|
|
36
|
+
opts.on("-b", "--broker URI (default: #{DEFAULT_BROKER_URI})") do |b|
|
37
|
+
options[:broker] = URI.parse(b)
|
38
|
+
end
|
39
|
+
end.parse!
|
40
|
+
|
41
|
+
options
|
42
|
+
end
|
43
|
+
end # class << self
|
44
|
+
|
45
|
+
attr_reader :broker, :scenario_runner
|
46
|
+
|
47
|
+
def initialize(broker, scenario_runner)
|
48
|
+
@broker = broker
|
49
|
+
@scenario_runner = scenario_runner
|
50
|
+
end
|
51
|
+
|
52
|
+
def start
|
53
|
+
@broker.start {
|
54
|
+
ping_pong
|
55
|
+
poll
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Publish a message on the results queue
|
61
|
+
#
|
62
|
+
|
63
|
+
def publish(message)
|
64
|
+
log log_name, :publish, message
|
65
|
+
|
66
|
+
@broker.publish :results, message.to_json
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Run a job
|
71
|
+
#
|
72
|
+
|
73
|
+
def job(message)
|
74
|
+
log log_name, :job, message
|
75
|
+
|
76
|
+
@scenario_runner.run(message) { |result|
|
77
|
+
publish(result)
|
78
|
+
EM.next_tick { poll } # job done, start polling again
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
POLL_INTERVAL = 0.25
|
83
|
+
|
84
|
+
#
|
85
|
+
# Poll for new jobs
|
86
|
+
#
|
87
|
+
|
88
|
+
def poll
|
89
|
+
@broker.queue_for(:jobs).pop { |input|
|
90
|
+
if input
|
91
|
+
job JSON.parse(input)
|
92
|
+
else
|
93
|
+
EM.add_timer(POLL_INTERVAL) { poll }
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Subscribe to :ping, respond to :pong
|
100
|
+
#
|
101
|
+
|
102
|
+
def ping_pong
|
103
|
+
@broker.subscribe(:ping) { |message|
|
104
|
+
log log_name, :ping
|
105
|
+
@broker.publish :pong, {:id => CukeQ.identifier, :class => self.class.name}.to_json
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_name
|
110
|
+
@log_name ||= [self.class, Process.pid]
|
111
|
+
end
|
112
|
+
|
113
|
+
end # Slave
|
114
|
+
end # CukeQ
|
data/lib/cukeq/webapp.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module CukeQ
|
2
|
+
class WebApp
|
3
|
+
attr_reader :uri
|
4
|
+
|
5
|
+
def initialize(uri)
|
6
|
+
@uri = uri
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(callback)
|
10
|
+
@callback = callback
|
11
|
+
handler.run(self, :Host => @uri.host, :Port => @uri.port)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
log self.class, :called
|
16
|
+
|
17
|
+
request = Rack::Request.new(env)
|
18
|
+
|
19
|
+
unless request.post?
|
20
|
+
return [405, {'Allow' => 'POST'}, []]
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
data = JSON.parse(request.body.read)
|
25
|
+
@callback.call(data) if @callback
|
26
|
+
rescue JSON::ParserError
|
27
|
+
return [406, {'Content-Type' => 'application/json'}, []]
|
28
|
+
end
|
29
|
+
|
30
|
+
[202, {}, %w[ok]]
|
31
|
+
end
|
32
|
+
|
33
|
+
def handler
|
34
|
+
Rack::Handler::Thin
|
35
|
+
end
|
36
|
+
|
37
|
+
end # App
|
38
|
+
end # CukeQ
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
describe CukeQ::Broker do
|
4
|
+
def queues
|
5
|
+
@queues ||= {
|
6
|
+
:results => mock("results-queue"),
|
7
|
+
:jobs => mock("jobs-queue")
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def running_broker
|
12
|
+
broker = CukeQ::Broker.new(URI.parse("amqp://cukeq-master@localhost:1234/cukeq"))
|
13
|
+
broker.instance_variable_set("@queues", queues)
|
14
|
+
|
15
|
+
broker
|
16
|
+
end
|
17
|
+
|
18
|
+
it "accepts a String URI" do
|
19
|
+
broker = CukeQ::Broker.new("amqp://cukeq-master@localhost:1234/cukeq")
|
20
|
+
broker.user.should == "cukeq-master"
|
21
|
+
broker.host.should == "localhost"
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#start" do
|
25
|
+
it "starts AMQP with the given broker config" do
|
26
|
+
broker = CukeQ::Broker.new(URI.parse("amqp://cukeq-master@localhost:1234/cukeq"))
|
27
|
+
|
28
|
+
expected_params = {
|
29
|
+
:host => 'localhost',
|
30
|
+
:port => 1234,
|
31
|
+
:vhost => "/cukeq",
|
32
|
+
:user => 'cukeq-master'
|
33
|
+
}
|
34
|
+
|
35
|
+
AMQP.should_receive(:start).with(hash_including(expected_params)).and_yield
|
36
|
+
|
37
|
+
mock_q = mock("queue")
|
38
|
+
MQ.should_receive(:new).exactly(4).times.and_return(mock_q)
|
39
|
+
mock_q.should_receive(:queue).exactly(4).times
|
40
|
+
|
41
|
+
broker.start
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should publish messages on the given queue" do
|
46
|
+
broker = running_broker
|
47
|
+
message = "some message"
|
48
|
+
|
49
|
+
queues.each do |name, queue|
|
50
|
+
queue.should_receive(:publish).with(message)
|
51
|
+
broker.publish(name, message)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should subscribe/unsubscribe from the given queue" do
|
56
|
+
EM.stub(:add_periodic_timer)
|
57
|
+
broker = running_broker
|
58
|
+
|
59
|
+
# TODO: this looks pretty stupid, could we just expose the queues directly?
|
60
|
+
queues[:results].should_receive(:subscribe)
|
61
|
+
broker.subscribe(:results) {}
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#queue_for" do
|
65
|
+
it "should return the right queue" do
|
66
|
+
broker = running_broker
|
67
|
+
broker.queue_for(:results).should == queues[:results]
|
68
|
+
broker.queue_for(:jobs).should == queues[:jobs]
|
69
|
+
end
|
70
|
+
|
71
|
+
it "raises an error if the queue is not found" do
|
72
|
+
broker = running_broker
|
73
|
+
lambda { broker.queue_for(:foo) }.should raise_error
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
end
|