integrity-bob 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ dist/*
3
+ tmp/*
4
+ test/tmp/*
5
+ doc/*
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2008-2009 Nicolas Sanguinetti, entp.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,30 @@
1
+ = Bob the Builder
2
+
3
+ Given a Buildable object with a determined API (described in it's documentation),
4
+ Bob will, when called like:
5
+
6
+ Bob.build(buildable, commit_id) # or Bob.build(buildable, [commit_id, commit_id, ...])
7
+
8
+ or from your buildable (if you are using the Bob::Buildable mixin provided) as:
9
+
10
+ buildable.build(commit_id) # or buildable.build([commit_id, commit_id, ...])
11
+
12
+ 1. Checkout the buildable on the specified commit
13
+ 2. Call <tt>Buildable#start_building</tt>
14
+ 3. Run the script provided in <tt>Buildable#build_script</tt> in the buildable.
15
+ 4. When the build process finishes, it will call <tt>Buildable#finish_building</tt> with
16
+ the commit_id, the build status (true if the script returns a status code
17
+ of 0, false otherwise), and a string with the build output (both STDOUT and STDERR).
18
+
19
+ If you pass an array of commits, the steps 1-4 will be repeated for each commit provided,
20
+ in order.
21
+
22
+ == Do I need this?
23
+
24
+ Probably not. Check out integrity[http://integrityapp.com] for a full fledged
25
+ automated CI server, which is what most people need.
26
+
27
+ == Credits
28
+
29
+ Authors:: Nicolas Sanguinetti (foca[http://github.com/foca]) and Simon Rozet (sr[http://github.com/sr])
30
+ License:: MIT (Check LICENSE for details)
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require "rake/testtask"
2
+
3
+ rdoc_sources = %w(hanna/rdoctask rdoc/task rake/rdoctask)
4
+ begin
5
+ require rdoc_sources.shift
6
+ rescue LoadError
7
+ retry
8
+ end
9
+
10
+ begin
11
+ require "metric_fu" if RUBY_VERSION < "1.9"
12
+ rescue LoadError
13
+ end
14
+
15
+ begin
16
+ require "mg"
17
+ MG.new("bob.gemspec")
18
+ rescue LoadError
19
+ end
20
+
21
+ desc "Default: run all tests"
22
+ task :default => :test
23
+
24
+ SCMs = %w[git svn]
25
+
26
+ desc "Run unit tests"
27
+ task :test => SCMs.map { |scm| "test:#{scm}" } do
28
+ ruby "test/bob_test.rb"
29
+ ruby "test/engine/threaded_test.rb"
30
+ end
31
+
32
+ SCMs.each { |scm|
33
+ desc "Run unit tests with #{scm}"
34
+ task "test:#{scm}" do
35
+ ruby "test/scm/#{scm}_test.rb"
36
+ end
37
+ }
38
+
39
+ (defined?(RDoc::Task) ? RDoc::Task : Rake::RDocTask).new do |rd|
40
+ rd.main = "README.rdoc"
41
+ rd.title = "Documentation for Bob the Builder"
42
+ rd.rdoc_files.include("README.rdoc", "LICENSE", "lib/**/*.rb")
43
+ rd.rdoc_dir = "doc"
44
+ end
data/bob.gemspec ADDED
@@ -0,0 +1,51 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "bob"
3
+ s.version = "0.2.0"
4
+ s.date = "2009-07-02"
5
+
6
+ s.description = "Bob the Builder will build your code. Simple."
7
+ s.summary = "Bob builds!"
8
+ s.homepage = "http://integrityapp.com"
9
+
10
+ s.authors = ["Nicolás Sanguinetti", "Simon Rozet"]
11
+ s.email = "info@integrityapp.com"
12
+
13
+ s.require_paths = ["lib"]
14
+ s.rubyforge_project = "integrity"
15
+ s.has_rdoc = true
16
+ s.rubygems_version = "1.3.1"
17
+
18
+ s.add_dependency "addressable"
19
+
20
+ if s.respond_to?(:add_development_dependency)
21
+ s.add_development_dependency "sr-mg"
22
+ s.add_development_dependency "sr-bob-test"
23
+ s.add_development_dependency "contest"
24
+ s.add_development_dependency "redgreen"
25
+ s.add_development_dependency "ruby-debug"
26
+ end
27
+
28
+ s.files = %w[
29
+ .gitignore
30
+ LICENSE
31
+ README.rdoc
32
+ Rakefile
33
+ bob.gemspec
34
+ lib/bob.rb
35
+ lib/bob/buildable.rb
36
+ lib/bob/builder.rb
37
+ lib/bob/engine.rb
38
+ lib/bob/engine/foreground.rb
39
+ lib/bob/engine/threaded.rb
40
+ lib/bob/scm.rb
41
+ lib/bob/scm/abstract.rb
42
+ lib/bob/scm/git.rb
43
+ lib/bob/scm/svn.rb
44
+ lib/core_ext/object.rb
45
+ test/bob_test.rb
46
+ test/engine/threaded_test.rb
47
+ test/helper.rb
48
+ test/scm/git_test.rb
49
+ test/scm/svn_test.rb
50
+ ]
51
+ end
@@ -0,0 +1,70 @@
1
+ module Bob
2
+ # Mixin to add to your classes.
3
+ module Buildable
4
+ # Builds the list of commits you pass. You must provide an
5
+ # enumerable with commit identifiers appropriate to the
6
+ # repository the code is in (SHAs if git, rev numbers if svn,
7
+ # etc), or a single string with a commit id.
8
+ def build(commits)
9
+ Bob.build(self, commits)
10
+ end
11
+
12
+ # What kind of repository this buildable represents. Must
13
+ # return a Symbol (:git, :svn, etc.)
14
+ #
15
+ # <b>You must implement this in the classes where you mixin this module</b>
16
+ def scm
17
+ raise NotImplementedError
18
+ end
19
+
20
+ # Full URI to the repository to clone/checkout.
21
+ #
22
+ # <b>You must implement this in the classes where you mixin this module</b>
23
+ def uri
24
+ raise NotImplementedError
25
+ end
26
+
27
+ # Branch of the code you want to watch in order to build.
28
+ #
29
+ # <b>You must implement this in the classes where you mixin this module</b>
30
+ def branch
31
+ raise NotImplementedError
32
+ end
33
+
34
+ # Script that will be run in the buildable's checked out code,
35
+ # if it returns a status code of 0 it will be considered a
36
+ # successfull build. Else it will be considered a failed build.
37
+ #
38
+ # <b>You must implement this in the classes where you mixin this module</b>
39
+ def build_script
40
+ raise NotImplementedError
41
+ end
42
+
43
+ # Callback sent when a build starts. The first argument is a
44
+ # string with whatever identifier is appropriate for a repository
45
+ # of this kind. The second is a hash with information about the
46
+ # commit.
47
+ #
48
+ # <tt>:author</tt>:: A string with the name/email of the committer
49
+ # <tt>:message</tt>:: The commit message
50
+ # <tt>:committed_at</tt>:: A Time object with the timestamp of the
51
+ # commit
52
+ #
53
+ # <b>You must implement this in the classes where you mixin this module</b>
54
+ def start_building(commit_id, commit_info)
55
+ raise NotImplementedError
56
+ end
57
+
58
+ # Callback sent after a build finishes. The first argument is a
59
+ # string with whatever identifier is appropriate for a respository
60
+ # of this kind. The second is a boolean which is true if the build
61
+ # was successful or false if it failed. And the last one is a string
62
+ # with the full output returned by the build process (STDOUT and
63
+ # STDERR interleaved)
64
+ #
65
+ # <b>You must implement this in the classes where you mixin this module</b>
66
+ def finish_building(commit_id, build_status, build_output)
67
+ raise NotImplementedError
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,64 @@
1
+ module Bob
2
+ # A Builder will take care of building a buildable (wow, you didn't see that coming,
3
+ # right?).
4
+ class Builder
5
+ attr_reader :buildable
6
+
7
+ # Instantiate the Builder, passing an object that understands the <tt>Buildable</tt>
8
+ # interface, and a <tt>commit_id</tt>.
9
+ #
10
+ # You can pass <tt>:head</tt> as the commit id, in which case it will resolve to the
11
+ # head commit of the current branch (for example, "HEAD" under git, or the latest
12
+ # revision under svn)
13
+ def initialize(buildable, commit_id)
14
+ @buildable = buildable
15
+ @commit_id = commit_id
16
+ end
17
+
18
+ # This is where the magic happens:
19
+ #
20
+ # 1. Check out the repo to the appropriate commit.
21
+ # 2. Notify the buildable that the build is starting.
22
+ # 3. Run the build script on it in the background.
23
+ # 4. Reports the build back to the buildable.
24
+ def build
25
+ Bob.logger.info "Building #{commit_id} of the #{buildable.scm} repo at #{buildable.uri}"
26
+
27
+ in_background do
28
+ scm.with_commit(commit_id) {
29
+ buildable.start_building(commit_id, scm.info(commit_id))
30
+ build_status, build_output = run_build_script
31
+ buildable.finish_building(commit_id, build_status, build_output)
32
+ }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def commit_id
39
+ @commit_id == :head ? scm.head : @commit_id
40
+ end
41
+
42
+ def run_build_script
43
+ build_output = nil
44
+
45
+ Bob.logger.debug("Running the build script for #{buildable.uri}")
46
+ IO.popen(build_script, "r") { |output| build_output = output.read }
47
+ Bob.logger.debug("Ran build script `#{build_script}` and got:\n#{build_output}")
48
+
49
+ [$?.success?, build_output]
50
+ end
51
+
52
+ def build_script
53
+ "(cd #{scm.working_dir} && #{buildable.build_script} 2>&1)"
54
+ end
55
+
56
+ def scm
57
+ @scm ||= SCM.new(buildable.scm, buildable.uri, buildable.branch)
58
+ end
59
+
60
+ def in_background(&block)
61
+ Bob.engine.call(block)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ module Bob
2
+ module Engine
3
+ # Dummy engine that just runs in the foreground (useful for tests).
4
+ Foreground = lambda {|b| b.call }
5
+ end
6
+ end
@@ -0,0 +1,138 @@
1
+ require "thread"
2
+
3
+ module Bob
4
+ module Engine
5
+ # A thread pool based build engine. This engine simply adds jobs to an
6
+ # in-memory queue, and processes them as soon as possible.
7
+ class Threaded
8
+ # The optional pool size controls how many threads will be created.
9
+ def initialize(pool_size = 2)
10
+ @pool = ThreadPool.new(pool_size)
11
+ end
12
+
13
+ # Adds a job to the queue.
14
+ def call(job)
15
+ @pool << job
16
+ end
17
+
18
+ # The number of jobs currently in the queue.
19
+ def njobs
20
+ @pool.njobs
21
+ end
22
+
23
+ # This method will not return until #njobs returns 0.
24
+ def wait!
25
+ Thread.pass until @pool.njobs == 0
26
+ end
27
+
28
+ # Manage a pool of threads, allowing for spin up / spin down of the
29
+ # contained threads.
30
+ # Simply processes work added to it's queue via #push.
31
+ # The default size for the pool is 2 threads.
32
+ class ThreadPool
33
+ # A thread safe single value for use as a counter.
34
+ class Incrementor
35
+ # The value passed in will be the initial value.
36
+ def initialize(v = 0)
37
+ @m = Mutex.new
38
+ @v = v
39
+ end
40
+
41
+ # Add the given value to self, default 1.
42
+ def inc(v = 1)
43
+ sync { @v += v }
44
+ end
45
+
46
+ # Subtract the given value to self, default 1.
47
+ def dec(v = 1)
48
+ sync { @v -= v }
49
+ end
50
+
51
+ # Simply shows the value inspect for convenience.
52
+ def inspect
53
+ @v.inspect
54
+ end
55
+
56
+ # Extract the value.
57
+ def to_i
58
+ @v
59
+ end
60
+
61
+ private
62
+
63
+ # Wrap the given block in a mutex.
64
+ def sync(&b)
65
+ @m.synchronize &b
66
+ end
67
+ end
68
+
69
+ # The number of threads in the pool.
70
+ attr_reader :size
71
+
72
+ # The job queue.
73
+ attr_reader :jobs
74
+
75
+ # Set the size of the thread pool. Asynchronously run down threads
76
+ # that are no longer required, and synchronously spawn new required
77
+ # threads.
78
+ def size=(other)
79
+ @size = other
80
+
81
+ if @workers.size > @size
82
+ (@workers.size - @size).times do
83
+ @workers.shift[:run] = false
84
+ end
85
+ else
86
+ (@size - @workers.size).times do
87
+ @workers << spawn
88
+ end
89
+ end
90
+ end
91
+
92
+ # Default pool size is 2 threads.
93
+ def initialize(size = nil)
94
+ size ||= 2
95
+ @jobs = Queue.new
96
+ @njobs = Incrementor.new
97
+ @workers = Array.new(size) { spawn }
98
+ end
99
+
100
+ # Adds a job to the queue, the job can be any number of objects
101
+ # responding to call, and/or a block.
102
+ def add(*jobs, &blk)
103
+ jobs = jobs + Array(blk)
104
+
105
+ jobs.each do |job|
106
+ @jobs << job
107
+ @njobs.inc
108
+ end
109
+ end
110
+
111
+ alias_method :push, :add
112
+ alias_method :<<, :add
113
+
114
+ # A peak at the number of jobs in the queue. N.B. May differ, but
115
+ # should be more accurate than +jobs.size+.
116
+ def njobs
117
+ @njobs.to_i
118
+ end
119
+
120
+ private
121
+
122
+ # Create a new thread and return it. The thread will run until the
123
+ # thread-local value +:run+ is changed to false or nil.
124
+ def spawn
125
+ Thread.new do
126
+ c = Thread.current
127
+ c[:run] = true
128
+
129
+ while c[:run]
130
+ @jobs.pop.call
131
+ @njobs.dec
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
data/lib/bob/engine.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Bob
2
+ # Different engines to run code in background. An engine is any object
3
+ # that responds to #call and takes a Proc object, which should be executed
4
+ # "in the background". The different engines are:
5
+ module Engine
6
+ autoload :Foreground, "bob/engine/foreground"
7
+ autoload :Threaded, "bob/engine/threaded"
8
+ end
9
+ end
@@ -0,0 +1,59 @@
1
+ module Bob
2
+ module SCM
3
+ class Abstract
4
+ attr_reader :uri, :branch
5
+
6
+ def initialize(uri, branch)
7
+ @uri = Addressable::URI.parse(uri)
8
+ @branch = branch
9
+ end
10
+
11
+ # Checkout the code into <tt>working_dir</tt> at the specified revision
12
+ # and call the passed block
13
+ def with_commit(commit_id)
14
+ update_code
15
+ checkout(commit_id)
16
+ yield
17
+ end
18
+
19
+ # Directory where the code will be checked out. Make sure the user
20
+ # running Bob is allowed to write to this directory (or you'll get a
21
+ # <tt>Errno::EACCESS</tt>)
22
+ def working_dir
23
+ @working_dir ||= "#{Bob.directory}/#{path}".tap { |dir|
24
+ FileUtils.mkdir_p(dir)
25
+ }
26
+ end
27
+
28
+ # Get some information about the specified commit. Returns a hash with:
29
+ #
30
+ # [<tt>:author</tt>] Commit author's name and email
31
+ # [<tt>:message</tt>] Commit message
32
+ # [<tt>:committed_at</tt>] Commit date (as a <tt>Time</tt> object)
33
+ def info(commit_id)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ # Return the identifier for the last commit in this branch of the
38
+ # repository.
39
+ def head
40
+ raise NotImplementedError
41
+ end
42
+
43
+ protected
44
+
45
+ def run(command, cd=true)
46
+ command = "(#{cd ? "cd #{working_dir} && " : ""}#{command} &>/dev/null)"
47
+ Bob.logger.debug(command)
48
+ system(command) || raise(Error, "Couldn't run SCM command `#{command}`")
49
+ end
50
+
51
+ def path
52
+ @path ||= "#{uri}-#{branch}".
53
+ gsub(/[^\w_ \-]+/i, '-'). # Remove unwanted chars.
54
+ gsub(/[ \-]+/i, '-'). # No more than one of the separator in a row.
55
+ gsub(/^\-|\-$/i, '') # Remove leading/trailing separator.
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ module Bob
2
+ module SCM
3
+ class Git < Abstract
4
+ def info(commit_id)
5
+ format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:committed_at: %ci%n)
6
+ YAML.load(`cd #{working_dir} && git show -s --pretty=format:"#{format}" #{commit_id}`).tap { |info|
7
+ info[:committed_at] = Time.parse(info[:committed_at])
8
+ }
9
+ end
10
+
11
+ def head
12
+ `git ls-remote --heads #{uri} #{branch} | cut -f1`.chomp
13
+ end
14
+
15
+ private
16
+
17
+ def update_code
18
+ cloned? ? fetch : clone
19
+ end
20
+
21
+ def cloned?
22
+ File.directory?("#{working_dir}/.git")
23
+ end
24
+
25
+ def clone
26
+ run "git clone #{uri} #{working_dir}", false
27
+ end
28
+
29
+ def fetch
30
+ git "fetch origin"
31
+ end
32
+
33
+ def checkout(commit_id)
34
+ # First checkout the branch just in case the commit_id
35
+ # turns out to be HEAD or other non-sha identifier
36
+ git "checkout origin/#{branch}"
37
+ git "reset --hard #{commit_id}"
38
+ end
39
+
40
+ def git(command)
41
+ run "git #{command}"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ require "bob/scm/abstract"
2
+
3
+ module Bob
4
+ module SCM
5
+ class Svn < Abstract
6
+ def info(revision)
7
+ dump = `svn log --non-interactive --revision #{revision} #{uri}`.split("\n")
8
+ meta = dump[1].split(" | ")
9
+
10
+ { :message => dump[3],
11
+ :author => meta[1],
12
+ :committed_at => Time.parse(meta[2]) }
13
+ end
14
+
15
+ def head
16
+ `svn info #{uri}`.split("\n").detect { |l| l =~ /^Revision: (\d+)/ }
17
+ $1.to_s
18
+ end
19
+
20
+ def with_commit(commit_id)
21
+ update_code
22
+ checkout(commit_id)
23
+ yield
24
+ end
25
+
26
+ private
27
+
28
+ def update_code
29
+ initial_checkout unless checked_out?
30
+ end
31
+
32
+ def checkout(revision)
33
+ run("svn up -q -r#{revision}")
34
+ end
35
+
36
+ def initial_checkout(revision=nil)
37
+ run("svn co -q #{uri} #{working_dir}")
38
+ end
39
+
40
+ def checked_out?
41
+ File.directory?(working_dir + "/.svn")
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/bob/scm.rb ADDED
@@ -0,0 +1,25 @@
1
+ require "bob/scm/abstract"
2
+
3
+ module Bob
4
+ module SCM
5
+ autoload :Git, "bob/scm/git"
6
+ autoload :Svn, "bob/scm/svn"
7
+
8
+ class Error < StandardError; end
9
+
10
+ # Factory to return appropriate SCM instances (according to repository kind)
11
+ def self.new(scm, uri, branch)
12
+ class_for(scm).new(uri, branch)
13
+ end
14
+
15
+ # A copy of Inflector.camelize, from ActiveSupport. It will convert
16
+ # string to UpperCamelCase.
17
+ def self.class_for(string)
18
+ class_name = string.to_s.
19
+ gsub(/\/(.?)/) { "::#{$1.upcase}" }.
20
+ gsub(/(?:^|_)(.)/) { $1.upcase }
21
+ const_get(class_name)
22
+ end
23
+ private_class_method :class_for
24
+ end
25
+ end
data/lib/bob.rb ADDED
@@ -0,0 +1,57 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+ require "logger"
4
+ require "time"
5
+ require "addressable/uri"
6
+
7
+ require "bob/buildable"
8
+ require "bob/builder"
9
+ require "bob/scm"
10
+ require "bob/engine"
11
+ require "core_ext/object"
12
+
13
+ module Bob
14
+ # Builds the specified <tt>buildable</tt>. This object must understand
15
+ # the API described in the README.
16
+ #
17
+ # The second argument will take an array of commit_ids, which should be
18
+ # strings with the relevant identifier (a SHA1 hash for git repositories,
19
+ # a numerical revision for svn repositories, etc).
20
+ #
21
+ # You can pass :head as a commit identifier to build the latest commit
22
+ # in the repo. Examples:
23
+ #
24
+ # Bob.build(buildable, :head) # just build the head
25
+ # Bob.build(buildable, ["4", "3", "2"]) # build revision 4, 3, and 2
26
+ # # (in that order)
27
+ # Bob.build(buildable, [:head, "a30fb12"]) # build the HEAD and a30fb12
28
+ # # commits in this repo.
29
+ def self.build(buildable, commit_ids)
30
+ Array(commit_ids).each do |commit_id|
31
+ Builder.new(buildable, commit_id).build
32
+ end
33
+ end
34
+
35
+ # Directory where the code for the different buildables will be checked out.
36
+ # Make sure the user running Bob is allowed to write to this directory.
37
+ def self.directory
38
+ @directory || "/tmp"
39
+ end
40
+
41
+ # What will you use to build in background. Must respond to <tt>call</tt> and
42
+ # take a block which will be run "in background". The default is to run in
43
+ # foreground.
44
+ def self.engine
45
+ @engine || Engine::Foreground
46
+ end
47
+
48
+ # What to log with (must implement ruby's Logger interface). Logs to STDOUT
49
+ # by default.
50
+ def self.logger
51
+ @logger || Logger.new(STDOUT)
52
+ end
53
+
54
+ class << self
55
+ attr_writer :directory, :engine, :logger
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ class Object #:nodoc:
2
+ # Add Object#tap for backwards compatibility
3
+ def tap
4
+ yield self
5
+ self
6
+ end unless method_defined?(:tap)
7
+ end
data/test/bob_test.rb ADDED
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + "/helper"
2
+
3
+ class BobTest < Test::Unit::TestCase
4
+ test "directory" do
5
+ Bob.directory = "/foo/bar"
6
+ assert_equal "/foo/bar", Bob.directory
7
+ end
8
+
9
+ test "logger" do
10
+ logger = Logger.new("/tmp/bob.log")
11
+ Bob.logger = logger
12
+
13
+ assert_same logger, Bob.logger
14
+ end
15
+
16
+ test "engine" do
17
+ engine = Object.new
18
+ Bob.engine = engine
19
+
20
+ assert_same engine, Bob.engine
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + "/../helper"
2
+
3
+ class ThreadedBobTest < Test::Unit::TestCase
4
+ def setup
5
+ super
6
+
7
+ @repo = GitRepo.new(:test_repo)
8
+ @repo.create
9
+
10
+ @buildable = BuildableStub.call(@repo)
11
+ end
12
+
13
+ test "with a successful threaded build" do
14
+ old_engine = Bob.engine
15
+
16
+ repo.add_successful_commit
17
+ commit_id = repo.commits.last[:identifier]
18
+
19
+ begin
20
+ Thread.abort_on_exception = true
21
+ Bob.engine = Bob::Engine::Threaded.new(5)
22
+ Bob.build(buildable, commit_id)
23
+ Bob.engine.wait!
24
+
25
+ status, output = buildable.builds[commit_id]
26
+ assert_equal :successful, status
27
+ assert_equal "Running tests...\n", output
28
+
29
+ commit = buildable.metadata[commit_id]
30
+ assert_equal "This commit will work", commit[:message]
31
+ assert_equal Time.now.min, commit[:committed_at].min
32
+ ensure
33
+ Bob.engine = old_engine
34
+ end
35
+ end
36
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "test/unit"
2
+ require "contest"
3
+ require "hpricot"
4
+
5
+ begin
6
+ require "redgreen"
7
+ require "ruby-debug"
8
+ rescue LoadError
9
+ end
10
+
11
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"),
12
+ File.expand_path(File.dirname(__FILE__) + "/../test/helper"))
13
+
14
+ require "bob"
15
+ require "bob/test"
16
+
17
+ class Test::Unit::TestCase
18
+ include Bob
19
+ include Bob::Test
20
+
21
+ attr_reader :repo, :buildable
22
+
23
+ def setup
24
+ Bob.logger = Logger.new("/dev/null")
25
+ Bob.engine = Bob::Engine::Foreground
26
+ Bob.directory = File.expand_path(File.dirname(__FILE__) + "/../tmp")
27
+
28
+ FileUtils.rm_rf(Bob.directory) if File.directory?(Bob.directory)
29
+ end
30
+ end
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + "/../helper"
2
+
3
+ class BobGitTest < Test::Unit::TestCase
4
+ def setup
5
+ super
6
+
7
+ @repo = GitRepo.new(:test_repo)
8
+ @repo.create
9
+
10
+ @buildable = BuildableStub.call(@repo)
11
+ end
12
+
13
+ def path(uri, branch="master")
14
+ SCM::Git.new(uri, branch).__send__(:path)
15
+ end
16
+
17
+ test "converts repo uri into a path" do
18
+ assert_equal "git-github-com-integrity-bob-master",
19
+ path("git://github.com/integrity/bob")
20
+ assert_equal "git-example-org-foo-repo-master",
21
+ path("git@example.org:~foo/repo")
22
+ assert_equal "tmp-repo-git-master", path("/tmp/repo.git")
23
+ assert_equal "tmp-repo-git-foo", path("/tmp/repo.git", "foo")
24
+ end
25
+
26
+ test "with a successful build" do
27
+ repo.add_successful_commit
28
+
29
+ commit_id = repo.commits.last[:identifier]
30
+
31
+ buildable.build(commit_id)
32
+
33
+ status, output = buildable.builds[commit_id]
34
+ assert_equal :successful, status
35
+ assert_equal "Running tests...\n", output
36
+
37
+ assert_equal 1, buildable.metadata.length
38
+
39
+ commit = buildable.metadata[commit_id]
40
+ assert_equal "This commit will work", commit[:message]
41
+ assert commit[:committed_at].is_a?(Time)
42
+ end
43
+
44
+ test "with a failed build" do
45
+ repo.add_failing_commit
46
+
47
+ commit_id = repo.commits.last[:identifier]
48
+
49
+ buildable.build(commit_id)
50
+
51
+ status, output = buildable.builds[commit_id]
52
+ assert_equal :failed, status
53
+ assert_equal "Running tests...\n", output
54
+
55
+ assert_equal 1, buildable.metadata.length
56
+
57
+ commit = buildable.metadata[commit_id]
58
+ assert_equal "This commit will fail", commit[:message]
59
+ assert commit[:committed_at].is_a?(Time)
60
+ end
61
+
62
+ test "with multiple commits" do
63
+ 2.times { repo.add_failing_commit }
64
+ commits = repo.commits.collect { |c| c[:identifier] }
65
+
66
+ buildable.build(commits)
67
+
68
+ assert_equal 2, commits.length
69
+ assert_equal 2, buildable.metadata.length
70
+ assert_equal 2, buildable.builds.length
71
+ end
72
+
73
+ test "can build the head of a repository" do
74
+ repo.add_failing_commit
75
+ repo.add_successful_commit
76
+
77
+ buildable.build(:head)
78
+
79
+ assert_equal 1, buildable.builds.length
80
+
81
+ status, output = buildable.builds[repo.head]
82
+ assert_equal :successful, status
83
+ assert_equal "Running tests...\n", output
84
+ end
85
+ end
@@ -0,0 +1,87 @@
1
+ require File.dirname(__FILE__) + "/../helper"
2
+
3
+ class BobSvnTest < Test::Unit::TestCase
4
+ def setup
5
+ super
6
+
7
+ @repo = SvnRepo.new(:test_repo)
8
+ @repo.create
9
+
10
+ @buildable = BuildableStub.call(@repo)
11
+ end
12
+
13
+ def path(uri)
14
+ SCM::Svn.new(uri, "").__send__(:path)
15
+ end
16
+
17
+ test "converts svn repo uri into a path" do
18
+ assert_equal "http-rubygems-rubyforge-org-svn",
19
+ path("http://rubygems.rubyforge.org/svn/")
20
+
21
+ assert_equal "svn-rubyforge-org-var-svn-rubygems",
22
+ path("svn://rubyforge.org/var/svn/rubygems")
23
+
24
+ assert_equal "svn-ssh-developername-rubyforge-org-var-svn-rubygems",
25
+ path("svn+ssh://developername@rubyforge.org/var/svn/rubygems")
26
+
27
+ assert_equal "home-user-code-repo",
28
+ path("/home/user/code/repo")
29
+ end
30
+
31
+ test "with a successful build" do
32
+ repo.add_successful_commit
33
+
34
+ buildable.build("2")
35
+
36
+ assert_equal 1, buildable.metadata.length
37
+
38
+ status, output = buildable.builds["2"]
39
+ assert_equal :successful, status
40
+ assert_equal "Running tests...\n", output
41
+
42
+ assert_equal 1, buildable.metadata.length
43
+
44
+ commit = buildable.metadata["2"]
45
+ assert commit[:committed_at].is_a?(Time)
46
+ assert_equal "This commit will work", commit[:message]
47
+ end
48
+
49
+ test "with a failed build" do
50
+ repo.add_failing_commit
51
+
52
+ buildable.build("2")
53
+
54
+ status, output = buildable.builds["2"]
55
+ assert_equal :failed, status
56
+ assert_equal "Running tests...\n", output
57
+
58
+ assert_equal 1, buildable.metadata.length
59
+
60
+ commit = buildable.metadata["2"]
61
+ assert commit[:committed_at].is_a?(Time)
62
+ assert_equal "This commit will fail", commit[:message]
63
+ end
64
+
65
+ test "with multiple commits" do
66
+ repo.add_successful_commit
67
+ 2.times { repo.add_failing_commit }
68
+
69
+ buildable.build(repo.commits.collect { |c| c[:identifier] })
70
+
71
+ assert_equal 3, buildable.metadata.length
72
+ assert_equal 3, buildable.builds.length
73
+ end
74
+
75
+ test "can build the head of a repository" do
76
+ repo.add_failing_commit
77
+ repo.add_successful_commit
78
+
79
+ buildable.build(:head)
80
+
81
+ assert_equal 1, buildable.builds.length
82
+
83
+ status, output = buildable.builds["3"]
84
+ assert_equal :successful, status
85
+ assert_equal "Running tests...\n", output
86
+ end
87
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: integrity-bob
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - "Nicol\xC3\xA1s Sanguinetti"
8
+ - Simon Rozet
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-07-02 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: addressable
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: sr-mg
28
+ type: :development
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ version:
36
+ - !ruby/object:Gem::Dependency
37
+ name: sr-bob-test
38
+ type: :development
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ - !ruby/object:Gem::Dependency
47
+ name: contest
48
+ type: :development
49
+ version_requirement:
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ - !ruby/object:Gem::Dependency
57
+ name: redgreen
58
+ type: :development
59
+ version_requirement:
60
+ version_requirements: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ - !ruby/object:Gem::Dependency
67
+ name: ruby-debug
68
+ type: :development
69
+ version_requirement:
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ version:
76
+ description: Bob the Builder will build your code. Simple.
77
+ email: info@integrityapp.com
78
+ executables: []
79
+
80
+ extensions: []
81
+
82
+ extra_rdoc_files: []
83
+
84
+ files:
85
+ - .gitignore
86
+ - LICENSE
87
+ - README.rdoc
88
+ - Rakefile
89
+ - bob.gemspec
90
+ - lib/bob.rb
91
+ - lib/bob/buildable.rb
92
+ - lib/bob/builder.rb
93
+ - lib/bob/engine.rb
94
+ - lib/bob/engine/foreground.rb
95
+ - lib/bob/engine/threaded.rb
96
+ - lib/bob/scm.rb
97
+ - lib/bob/scm/abstract.rb
98
+ - lib/bob/scm/git.rb
99
+ - lib/bob/scm/svn.rb
100
+ - lib/core_ext/object.rb
101
+ - test/bob_test.rb
102
+ - test/engine/threaded_test.rb
103
+ - test/helper.rb
104
+ - test/scm/git_test.rb
105
+ - test/scm/svn_test.rb
106
+ has_rdoc: true
107
+ homepage: http://integrityapp.com
108
+ post_install_message:
109
+ rdoc_options: []
110
+
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: "0"
118
+ version:
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: "0"
124
+ version:
125
+ requirements: []
126
+
127
+ rubyforge_project: integrity
128
+ rubygems_version: 1.2.0
129
+ signing_key:
130
+ specification_version: 2
131
+ summary: Bob builds!
132
+ test_files: []
133
+