joshuapinter-cijoe 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ class CIJoe
2
+ # An in memory queue used for maintaining an order list of requested
3
+ # builds.
4
+ class Queue
5
+ # enabled - determines whether builds should be queued or not.
6
+ def initialize(enabled, verbose=false)
7
+ @enabled = enabled
8
+ @verbose = verbose
9
+ @queue = []
10
+
11
+ log("Build queueing enabled") if enabled
12
+ end
13
+
14
+ # Public: Appends a branch to be built, unless it already exists
15
+ # within the queue.
16
+ #
17
+ # branch - the name of the branch to build or nil if the default
18
+ # should be built.
19
+ #
20
+ # Returns nothing
21
+ def append_unless_already_exists(branch)
22
+ return unless enabled?
23
+ unless @queue.include? branch
24
+ @queue << branch
25
+ log "#{Time.now.to_i}: Queueing #{branch}"
26
+ end
27
+ end
28
+
29
+ # Returns a String of the next branch to build
30
+ def next_branch_to_build
31
+ branch = @queue.shift
32
+ log "#{Time.now.to_i}: De-queueing #{branch}"
33
+ branch
34
+ end
35
+
36
+ # Returns true if there are requested builds waiting and false
37
+ # otherwise.
38
+ def waiting?
39
+ if enabled?
40
+ not @queue.empty?
41
+ else
42
+ false
43
+ end
44
+ end
45
+
46
+ protected
47
+ def log(msg)
48
+ puts msg if @verbose
49
+ end
50
+
51
+ def enabled?
52
+ @enabled
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,114 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+ require 'json'
4
+
5
+ class CIJoe
6
+ class Server < Sinatra::Base
7
+ attr_reader :joe
8
+
9
+ dir = File.dirname(File.expand_path(__FILE__))
10
+
11
+ set :views, "#{dir}/views"
12
+ set :public_folder, "#{dir}/public"
13
+ set :static, true
14
+ set :lock, true
15
+
16
+ before { joe.restore }
17
+
18
+ get '/ping' do
19
+ if joe.building? || !joe.last_build || !joe.last_build.worked?
20
+ halt 412, (joe.building? || joe.last_build.nil?) ? "building" : joe.last_build.sha
21
+ end
22
+
23
+ joe.last_build.sha
24
+ end
25
+
26
+ get '/?' do
27
+ erb(:template, {}, :joe => joe)
28
+ end
29
+
30
+ post '/?' do
31
+ unless params[:rebuild]
32
+ payload = JSON.parse(params[:payload])
33
+ pushed_branch = payload["ref"].split('/').last
34
+ end
35
+
36
+ # Only build if we were given an explicit branch via `?branch=blah`
37
+ # or the payload exists and the "ref" property matches our
38
+ # specified build branch.
39
+ if params[:branch] || params[:rebuild] || pushed_branch == joe.git_branch
40
+ joe.build(params[:branch])
41
+ end
42
+
43
+ redirect request.path
44
+ end
45
+
46
+ get '/api/json' do
47
+ response = [200, {'Content-Type' => 'application/json'}]
48
+ response_json = erb(:json, {}, :joe => joe)
49
+ if params[:jsonp]
50
+ response << params[:jsonp] + '(' + response_json + ')'
51
+ else
52
+ response << response_json
53
+ end
54
+ response
55
+ end
56
+
57
+
58
+ helpers do
59
+ include Rack::Utils
60
+ alias_method :h, :escape_html
61
+
62
+ # thanks integrity!
63
+ def ansi_color_codes(string)
64
+ string.gsub("\e[0m", '</span>').
65
+ gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
66
+ end
67
+
68
+ def pretty_time(time)
69
+ time.strftime("%Y-%m-%d %H:%M")
70
+ end
71
+
72
+ def cijoe_root
73
+ root = request.path
74
+ root = "" if root == "/"
75
+ root
76
+ end
77
+ end
78
+
79
+ def initialize(*args)
80
+ super
81
+ check_project
82
+ @joe = CIJoe.new(options.project_path)
83
+ end
84
+
85
+ def self.start(host, port, project_path)
86
+ set :project_path, project_path
87
+ CIJoe::Server.run! :host => host, :port => port
88
+ end
89
+
90
+ def self.rack_start(project_path)
91
+ set :project_path, project_path
92
+ self.new
93
+ end
94
+
95
+ def self.project_path=(project_path)
96
+ user, pass = Config.cijoe(project_path).user.to_s, Config.cijoe(project_path).pass.to_s
97
+ if user != '' && pass != ''
98
+ use Rack::Auth::Basic do |username, password|
99
+ [ username, password ] == [ user, pass ]
100
+ end
101
+ puts "Using HTTP basic auth"
102
+ end
103
+ set :project_path, Proc.new{project_path}, true
104
+ end
105
+
106
+ def check_project
107
+ if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
108
+ puts "Whoops! I need the path to a Git repo."
109
+ puts " $ git clone git@github.com:username/project.git project"
110
+ abort " $ cijoe project"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ class CIJoe
2
+ Version = VERSION = "0.9.3"
3
+ end
@@ -0,0 +1,16 @@
1
+ { "jobs": [
2
+ <% if joe.last_build %>
3
+ {"name":"<%= joe.project %>",
4
+ "url":"<%= joe.url %>",
5
+ "color":"<%= joe.last_build.status.to_s == "failed" ? 'red' : 'blue' %>",
6
+ "status":"<%= joe.last_build.status %>",
7
+ "started_at":"<%= pretty_time(joe.last_build.started_at) %>",
8
+ "finished_at":"<%= pretty_time(joe.last_build.finished_at) %>",
9
+ "duration":"<%= joe.last_build.duration if joe.last_build.duration %>",
10
+ "sha":"<%= joe.last_build.sha %>",
11
+ "short_sha":"<%= joe.last_build.short_sha %>",
12
+ "commit_url":"<%= joe.last_build.commit.url if joe.last_build.commit %>",
13
+ "branch":"<%= joe.last_build.branch %>"
14
+ }
15
+ <% end %>
16
+ ]}
@@ -0,0 +1,74 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link href="<%= cijoe_root %>/screen.css" media="screen" rel="stylesheet" type="text/css" />
5
+ <link rel="shortcut icon" href="<%= cijoe_root %>/favicon.ico" type="image/x-icon" />
6
+ <title><%= h(joe.project) %>: CI Joe</title>
7
+ </head>
8
+ <body>
9
+ <div class="site">
10
+ <div class="title">
11
+ <a href="<%= cijoe_root %>/">CI Joe</a>
12
+ <span class="extra">because knowing is half the battle</span>
13
+ </div>
14
+
15
+ <div id="home">
16
+ <h1><a href="<%= joe.url %>"><%= joe.project %></a></h1>
17
+ <ul class="posts">
18
+ <% if joe.current_build %>
19
+ <li>
20
+ <span class="date"><%= pretty_time(joe.current_build.started_at) if joe.current_build %></span> &raquo;
21
+ <% if joe.current_build.sha %>
22
+ Building <%= joe.current_build.branch %> at <a href="<%= joe.current_build.commit.url %>"><%= joe.current_build.short_sha %></a> <small>(pid: <%= joe.pid %>)</small>
23
+ <% else %>
24
+ Build starting...
25
+ <% end %>
26
+ </li>
27
+ <% else %>
28
+ <li><form method="POST"><input type="hidden", name="rebuild" value="true"><input type="submit" value="Build"/></form></li>
29
+ <% end %>
30
+
31
+ <% if joe.last_build %>
32
+ <li>
33
+ <span class="date"><%= pretty_time(joe.last_build.finished_at) %></span> &raquo;
34
+ <% if joe.last_build.sha %>
35
+ Built <%= joe.last_build.branch %> at <a href="<%= joe.last_build.commit.url %>"><%= joe.last_build.short_sha %></a>
36
+ <% end %>
37
+ <span class="<%= joe.last_build.status %>">(<%= joe.last_build.status %>)</span>
38
+ <% if joe.last_build.duration %>
39
+ in <span class="duration"><%= joe.last_build.duration %></span> seconds.
40
+ <% end %>
41
+ </li>
42
+ <% if joe.last_build.failed? %>
43
+ <li><pre class="terminal"><code><%=ansi_color_codes h(joe.last_build.output) %></code></pre></li>
44
+ <% end %>
45
+ <% end %>
46
+ </ul>
47
+ </div>
48
+
49
+ <div class="footer">
50
+ <div class="contact">
51
+ <p>
52
+ <a href="https://github.com/defunkt/cijoe#readme">Documentation</a><br/>
53
+ <a href="https://github.com/defunkt/cijoe">Source</a><br/>
54
+ <a href="https://github.com/defunkt/cijoe/issues">Issues</a><br/>
55
+ <a href="https://github.com/defunkt/cijoe/tree/v<%= CIJoe::VERSION %>">v<%= CIJoe::VERSION %></a>
56
+ </p>
57
+ </div>
58
+ <div class="contact">
59
+ <p>
60
+ Designed by <a href="http://tom.preston-werner.com/">Tom Preston-Werner</a><br/>
61
+ Influenced by <a href="http://integrityapp.com/">Integrity</a><br/>
62
+ Built with <a href="http://sinatrarb.com/">Sinatra</a><br/>
63
+ Keep it simple, Sam.
64
+ </p>
65
+ </div>
66
+ <div class="rss">
67
+ <a href="http://github.com/defunkt/cijoe">
68
+ <img src="<%= cijoe_root %>/octocat.png" alt="Octocat!" />
69
+ </a>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </body>
74
+ </html>
data/lib/cijoe.rb ADDED
@@ -0,0 +1,225 @@
1
+ ##
2
+ # CI Joe.
3
+ # Because knowing is half the battle.
4
+ #
5
+ # This is a stupid simple CI server. It can build one (1)
6
+ # git-based project only.
7
+ #
8
+ # It only remembers the last build.
9
+ #
10
+ # It only notifies to Campfire.
11
+ #
12
+ # It's a RAH (Real American Hero).
13
+ #
14
+ # Seriously, I'm gonna be nuts about keeping this simple.
15
+
16
+ require 'cijoe/version'
17
+ require 'cijoe/config'
18
+ require 'cijoe/commit'
19
+ require 'cijoe/build'
20
+ require 'cijoe/campfire'
21
+ require 'cijoe/server'
22
+ require 'cijoe/queue'
23
+
24
+ class CIJoe
25
+ attr_reader :user, :project, :url, :current_build, :last_build, :campfire
26
+
27
+ def initialize(project_path)
28
+ @project_path = File.expand_path(project_path)
29
+
30
+ @user, @project = git_user_and_project
31
+ @url = "http://github.com/#{@user}/#{@project}"
32
+
33
+ @campfire = CIJoe::Campfire.new(project_path)
34
+
35
+ @last_build = nil
36
+ @current_build = nil
37
+ @queue = Queue.new(!repo_config.buildqueue.to_s.empty?, true)
38
+
39
+ trap("INT") { stop }
40
+ end
41
+
42
+ # is a build running?
43
+ def building?
44
+ !!@current_build
45
+ end
46
+
47
+ # the pid of the running child process
48
+ def pid
49
+ building? and current_build.pid
50
+ end
51
+
52
+ # kill the child and exit
53
+ def stop
54
+ Process.kill(9, pid) if pid
55
+ exit!
56
+ end
57
+
58
+ # build callbacks
59
+ def build_failed(output, error)
60
+ finish_build :failed, "#{error}\n\n#{output}"
61
+ run_hook "build-failed"
62
+ end
63
+
64
+ def build_worked(output)
65
+ finish_build :worked, output
66
+ run_hook "build-worked"
67
+ end
68
+
69
+ def finish_build(status, output)
70
+ @current_build.finished_at = Time.now
71
+ @current_build.status = status
72
+ @current_build.output = output
73
+ @last_build = @current_build
74
+
75
+ @current_build = nil
76
+ write_build 'current', @current_build
77
+ write_build 'last', @last_build
78
+ @campfire.notify(@last_build) if @campfire.valid?
79
+
80
+ build(@queue.next_branch_to_build) if @queue.waiting?
81
+ end
82
+
83
+ # run the build but make sure only one is running
84
+ # at a time (if new one comes in we will park it)
85
+ def build(branch=nil)
86
+ if building?
87
+ @queue.append_unless_already_exists(branch)
88
+ # leave anyway because a current build runs
89
+ return
90
+ end
91
+ @current_build = Build.new(@project_path, @user, @project)
92
+ write_build 'current', @current_build
93
+ Thread.new { build!(branch) }
94
+ end
95
+
96
+ def open_pipe(cmd)
97
+ read, write = IO.pipe
98
+
99
+ pid = fork do
100
+ read.close
101
+ $stdout.reopen write
102
+ exec cmd
103
+ end
104
+
105
+ write.close
106
+
107
+ yield read, pid
108
+ end
109
+
110
+ # update git then run the build
111
+ def build!(branch=nil)
112
+ @git_branch = branch
113
+ build = @current_build
114
+ output = ''
115
+ git_update
116
+ build.sha = git_sha
117
+ build.branch = git_branch
118
+ write_build 'current', build
119
+
120
+ open_pipe("cd \"#{@project_path}\" && #{runner_command} 2>&1") do |pipe, pid|
121
+ puts "#{Time.now.to_i}: Building #{build.branch} at #{build.short_sha}: pid=#{pid}"
122
+
123
+ build.pid = pid
124
+ write_build 'current', build
125
+ output = pipe.read
126
+ end
127
+
128
+ Process.waitpid(build.pid, 1)
129
+ status = $?.exitstatus.to_i
130
+ @current_build = build
131
+ puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
132
+
133
+ status == 0 ? build_worked(output) : build_failed('', output)
134
+ rescue Object => e
135
+ puts "Exception building: #{e.message} (#{e.class})"
136
+ build_failed('', e.to_s)
137
+ end
138
+
139
+ # shellin' out
140
+ def runner_command
141
+ runner = repo_config.runner.to_s
142
+ runner == '' ? "rake -s test:units" : runner
143
+ end
144
+
145
+ def git_sha
146
+ `cd \"#{@project_path}\" && git rev-parse origin/#{git_branch}`.chomp
147
+ end
148
+
149
+ def git_update
150
+ `cd \"#{@project_path}\" && git fetch origin && git reset --hard origin/#{git_branch}`
151
+ run_hook "after-reset"
152
+ end
153
+
154
+ def git_user_and_project
155
+ Config.remote(@project_path).origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
156
+ end
157
+
158
+ def git_branch
159
+ return @git_branch if @git_branch
160
+ branch = repo_config.branch.to_s
161
+ @git_branch = branch == '' ? "master" : branch
162
+ end
163
+
164
+ # massage our repo
165
+ def run_hook(hook)
166
+ if File.exists?(file=path_in_project(".git/hooks/#{hook}")) && File.executable?(file)
167
+ data =
168
+ if @last_build && @last_build.commit
169
+ {
170
+ "MESSAGE" => @last_build.commit.message,
171
+ "AUTHOR" => @last_build.commit.author,
172
+ "SHA" => @last_build.commit.sha,
173
+ "OUTPUT" => @last_build.env_output
174
+ }
175
+ else
176
+ {}
177
+ end
178
+
179
+ orig_ENV = ENV.to_hash
180
+ ENV.clear
181
+ data.each{ |k, v| ENV[k] = v }
182
+ output = `cd \"#{@project_path}\" && sh #{file}`
183
+
184
+ ENV.clear
185
+ orig_ENV.to_hash.each{ |k, v| ENV[k] = v}
186
+ output
187
+ end
188
+ end
189
+
190
+ # restore current / last build state from disk.
191
+ def restore
192
+ @last_build = read_build('last')
193
+ @current_build = read_build('current')
194
+
195
+ Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
196
+ rescue Errno::ESRCH
197
+ # build pid isn't running anymore. assume previous
198
+ # server died and reset.
199
+ @current_build = nil
200
+ end
201
+
202
+ def path_in_project(path)
203
+ File.join(@project_path, path)
204
+ end
205
+
206
+ # write build info for build to file.
207
+ def write_build(name, build)
208
+ filename = path_in_project(".git/builds/#{name}")
209
+ Dir.mkdir path_in_project('.git/builds') unless File.directory?(path_in_project('.git/builds'))
210
+ if build
211
+ build.dump filename
212
+ elsif File.exist?(filename)
213
+ File.unlink filename
214
+ end
215
+ end
216
+
217
+ def repo_config
218
+ Config.cijoe(@project_path)
219
+ end
220
+
221
+ # load build info from file.
222
+ def read_build(name)
223
+ Build.load(path_in_project(".git/builds/#{name}"), @project_path)
224
+ end
225
+ end
@@ -0,0 +1,52 @@
1
+ {
2
+ "after": "416cb2f7105e7f989bc223d1a975b93a6491b276",
3
+ "before": "0ae4da1eea2d191b33ebfbd5db48e3c1f91953ad",
4
+ "commits": [
5
+ {
6
+ "added": [
7
+
8
+ ],
9
+ "author": {
10
+ "email": "joshua.owens@gmail.com",
11
+ "name": "Josh Owens",
12
+ "username": "queso"
13
+ },
14
+ "id": "09293a1703b3bdb36aba70d38abd5e44396c50a5",
15
+ "message": "Update README",
16
+ "modified": [
17
+ "README.textile"
18
+ ],
19
+ "removed": [
20
+
21
+ ],
22
+ "timestamp": "2011-02-07T15:27:35-08:00",
23
+ "url": "https:\/\/github.com\/fourbeansoup\/broth\/commit\/09293a1703b3bdb36aba70d38abd5e44396c50a5"
24
+ }
25
+ ],
26
+ "compare": "https:\/\/github.com\/fourbeansoup\/broth\/compare\/0ae4da1...416cb2f",
27
+ "forced": false,
28
+ "ref": "refs\/heads\/master",
29
+ "repository": {
30
+ "created_at": "2009\/12\/05 10:44:57 -0800",
31
+ "description": "FourBeanSoup's Broth Application, a tasty starting point for every app",
32
+ "fork": true,
33
+ "forks": 4,
34
+ "has_downloads": true,
35
+ "has_issues": true,
36
+ "has_wiki": true,
37
+ "homepage": "",
38
+ "language": "JavaScript",
39
+ "name": "broth",
40
+ "open_issues": 2,
41
+ "organization": "fourbeansoup",
42
+ "owner": {
43
+ "email": "josh+scm@fourbeansoup.com",
44
+ "name": "fourbeansoup"
45
+ },
46
+ "private": false,
47
+ "pushed_at": "2011\/02\/07 17:17:00 -0800",
48
+ "size": 604,
49
+ "url": "https:\/\/github.com\/fourbeansoup\/broth",
50
+ "watchers": 10
51
+ }
52
+ }
data/test/helper.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+
5
+ ENV['RACK_ENV'] = 'test'
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+ require 'cijoe'
10
+
11
+ CIJoe::Server.set :project_path, "."
12
+ CIJoe::Server.set :environment, "test"
13
+
14
+ TMP_DIR = '/tmp/cijoe_test'
15
+
16
+ def tmp_dir
17
+ TMP_DIR
18
+ end
19
+
20
+ def setup_git_info(options = {})
21
+ @tmp_dirs ||= []
22
+ @tmp_dirs += [options[:tmp_dir]]
23
+ create_tmpdir!(options[:tmp_dir])
24
+ dir = options[:tmp_dir] || tmp_dir
25
+ `cd #{dir} && git init`
26
+ options[:config].each do |key, value|
27
+ `cd #{dir} && git config --add #{key} "#{value}"`
28
+ end
29
+ end
30
+
31
+ def teardown_git_info
32
+ remove_tmpdir!
33
+ @tmp_dirs.each do |dir|
34
+ remove_tmpdir!(dir)
35
+ end
36
+ end
37
+
38
+ def remove_tmpdir!(passed_dir = nil)
39
+ FileUtils.rm_rf(passed_dir || tmp_dir)
40
+ end
41
+
42
+ def create_tmpdir!(passed_dir = nil)
43
+ FileUtils.mkdir_p(passed_dir || tmp_dir)
44
+ end
45
+
46
+ class Test::Unit::TestCase
47
+ end
@@ -0,0 +1,48 @@
1
+ require "helper"
2
+ require "cijoe"
3
+ require "fakefs/safe"
4
+
5
+
6
+ class TestCampfire < Test::Unit::TestCase
7
+
8
+ class ::CIJoe
9
+ attr_writer :current_build, :last_build
10
+ end
11
+
12
+ attr_accessor :app
13
+
14
+ def setup
15
+ @app = CIJoe::Server.new
16
+ joe = @app.joe
17
+
18
+ # make Build#restore a no-op so we don't overwrite our current/last
19
+ # build attributes set from tests.
20
+ def joe.restore
21
+ end
22
+
23
+ # make CIJoe#build! and CIJoe#git_update a no-op so we don't overwrite our local changes
24
+ # or local commits nor should we run tests.
25
+ def joe.build!
26
+ end
27
+ end
28
+
29
+ def teardown
30
+ teardown_git_info
31
+ end
32
+
33
+ def test_campfire_pulls_campfire_config_from_git_config
34
+ setup_git_info(:config => {"campfire.subdomain" => "github", "remote.origin.url" => "https://github.com/defunkt/cijoe.git"})
35
+ cf = CIJoe::Campfire.new(tmp_dir)
36
+ assert_equal "github", cf.campfire_config[:subdomain]
37
+ end
38
+
39
+ def test_campfire_pulls_campfire_config_from_its_own_git_config
40
+ setup_git_info(:config => {"campfire.subdomain" => "github"})
41
+ setup_git_info(:config => {"campfire.subdomain" => "37signals"}, :tmp_dir => "/tmp/cijoe_test_37signals")
42
+ cf1 = CIJoe::Campfire.new(tmp_dir)
43
+ cf2 = CIJoe::Campfire.new("/tmp/cijoe_test_37signals")
44
+ assert_equal "github", cf1.campfire_config[:subdomain]
45
+ assert_equal "37signals", cf2.campfire_config[:subdomain]
46
+ end
47
+
48
+ end
@@ -0,0 +1,17 @@
1
+ require 'helper'
2
+
3
+ class TestCIJoe < Test::Unit::TestCase
4
+ def test_raise_error_on_invalid_command
5
+ assert_raise RuntimeError, LoadError do
6
+ CIJoe::Config.new('--invalid').to_s
7
+ end
8
+ end
9
+
10
+ def test_return_value_of_config
11
+ assert_equal `git config blame`.chomp, CIJoe::Config.new('blame').to_s
12
+ end
13
+
14
+ def test_return_empty_string_when_config_does_not_exist
15
+ assert_equal '', CIJoe::Config.new('invalid').to_s
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ require 'helper'
2
+
3
+ class TestCIJoeQueue < Test::Unit::TestCase
4
+ def test_a_disabled_queue
5
+ subject = CIJoe::Queue.new(false)
6
+ subject.append_unless_already_exists("test")
7
+ assert_equal false, subject.waiting?
8
+ end
9
+
10
+ def test_adding_two_items_to_a_queue
11
+ subject = CIJoe::Queue.new(true)
12
+ subject.append_unless_already_exists("test")
13
+ subject.append_unless_already_exists(nil)
14
+ assert_equal true, subject.waiting?
15
+ assert_equal "test", subject.next_branch_to_build
16
+ assert_equal nil, subject.next_branch_to_build
17
+ assert_equal false, subject.waiting?
18
+ end
19
+
20
+ def test_adding_two_duplicate_items_to_a_queue
21
+ subject = CIJoe::Queue.new(true)
22
+ subject.append_unless_already_exists("test")
23
+ subject.append_unless_already_exists("test")
24
+ assert_equal true, subject.waiting?
25
+ assert_equal "test", subject.next_branch_to_build
26
+ assert_equal false, subject.waiting?
27
+ end
28
+ end