bait 0.4.1 → 0.5.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.
@@ -1,5 +1,7 @@
1
1
  require 'bait/object'
2
2
  require 'bait/tester'
3
+ require 'json'
4
+ require 'bait/pubsub'
3
5
 
4
6
  module Bait
5
7
  class Build < Bait::Object
@@ -11,39 +13,103 @@ module Bait
11
13
  attribute :owner_email, String
12
14
  attribute :name, String
13
15
  attribute :clone_url, String
14
- attribute :passed, Boolean
15
16
  attribute :output, String, default: ""
16
- attribute :tested, Boolean, default: false
17
+ attribute :status, String, default: "queued"
17
18
 
18
19
  validates_presence_of :name
19
20
  validates_presence_of :clone_url
20
21
 
21
- def tester
22
- @tester ||= Bait::Tester.new(self)
22
+ after_create do
23
+ Bait.broadcast(:global, :new_build, self)
24
+ end
25
+
26
+ after_destroy do
27
+ self.broadcast(:remove)
28
+ self.cleanup!
29
+ end
30
+
31
+ def test!
32
+ Open3.popen2e(self.script) do |stdin, oe, wait_thr|
33
+ self.status = "testing"
34
+ self.broadcast :status, self.status
35
+ self.save
36
+ oe.each do |line|
37
+ self.output << line
38
+ self.broadcast(:output, line)
39
+ end
40
+ if wait_thr.value.exitstatus == 0
41
+ self.status = "passed"
42
+ else
43
+ self.status = "failed"
44
+ end
45
+ end
46
+ rescue Errno::ENOENT => ex
47
+ self.output << "A test script was expected but missing.\nError: #{ex.message}"
48
+ self.status = "script missing"
49
+ ensure
50
+ self.save
51
+ self.broadcast(:status, status)
23
52
  end
24
53
 
25
54
  def test_later
26
- self.tested = false
55
+ self.status = "queued"
56
+ self.output = ""
27
57
  self.save
28
- fork do
29
- self.tester.clone!
30
- self.tester.test!
31
- end
58
+ Bait::Tester.new.async.perform(self.id) unless Bait.env == "test"
32
59
  self
33
60
  end
34
61
 
35
62
  def queued?
36
- !self.reload.tested?
63
+ self.reload.status == "queued"
64
+ end
65
+
66
+ def passed?
67
+ self.reload.status == "passed"
68
+ end
69
+
70
+ def clone_path
71
+ File.join(sandbox_directory, self.name)
37
72
  end
38
73
 
39
- def status
40
- if queued?
41
- "queued"
42
- elsif tested?
43
- passed? ? "passed" : "failed"
74
+ def bait_dir
75
+ File.join(clone_path, ".bait")
76
+ end
77
+
78
+ def script
79
+ File.join(bait_dir, "test.sh")
80
+ end
81
+
82
+ def cloned?
83
+ Dir.exists? File.join(clone_path, ".git/")
84
+ end
85
+
86
+ def cleanup!
87
+ FileUtils.rm_rf(sandbox_directory) if Dir.exists?(sandbox_directory)
88
+ end
89
+
90
+ def sandbox_directory
91
+ File.join Bait.storage_dir, "tester", self.name, self.id
92
+ end
93
+
94
+ def clone!
95
+ unless cloned?
96
+ unless Dir.exists?(sandbox_directory)
97
+ FileUtils.mkdir_p sandbox_directory
98
+ end
99
+ begin
100
+ Git.clone(clone_url, name, :path => sandbox_directory)
101
+ rescue => ex
102
+ msg = "Failed to clone #{clone_url}"
103
+ self.output << "#{msg}\n\n#{ex.message}\n\n#{ex.backtrace.join("\n")}"
104
+ self.save
105
+ end
44
106
  end
45
107
  end
46
108
 
47
- after_destroy { tester.cleanup! }
109
+ protected
110
+
111
+ def broadcast attr, *args
112
+ Bait.broadcast :build, attr, self.id, *args
113
+ end
48
114
  end
49
115
  end
@@ -1,3 +1,6 @@
1
+ require 'celluloid'
2
+ require 'bait/build'
3
+
1
4
  module Bait
2
5
  module CLI
3
6
  USAGE = %{usage:
@@ -9,7 +12,7 @@ module Bait
9
12
  ##
10
13
  # Start the server
11
14
  def self.server
12
- puts "Starting bait server"
15
+ puts "== Bait/#{Bait::VERSION} booting up..."
13
16
  require 'bait/api'
14
17
  Bait::Api.run!
15
18
  end
@@ -0,0 +1,8 @@
1
+ /*
2
+ YUI 3.11.0 (build d549e5c)
3
+ Copyright 2013 Yahoo! Inc. All rights reserved.
4
+ Licensed under the BSD License.
5
+ http://yuilibrary.com/license/
6
+ */
7
+
8
+ html{color:#000;background:#FFF}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}q:before,q:after{content:''}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}input,textarea,select{*font-size:100%}legend{color:#000}#yui3-css-stamp.cssreset{display:none}
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ module Bait
4
+ class << self
5
+ @@Subscribers = []
6
+ def add_subscriber stream
7
+ @@Subscribers << stream
8
+ end
9
+ def remove_subscriber stream
10
+ @@Subscribers.delete stream
11
+ end
12
+ def broadcast *args
13
+ @@Subscribers.each do |out|
14
+ out << "data: #{args.to_json}\n\n"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,63 +2,15 @@ require 'bait'
2
2
  require 'bait/build'
3
3
  require 'git'
4
4
  require 'open3'
5
+ require 'sucker_punch'
5
6
 
6
7
  module Bait
7
8
  class Tester
8
- attr_reader :passed
9
-
10
- def initialize build
11
- @build = build
12
- end
13
-
14
- def bait_dir
15
- File.join(clone_path, ".bait")
16
- end
17
-
18
- def script
19
- File.join(bait_dir, "test.sh")
20
- end
21
-
22
- def test!
23
- Open3.popen2e(script) do |stdin, oe, wait_thr|
24
- oe.each {|line| @build.output << line }
25
- @build.passed = wait_thr.value.exitstatus == 0
26
- end
27
- @build.tested = true
28
- rescue Errno::ENOENT => ex
29
- @build.output << "A test script was expected but missing.\nError: #{ex.message}"
30
- ensure
31
- @build.save
32
- end
33
-
34
- def clone_path
35
- File.join(sandbox_directory, @build.name)
36
- end
37
-
38
- def clone!
39
- unless cloned?
40
- begin
41
- Git.clone(@build.clone_url, @build.name, :path => sandbox_directory)
42
- rescue => ex
43
- msg = "Failed to clone #{@build.clone_url}"
44
- @build.output << "#{msg}\n\n#{ex.message}\n\n#{ex.backtrace}"
45
- @build.save
46
- end
47
- end
48
- end
49
-
50
- def cloned?
51
- Dir.exists? File.join(clone_path, ".git/")
52
- end
53
-
54
- def cleanup!
55
- FileUtils.rm_rf sandbox_directory
56
- end
57
-
58
- def sandbox_directory
59
- @sandbox_directory ||= begin
60
- dir = File.join Bait.storage_dir, "build_tester", @build.id
61
- FileUtils.mkdir_p(dir) && dir
9
+ include SuckerPunch::Job
10
+ def perform(build_id)
11
+ if @build = ::Bait::Build.find(build_id)
12
+ @build.clone!
13
+ @build.test!
62
14
  end
63
15
  end
64
16
  end
@@ -1,26 +1,9 @@
1
- Listing Builds
2
- %ul
3
- - @builds.each do |build|
4
- %li.build{id:build.id}
5
- .header{class:build.status}
6
- .status_icon= build.status
7
- %a{href:build.clone_url}= build.name
8
- .ref build.ref
9
- .output
10
- %pre= build.output
11
- .actions
12
- %a{href:"/build/#{build.id}/remove"} Remove
13
- |
14
- %a{href:"/build/#{build.id}/retest"} Retest
1
+ %ul#builds
2
+ %li#loading Loading builds...
15
3
 
16
4
  %hr
17
5
 
18
6
  .manual_clone
19
- To clone manually without waiting for a post-receive hook, enter the following and hit submit
20
- %form{ :action => "/build/create", :method => "post"}
21
- %fieldset
22
- %ol
23
- %li
24
- %label{:for => "clone_url"} clone_url:
25
- %input{:type => "text", :name => "clone_url", :class => "text"}
26
- %input{:type => "submit", :value => "Go", :class => "button"}
7
+ Manually clone a local or remote repository:
8
+ %input{:type => "text", :name => "clone_url", :class => "text"}
9
+ %button Go
@@ -2,10 +2,11 @@
2
2
  %html
3
3
  %head
4
4
  %title Bait
5
- %link{rel:'stylesheet', href:'css/styles.css'}
6
- %script{src:'js/zepto.min.js'}
7
- %script{src:'js/ansi2html.js'}
8
- %script{src:'js/main.js'}
5
+ %link{rel:'stylesheet', href:'/stylesheets/reset.css'}
6
+ %link{rel:'stylesheet', href:nocache('/stylesheets/application.css')}
7
+ %script{src:'/javascript/zepto.min.js'}
8
+ %script{src:'/javascript/ansi2html.js'}
9
+ %script{src:nocache('/javascript/application.js')}
9
10
  %body
10
11
  #nav
11
12
  %a{href:'/'}Home
@@ -66,11 +66,13 @@ describe Bait::Api do
66
66
 
67
67
  describe "GET /" do
68
68
  before { get '/' }
69
- it { should be_redirect }
69
+ it "renders the loading screen" do
70
+ subject.body.should match /Loading/
71
+ end
70
72
  end
71
73
 
72
- describe "GET /build" do
73
- before do
74
+ describe "GET /build" do
75
+ before do
74
76
  Bait::Build.create(name: "quickfox", clone_url:'...')
75
77
  Bait::Build.create(name: "slowsloth", clone_url:'...')
76
78
  get '/build'
@@ -78,9 +80,8 @@ describe Bait::Api do
78
80
 
79
81
  it { should be_ok }
80
82
 
81
- it "shows the builds" do
82
- subject.body.should match /quickfox/
83
- subject.body.should match /slowsloth/
83
+ it "returns the builds as JSON" do
84
+ JSON.parse(subject.body).should have(2).items
84
85
  end
85
86
  end
86
87
 
@@ -94,35 +95,43 @@ describe Bait::Api do
94
95
  specify { build.should be_queued }
95
96
  end
96
97
 
97
- describe "GET /build/:id/remove" do
98
+ describe "DELETE /build/:id" do
98
99
  before do
99
100
  @build = Bait::Build.create(name: "quickfox", clone_url:'...')
100
- @sandbox = @build.tester.sandbox_directory
101
- get "/build/#{@build.id}/remove"
101
+ @sandbox = @build.sandbox_directory
102
+ delete "/build/#{@build.id}"
102
103
  end
103
104
  it "removes the build from store and its files from the filesystem" do
104
105
  expect{@build.reload}.to raise_error Toy::NotFound
105
106
  Bait::Build.ids.should be_empty
106
107
  Pathname.new(@sandbox).should_not exist
107
108
  end
108
- it { should be_redirect }
109
109
  end
110
110
 
111
- describe "GET /build/:id/retest" do
111
+ describe "POST /build/:id/retest" do
112
112
  before do
113
113
  @build = Bait::Build.create(name: "quickfox", clone_url:'...')
114
- @build.tested = true
115
114
  @build.output = "bla bla old output"
116
115
  @build.save
117
- get "/build/#{@build.id}/retest"
118
- @build.reload
116
+ post "/build/#{@build.id}/retest"
119
117
  end
120
118
  it "queues the build for retesting" do
121
- @build.should be_queued
119
+ @build.reload.status.should eq 'queued'
122
120
  end
123
121
  it "clears the previous output" do
124
- @build.output.should be_blank
122
+ @build.reload.output.should be_blank
123
+ end
124
+ end
125
+
126
+ describe "GET /events" do
127
+ let (:connect!) { get "/events" }
128
+ it "adds a global and build subscriber" do
129
+ Bait.should_receive(:add_subscriber).with(anything()).once
130
+ connect!
131
+ end
132
+ it "provides an event stream connection" do
133
+ connect!
134
+ last_response.content_type.should match(/text\/event-stream/)
125
135
  end
126
- it { should be_redirect }
127
136
  end
128
137
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
  require 'bait/build'
3
3
 
4
4
  describe Bait::Build do
5
- subject { Bait::Build }
5
+ subject { Bait::Build }
6
6
 
7
7
  describe ".all" do
8
8
  context "with nothing in the store" do
@@ -41,24 +41,20 @@ describe Bait::Build do
41
41
 
42
42
  let (:build) { Bait::Build.create(name: "app", clone_url:'...') }
43
43
 
44
- describe "#test_later" do
45
- it "forks in order to clone and test" do
46
- build.should_receive(:fork) do |&block|
47
- build.tester.should_receive(:clone!)
48
- build.tester.should_receive(:test!)
49
- block.call
44
+ describe "hooks" do
45
+ describe "after_create hook" do
46
+ it "broadcasts its creation" do
47
+ Bait.should_receive(:broadcast).with(:global, :new_build, kind_of(Bait::Build))
48
+ build
50
49
  end
51
- build.test_later
52
50
  end
53
- end
54
-
55
- describe "#tester" do
56
- specify { build.tester.should be_a Bait::Tester }
57
- end
58
51
 
59
- describe "#passed" do
60
- it "starts as nil" do
61
- build.passed.should be_nil
52
+ describe "after_destroy hook" do
53
+ before { build }
54
+ it "broadcasts its removal" do
55
+ Bait.should_receive(:broadcast).with(:build, :remove, build.id)
56
+ build.destroy
57
+ end
62
58
  end
63
59
  end
64
60
 
@@ -74,43 +70,37 @@ describe Bait::Build do
74
70
  end
75
71
  end
76
72
 
77
- describe "#queued" do
78
- subject { build }
79
- context "already tested" do
80
- before { build.tested = true ; build.save }
81
- it { should_not be_queued }
73
+ describe "#sandbox_directory" do
74
+ it "is beneath Bait storage directory" do
75
+ build.sandbox_directory.should match Bait.storage_dir
82
76
  end
77
+ end
83
78
 
84
- context "not tested" do
85
- before { build.tested = false ; build.save }
86
- it { should be_queued }
87
- end
79
+ describe "#cloned?" do
80
+ specify { build.should_not be_cloned }
88
81
  end
89
82
 
90
- describe "#status" do
91
- subject { build.reload.status }
92
- context 'queued' do
93
- before do
94
- build.tested = false
95
- build.save
96
- end
97
- it { should eq "queued" }
83
+ describe "#clone!" do
84
+ context 'valid clone url' do
85
+ before { build.clone_url = repo_path ; build.clone! }
86
+ specify { build.output.should_not match /Failed to clone/ }
87
+ specify { build.should be_cloned }
98
88
  end
99
- context 'passed' do
100
- before do
101
- build.tested = true
102
- build.passed = true
103
- build.save
104
- end
105
- it { should eq "passed" }
89
+ context "invalid clone url" do
90
+ before { build.clone_url = "invalid" ; build.clone! }
91
+ specify { build.output.should match /Failed to clone/ }
92
+ specify { build.should_not be_cloned }
106
93
  end
107
- context 'failed' do
108
- before do
109
- build.tested = true
110
- build.passed = false
111
- build.save
112
- end
113
- it { should eq "failed" }
94
+ end
95
+
96
+ describe "cleanup!" do
97
+ before do
98
+ build.clone!
99
+ end
100
+ it "removes the entire sandbox" do
101
+ Dir.exists?(build.sandbox_directory).should be_true
102
+ build.cleanup!
103
+ Dir.exists?(build.sandbox_directory).should be_false
114
104
  end
115
105
  end
116
106
  end