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