cukeq 0.0.1.dev

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.autotest +1 -0
  2. data/.document +5 -0
  3. data/.gitignore +21 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +73 -0
  6. data/Rakefile +68 -0
  7. data/VERSION +1 -0
  8. data/bin/cukeq +34 -0
  9. data/features/cukeq.feature +15 -0
  10. data/features/example1.feature +1 -0
  11. data/features/example2.feature +1 -0
  12. data/features/step_definitions/cukeq_steps.rb +22 -0
  13. data/features/support/cukeq_helper.rb +73 -0
  14. data/features/support/env.rb +13 -0
  15. data/features/support/report_app.rb +36 -0
  16. data/lib/cukeq.rb +45 -0
  17. data/lib/cukeq/broker.rb +65 -0
  18. data/lib/cukeq/em/system3.rb +53 -0
  19. data/lib/cukeq/job_clearer.rb +19 -0
  20. data/lib/cukeq/master.rb +146 -0
  21. data/lib/cukeq/reporter.rb +22 -0
  22. data/lib/cukeq/runner.rb +63 -0
  23. data/lib/cukeq/scenario_exploder.rb +32 -0
  24. data/lib/cukeq/scenario_runner.rb +134 -0
  25. data/lib/cukeq/scm.rb +51 -0
  26. data/lib/cukeq/scm/git_bridge.rb +36 -0
  27. data/lib/cukeq/scm/shell_svn_bridge.rb +65 -0
  28. data/lib/cukeq/scm/svn_bridge.rb +77 -0
  29. data/lib/cukeq/slave.rb +114 -0
  30. data/lib/cukeq/webapp.rb +38 -0
  31. data/spec/cukeq/broker_spec.rb +78 -0
  32. data/spec/cukeq/cukeq_spec.rb +10 -0
  33. data/spec/cukeq/master_spec.rb +153 -0
  34. data/spec/cukeq/reporter_spec.rb +29 -0
  35. data/spec/cukeq/runner_spec.rb +11 -0
  36. data/spec/cukeq/scenario_exploder_spec.rb +17 -0
  37. data/spec/cukeq/scenario_runner_spec.rb +32 -0
  38. data/spec/cukeq/scm/git_bridge_spec.rb +52 -0
  39. data/spec/cukeq/scm/svn_bridge_spec.rb +5 -0
  40. data/spec/cukeq/scm_spec.rb +48 -0
  41. data/spec/cukeq/slave_spec.rb +86 -0
  42. data/spec/cukeq/webapp_spec.rb +37 -0
  43. data/spec/spec.opts +1 -0
  44. data/spec/spec_helper.rb +12 -0
  45. 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
@@ -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
@@ -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