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