foca-integrity 0.1.8 → 0.1.9.0

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 (75) hide show
  1. data/README.markdown +7 -0
  2. data/Rakefile +89 -81
  3. data/config/config.sample.ru +11 -21
  4. data/config/config.sample.yml +15 -12
  5. data/lib/integrity.rb +21 -23
  6. data/lib/integrity/app.rb +138 -0
  7. data/lib/integrity/author.rb +39 -0
  8. data/lib/integrity/build.rb +54 -31
  9. data/lib/integrity/commit.rb +71 -0
  10. data/lib/integrity/helpers.rb +3 -3
  11. data/lib/integrity/helpers/authorization.rb +2 -2
  12. data/lib/integrity/helpers/forms.rb +3 -3
  13. data/lib/integrity/helpers/pretty_output.rb +1 -1
  14. data/lib/integrity/helpers/rendering.rb +6 -1
  15. data/lib/integrity/helpers/resources.rb +9 -3
  16. data/lib/integrity/helpers/urls.rb +15 -13
  17. data/lib/integrity/installer.rb +43 -60
  18. data/lib/integrity/migrations.rb +31 -48
  19. data/lib/integrity/notifier.rb +14 -16
  20. data/lib/integrity/notifier/base.rb +29 -19
  21. data/lib/integrity/notifier/test_helpers.rb +100 -0
  22. data/lib/integrity/project.rb +69 -33
  23. data/lib/integrity/project_builder.rb +23 -14
  24. data/lib/integrity/scm/git.rb +15 -14
  25. data/lib/integrity/scm/git/uri.rb +9 -9
  26. data/test/acceptance/api_test.rb +97 -0
  27. data/test/acceptance/browse_project_builds_test.rb +65 -0
  28. data/test/acceptance/browse_project_test.rb +95 -0
  29. data/test/acceptance/build_notifications_test.rb +42 -0
  30. data/test/acceptance/create_project_test.rb +97 -0
  31. data/test/acceptance/delete_project_test.rb +53 -0
  32. data/test/acceptance/edit_project_test.rb +117 -0
  33. data/test/acceptance/error_page_test.rb +18 -0
  34. data/test/acceptance/helpers.rb +2 -0
  35. data/test/acceptance/installer_test.rb +62 -0
  36. data/test/acceptance/manual_build_project_test.rb +82 -0
  37. data/test/acceptance/notifier_test.rb +109 -0
  38. data/test/acceptance/project_syndication_test.rb +30 -0
  39. data/test/acceptance/stylesheet_test.rb +18 -0
  40. data/test/helpers.rb +59 -27
  41. data/test/helpers/acceptance.rb +19 -64
  42. data/test/helpers/acceptance/email_notifier.rb +55 -0
  43. data/test/helpers/acceptance/git_helper.rb +15 -15
  44. data/test/helpers/acceptance/textfile_notifier.rb +3 -3
  45. data/test/helpers/expectations.rb +0 -1
  46. data/test/helpers/expectations/be_a.rb +4 -4
  47. data/test/helpers/expectations/change.rb +5 -5
  48. data/test/helpers/expectations/have.rb +4 -4
  49. data/test/helpers/expectations/predicates.rb +4 -4
  50. data/test/helpers/fixtures.rb +44 -18
  51. data/test/helpers/initial_migration_fixture.sql +44 -0
  52. data/test/unit/build_test.rb +51 -0
  53. data/test/unit/commit_test.rb +83 -0
  54. data/test/unit/helpers_test.rb +56 -0
  55. data/test/unit/integrity_test.rb +18 -0
  56. data/test/unit/migrations_test.rb +56 -0
  57. data/test/unit/notifier_test.rb +123 -0
  58. data/test/unit/project_builder_test.rb +108 -0
  59. data/test/unit/project_test.rb +282 -0
  60. data/test/unit/scm_test.rb +54 -0
  61. data/views/_commit_info.haml +24 -0
  62. data/views/build.haml +2 -2
  63. data/views/error.haml +4 -3
  64. data/views/home.haml +3 -5
  65. data/views/integrity.sass +19 -6
  66. data/views/new.haml +6 -6
  67. data/views/project.builder +9 -9
  68. data/views/project.haml +14 -12
  69. metadata +98 -116
  70. data/VERSION.yml +0 -4
  71. data/app.rb +0 -137
  72. data/integrity.gemspec +0 -76
  73. data/lib/integrity/core_ext/string.rb +0 -5
  74. data/test/helpers/expectations/have_tag.rb +0 -128
  75. data/views/_build_info.haml +0 -18
@@ -1,23 +1,24 @@
1
1
  module Integrity
2
2
  class ProjectBuilder
3
- attr_reader :build_script
4
-
5
3
  def initialize(project)
4
+ @project = project
6
5
  @uri = project.uri
7
6
  @build_script = project.command
8
7
  @branch = project.branch
9
8
  @scm = SCM.new(@uri, @branch, export_directory)
10
- @build = Build.new(:project => project)
11
9
  end
12
10
 
13
11
  def build(commit)
14
- Integrity.log "Building #{commit} (#{@branch}) of #{@build.project.name} in #{export_directory} using #{scm_name}"
15
- @scm.with_revision(commit) { run_build_script }
12
+ @commit = commit
13
+ @build = commit.build
14
+ @build.start!
15
+ Integrity.log "Building #{commit.identifier} (#{@branch}) of #{@project.name} in #{export_directory} using #{@scm.name}"
16
+ @scm.with_revision(commit.identifier) { run_build_script }
16
17
  @build
17
18
  ensure
18
- @build.commit_identifier = @scm.commit_identifier(commit)
19
- @build.commit_metadata = @scm.commit_metadata(commit)
20
- @build.save
19
+ @build.complete!
20
+ @commit.update_attributes(@scm.info(commit.identifier))
21
+ send_notifications
21
22
  end
22
23
 
23
24
  def delete_code
@@ -27,18 +28,26 @@ module Integrity
27
28
  end
28
29
 
29
30
  private
30
- def export_directory
31
- Integrity.config[:export_directory] / "#{SCM.working_tree_path(@uri)}-#{@branch}"
31
+ def send_notifications
32
+ @project.notifiers.each do |notifier|
33
+ begin
34
+ Integrity.log "Notifying of build #{@commit.short_identifier} using the #{notifier.name} notifier"
35
+ notifier.notify_of_build @commit
36
+ rescue Timeout::Error
37
+ Integrity.log "#{notifier.name} notifier timed out"
38
+ next
39
+ end
40
+ end
32
41
  end
33
42
 
34
- def scm_name
35
- @scm.name
43
+ def export_directory
44
+ Integrity.config[:export_directory] / "#{SCM.working_tree_path(@uri)}-#{@branch}"
36
45
  end
37
46
 
38
47
  def run_build_script
39
- Integrity.log "Running `#{build_script}` in #{@scm.working_directory}"
48
+ Integrity.log "Running `#{@build_script}` in #{@scm.working_directory}"
40
49
 
41
- IO.popen "(cd #{@scm.working_directory} && #{build_script}) 2>&1", "r" do |pipe|
50
+ IO.popen "(cd #{@scm.working_directory} && #{@build_script}) 2>&1", "r" do |pipe|
42
51
  @build.output = pipe.read
43
52
  end
44
53
  @build.successful = $?.success?
@@ -9,8 +9,8 @@ module Integrity
9
9
  Git::URI.new(uri).working_tree_path
10
10
  end
11
11
 
12
- def initialize(uri, branch, working_directory)
13
- @uri = uri.to_s
12
+ def initialize(uri, branch, working_directory=nil)
13
+ @uri = uri.to_s
14
14
  @branch = branch.to_s
15
15
  @working_directory = working_directory
16
16
  end
@@ -21,27 +21,28 @@ module Integrity
21
21
  yield
22
22
  end
23
23
 
24
- def commit_identifier(sha1)
25
- `cd #{working_directory} && git show -s --pretty=format:%H #{sha1}`.chomp
24
+ def name
25
+ self.class.name.split("::").last
26
26
  end
27
27
 
28
- def commit_metadata(sha1)
29
- format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:date: %ci%n)
30
- YAML.load(`cd #{working_directory} && git show -s --pretty=format:"#{format}" #{sha1}`)
28
+ def head
29
+ log "Getting the HEAD of '#{uri}' at '#{branch}'"
30
+ `git ls-remote --heads #{uri} #{branch} | awk '{print $1}'`.chomp
31
31
  end
32
-
33
- def name
34
- self.class.name.split("::").last
32
+
33
+ def info(revision)
34
+ format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:committed_at: %ci%n)
35
+ YAML.load(`cd #{working_directory} && git show -s --pretty=format:"#{format}" #{revision}`)
35
36
  end
36
37
 
37
38
  private
38
39
 
39
40
  def fetch_code
40
- clone unless cloned?
41
+ clone unless cloned?
41
42
  checkout unless on_branch?
42
43
  pull
43
44
  end
44
-
45
+
45
46
  def clone
46
47
  log "Cloning #{uri} to #{working_directory}"
47
48
  `git clone #{uri} #{working_directory} &>/dev/null`
@@ -51,11 +52,11 @@ module Integrity
51
52
  strategy = case
52
53
  when treeish then treeish
53
54
  when local_branches.include?(branch) then branch
54
- else "-b #{branch} origin/#{branch}"
55
+ else "origin/#{branch}"
55
56
  end
56
57
 
57
58
  log "Checking-out #{strategy}"
58
- `cd #{working_directory} && git checkout #{strategy} &>/dev/null`
59
+ `cd #{working_directory} && git reset --hard #{strategy} &>/dev/null`
59
60
  end
60
61
 
61
62
  def pull
@@ -13,13 +13,13 @@ module Integrity
13
13
  # ssh://[user@]host.xz/path/to/repo.git/
14
14
  # ssh://[user@]host.xz/~user/path/to/repo.git/
15
15
  # ssh://[user@]host.xz/~/path/to/repo.git
16
- #
17
- # SSH is the default transport protocol over the network. You can optionally
18
- # specify which user to log-in as, and an alternate, scp-like syntax is also
16
+ #
17
+ # SSH is the default transport protocol over the network. You can optionally
18
+ # specify which user to log-in as, and an alternate, scp-like syntax is also
19
19
  # supported
20
20
  #
21
- # Both syntaxes support username expansion, as does the native git protocol,
22
- # but only the former supports port specification. The following three are
21
+ # Both syntaxes support username expansion, as does the native git protocol,
22
+ # but only the former supports port specification. The following three are
23
23
  # identical to the last three above, respectively:
24
24
  #
25
25
  # [user@]host.xz:/path/to/repo.git/
@@ -30,13 +30,13 @@ module Integrity
30
30
  def initialize(uri_string)
31
31
  @uri = Addressable::URI.parse(uri_string)
32
32
  end
33
-
33
+
34
34
  def working_tree_path
35
35
  strip_extension(path).gsub("/", "-")
36
36
  end
37
-
37
+
38
38
  private
39
-
39
+
40
40
  def strip_extension(string)
41
41
  uri = Pathname.new(string)
42
42
  if uri.extname.any?
@@ -46,7 +46,7 @@ module Integrity
46
46
  string
47
47
  end
48
48
  end
49
-
49
+
50
50
  def path
51
51
  path = @uri.path
52
52
  path.gsub(/\~[a-zA-Z0-9]*\//, "").gsub(/^\//, "")
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + "/helpers"
2
+
3
+ class ApiTest < Test::Unit::AcceptanceTestCase
4
+ story <<-EOF
5
+ As a project owner,
6
+ I want to be able to use GitHub as a build triggerer
7
+ So that my project is built everytime I push to the Holy Hub
8
+ EOF
9
+
10
+ def payload(after, branch="master", commits=[])
11
+ payload = { "after" => "#{after}", "ref" => "refs/heads/#{branch}" }
12
+ payload["commits"] = commits if commits.any?
13
+ payload.to_json
14
+ end
15
+
16
+ def post(path, data)
17
+ request_page(path, "post", data)
18
+ end
19
+
20
+ scenario "it only build commits for the branch being monitored" do
21
+ repo = git_repo(:my_test_project) # initial commit && successful commit
22
+ Project.gen(:my_test_project, :uri => repo.path, :branch => "my-branch")
23
+
24
+ basic_auth "admin", "test"
25
+
26
+ lambda do
27
+ post "/my-test-project/push", :payload => payload(repo.head, "master", repo.commits)
28
+ response_code.should == 422
29
+ end.should_not change(Build, :count)
30
+
31
+ visit "/my-test-project"
32
+
33
+ assert_contain("No builds for this project")
34
+ end
35
+
36
+ it "receiving a build request with build_all_commits *enabled* builds all commits, most recent first" do
37
+ Integrity.config[:build_all_commits] = true
38
+
39
+ repo = git_repo(:my_test_project) # initial commit && successful commit
40
+ 3.times do |i|
41
+ repo.add_commit("commit #{i}") do
42
+ system "echo commit_#{i} >> test-file"
43
+ system "git add test-file &>/dev/null"
44
+ end
45
+ end
46
+
47
+ Project.gen(:my_test_project, :uri => repo.path, :command => "echo successful", :branch => "master")
48
+
49
+ basic_auth "admin", "test"
50
+ post "/my-test-project/push", :payload => payload(repo.head, "master", repo.commits)
51
+
52
+ visit "/my-test-project"
53
+
54
+ assert_have_tag("h1", :content => "Built #{git_repo(:my_test_project).short_head} successfully")
55
+ assert_have_tag(".attribution", :content => "by John Doe")
56
+
57
+ assert_have_tag("#previous_builds li", :count => 4)
58
+ end
59
+
60
+ scenario "receiving a build request with build_all_commits *disabled* only builds the last commit passed" do
61
+ Integrity.config[:build_all_commits] = false
62
+
63
+ Project.gen(:my_test_project, :uri => git_repo(:my_test_project).path)
64
+
65
+ git_repo(:my_test_project).add_failing_commit
66
+ git_repo(:my_test_project).add_successful_commit
67
+ head = git_repo(:my_test_project).head
68
+
69
+ basic_auth "admin", "test"
70
+ post "/my-test-project/push", :payload => payload(head, "master", git_repo(:my_test_project).commits)
71
+
72
+ response_code.should == 201
73
+
74
+ visit "/my-test-project"
75
+
76
+ assert_have_tag("h1", :content => "Built #{git_repo(:my_test_project).short_head} successfully")
77
+
78
+ assert_have_no_tag("#previous_builds li")
79
+ end
80
+
81
+ scenario "an unauthenticated request returns a 401" do
82
+ Project.gen(:my_test_project, :uri => git_repo(:my_test_project).path)
83
+ head = git_repo(:my_test_project).head
84
+ post "/my-test-project/push", :payload => payload(head, "master")
85
+
86
+ response_code.should == 401
87
+ end
88
+
89
+ scenario "receiving a build request with an invalid payload returns an Invalid Request error" do
90
+ Project.gen(:my_test_project, :uri => git_repo(:my_test_project).path)
91
+
92
+ basic_auth "admin", "test"
93
+
94
+ post "/my-test-project/push", :payload => "foo"
95
+ response_code.should == 422
96
+ end
97
+ end
@@ -0,0 +1,65 @@
1
+ require File.dirname(__FILE__) + "/helpers"
2
+
3
+ class BrowseProjectBuildsTest < Test::Unit::AcceptanceTestCase
4
+ story <<-EOS
5
+ As a user,
6
+ I want to browse the builds of a project in Integrity
7
+ So I can see the history of a project
8
+ EOS
9
+
10
+ scenario "a project with no builds should say so in a friendly manner" do
11
+ Project.gen(:integrity, :public => true, :commits => [])
12
+
13
+ visit "/integrity"
14
+
15
+ assert_have_no_tag("#last_build")
16
+ assert_have_no_tag("#previous_builds")
17
+ assert_contain("No builds for this project, buddy")
18
+ end
19
+
20
+ scenario "a user can see the last build and the list of previous builds on a project page" do
21
+ Project.gen(:integrity, :public => true, :commits => \
22
+ 3.of { Commit.gen(:successful) } +
23
+ 2.of { Commit.gen(:failed) } +
24
+ 2.of { Commit.gen(:pending) })
25
+
26
+ visit "/integrity"
27
+
28
+ assert_have_tag("#last_build")
29
+
30
+ within("ul#previous_builds") do
31
+ assert_have_tag("li.pending", :count => 2)
32
+ assert_have_tag("li.failed", :count => 2)
33
+ assert_have_tag("li.success", :count => 2)
34
+ end
35
+ end
36
+
37
+ scenario "a user can see details about the last build on the project page" do
38
+ commit = Commit.gen(:successful, :identifier => "7fee3f0014b529e2b76d591a8085d76eab0ff923",
39
+ :author => "Nicolas Sanguinetti <contacto@nicolassanguinetti.info>",
40
+ :message => "No more pending tests :)",
41
+ :committed_at => Time.mktime(2008, 12, 15, 18))
42
+ commit.build.update_attributes(:output => "This is the build output")
43
+ Project.gen(:integrity, :public => true, :commits => [commit])
44
+
45
+ visit "/integrity"
46
+
47
+ assert_have_tag("h1", :content => "Built 7fee3f0 successfully")
48
+ assert_have_tag("blockquote p", :content => "No more pending tests")
49
+ assert_have_tag("span.who", :content => "by: Nicolas Sanguinetti")
50
+ assert_have_tag("span.when", :content => "Dec 15th")
51
+ assert_have_tag("pre.output", :content => "This is the build output")
52
+ end
53
+
54
+ scenario "a user can browse to individual build pages" do
55
+ Project.gen(:integrity, :public => true, :commits => [
56
+ Commit.gen(:successful, :identifier => "7fee3f0014b529e2b76d591a8085d76eab0ff923"),
57
+ Commit.gen(:successful, :identifier => "87e673a83d273ecde121624a3fcfae57a04f2b76")
58
+ ])
59
+
60
+ visit "/integrity"
61
+ click_link(/Build 87e673a/)
62
+
63
+ assert_have_tag("h1", :content => "Built 87e673a successfully")
64
+ end
65
+ end
@@ -0,0 +1,95 @@
1
+ require File.dirname(__FILE__) + "/helpers"
2
+
3
+ class BrowsePublicProjectsTest < Test::Unit::AcceptanceTestCase
4
+ story <<-EOS
5
+ As a user,
6
+ I want to browse public projects on Integrity,
7
+ So I can follow the status of my favorite OSS projects
8
+ EOS
9
+
10
+ scenario "a user can see a public project listed on the home page" do
11
+ Project.gen(:integrity, :public => true)
12
+ Project.gen(:my_test_project, :public => true)
13
+
14
+ visit "/"
15
+
16
+ assert_have_tag("a", :content => "Integrity")
17
+ assert_have_tag("a", :content => "My Test Project")
18
+ end
19
+
20
+ scenario "a user can't see a private project listed on the home page" do
21
+ Project.gen(:my_test_project, :public => false)
22
+ Project.gen(:integrity, :public => true)
23
+
24
+ visit "/"
25
+
26
+ assert_have_no_tag("a", :content => "My Test Project")
27
+ assert_have_tag("a", :content => "Integrity")
28
+ end
29
+
30
+ scenario "a user can see the projects status on the home page" do
31
+ integrity = Project.gen(:integrity, :commits => 3.of { Commit.gen(:successful) })
32
+ test = Project.gen(:my_test_project, :commits => 2.of { Commit.gen(:failed) })
33
+ no_build = Project.gen(:public => true, :building => false)
34
+ building = Project.gen(:public => true, :building => true)
35
+
36
+ visit "/"
37
+
38
+ assert_contain("Built #{integrity.last_commit.short_identifier} successfully")
39
+ assert_contain("Built #{test.last_commit.short_identifier} and failed")
40
+ assert_contain("Never built yet")
41
+ assert_contain("Building!")
42
+ end
43
+
44
+ scenario "a user clicking through a link on the home page for a public project arrives at the project page" do
45
+ Project.gen(:my_test_project, :public => true)
46
+
47
+ visit "/"
48
+ click_link "My Test Project"
49
+
50
+ assert_have_tag("h1", :content => "My Test Project")
51
+ end
52
+
53
+ scenario "a user gets a 404 when browsing to an unexisting project" do
54
+ visit "/who-are-you"
55
+
56
+ response_code.should == 404
57
+ assert_have_tag("h1", :content => "you seem a bit lost, sir")
58
+ end
59
+
60
+ scenario "a user browsing to the url of a private project gets a 401" do
61
+ Project.gen(:my_test_project, :public => false)
62
+
63
+ visit "/my-test-project"
64
+
65
+ response_code.should == 401
66
+ assert_have_tag("h1", :content => "know the password?")
67
+ end
68
+
69
+ scenario "an admin can browse to a private project just fine" do
70
+ Project.gen(:my_test_project, :public => false)
71
+
72
+ login_as "admin", "test"
73
+
74
+ visit "/"
75
+ click_link "My Test Project"
76
+
77
+ assert_have_tag("h1", :content => "My Test Project")
78
+ end
79
+
80
+ scenario "a user browsing to a public project with no build see a friendly message" do
81
+ project = Project.gen(:my_test_project, :public => true)
82
+
83
+ visit "/my-test-project"
84
+ assert_contain("No builds for this project, buddy")
85
+ end
86
+
87
+ scenario "an admin browsing to a private project with no build see a friendly message" do
88
+ Project.gen(:my_test_project, :public => false)
89
+
90
+ login_as "admin", "test"
91
+ visit "/my-test-project"
92
+
93
+ assert_contain("No builds for this project, buddy")
94
+ end
95
+ end
@@ -0,0 +1,42 @@
1
+ require File.dirname(__FILE__) + "/helpers"
2
+ require File.dirname(__FILE__) + "/../helpers/acceptance/textfile_notifier"
3
+
4
+ class BuildNotificationsTest < Test::Unit::AcceptanceTestCase
5
+ story <<-EOS
6
+ As an administrator,
7
+ I want to setup notifiers on my projects
8
+ So that I get alerts with every build
9
+ EOS
10
+
11
+ before(:each) do
12
+ # This is needed before any available notifier is unset
13
+ # in the global #before
14
+ load File.dirname(__FILE__) + "/../helpers/acceptance/textfile_notifier.rb"
15
+ end
16
+
17
+ scenario "an admin sets up a notifier for a project that didn't have any" do
18
+ git_repo(:my_test_project).add_successful_commit
19
+ Project.gen(:my_test_project, :notifiers => [], :uri => git_repo(:my_test_project).path)
20
+ rm_f "/tmp/textfile_notifications.txt"
21
+
22
+ login_as "admin", "test"
23
+
24
+ visit "/my-test-project"
25
+
26
+ click_link "Edit Project"
27
+ check "enabled_notifiers_textfile"
28
+ fill_in "File", :with => "/tmp/textfile_notifications.txt"
29
+ click_button "Update Project"
30
+
31
+ click_button "manual build"
32
+
33
+ notification = File.read("/tmp/textfile_notifications.txt")
34
+ notification.should =~ /=== Built #{git_repo(:my_test_project).short_head} successfully ===/
35
+ notification.should =~ /Build #{git_repo(:my_test_project).head} was successful/
36
+ notification.should =~ %r(http://www.example.com/my-test-project/commits/#{git_repo(:my_test_project).head})
37
+ notification.should =~ /Commit Author: John Doe/
38
+ notification.should =~ /Commit Date: (.+)/
39
+ notification.should =~ /Commit Message: This commit will work/
40
+ notification.should =~ /Build Output:\n\nRunning tests...\n/
41
+ end
42
+ end