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.
- checksums.yaml +4 -4
- data/.bait/test.sh +1 -1
- data/Gemfile.lock +31 -2
- data/README.md +18 -24
- data/Rakefile +4 -0
- data/VERSION +1 -1
- data/assets/js/bait.coffee +11 -0
- data/assets/js/build.coffee +67 -0
- data/assets/js/main.coffee +22 -0
- data/assets/js/manual_clone.coffee +24 -0
- data/assets/stylesheets/build.scss +30 -0
- data/assets/stylesheets/main.scss +28 -0
- data/bait.gemspec +3 -0
- data/lib/bait/api.rb +24 -10
- data/lib/bait/build.rb +82 -16
- data/lib/bait/cli.rb +4 -1
- data/lib/bait/public/{js → javascript}/ansi2html.js +0 -0
- data/lib/bait/public/{js → javascript}/zepto.min.js +0 -0
- data/lib/bait/public/stylesheets/reset.css +8 -0
- data/lib/bait/pubsub.rb +18 -0
- data/lib/bait/tester.rb +6 -54
- data/lib/bait/views/builds.haml +5 -22
- data/lib/bait/views/layout.haml +5 -4
- data/spec/lib/bait/api_spec.rb +26 -17
- data/spec/lib/bait/build_spec.rb +37 -47
- data/spec/lib/bait/pubsub_spec.rb +17 -0
- data/spec/lib/bait/tester_spec.rb +29 -62
- metadata +57 -8
- data/.rspec +0 -1
- data/lib/bait/public/css/styles.css +0 -77
- data/lib/bait/public/js/main.js +0 -26
data/lib/bait/build.rb
CHANGED
|
@@ -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 :
|
|
17
|
+
attribute :status, String, default: "queued"
|
|
17
18
|
|
|
18
19
|
validates_presence_of :name
|
|
19
20
|
validates_presence_of :clone_url
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
55
|
+
self.status = "queued"
|
|
56
|
+
self.output = ""
|
|
27
57
|
self.save
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
109
|
+
protected
|
|
110
|
+
|
|
111
|
+
def broadcast attr, *args
|
|
112
|
+
Bait.broadcast :build, attr, self.id, *args
|
|
113
|
+
end
|
|
48
114
|
end
|
|
49
115
|
end
|
data/lib/bait/cli.rb
CHANGED
|
@@ -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 "
|
|
15
|
+
puts "== Bait/#{Bait::VERSION} booting up..."
|
|
13
16
|
require 'bait/api'
|
|
14
17
|
Bait::Api.run!
|
|
15
18
|
end
|
|
File without changes
|
|
File without changes
|
|
@@ -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}
|
data/lib/bait/pubsub.rb
ADDED
|
@@ -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
|
data/lib/bait/tester.rb
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
data/lib/bait/views/builds.haml
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
%
|
|
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
|
-
|
|
20
|
-
%
|
|
21
|
-
|
|
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
|
data/lib/bait/views/layout.haml
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
%html
|
|
3
3
|
%head
|
|
4
4
|
%title Bait
|
|
5
|
-
%link{rel:'stylesheet', href:'
|
|
6
|
-
%
|
|
7
|
-
%script{src:'
|
|
8
|
-
%script{src:'
|
|
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
|
data/spec/lib/bait/api_spec.rb
CHANGED
|
@@ -66,11 +66,13 @@ describe Bait::Api do
|
|
|
66
66
|
|
|
67
67
|
describe "GET /" do
|
|
68
68
|
before { get '/' }
|
|
69
|
-
it
|
|
69
|
+
it "renders the loading screen" do
|
|
70
|
+
subject.body.should match /Loading/
|
|
71
|
+
end
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
describe "GET /build"
|
|
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 "
|
|
82
|
-
subject.body.should
|
|
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 "
|
|
98
|
+
describe "DELETE /build/:id" do
|
|
98
99
|
before do
|
|
99
100
|
@build = Bait::Build.create(name: "quickfox", clone_url:'...')
|
|
100
|
-
@sandbox = @build.
|
|
101
|
-
|
|
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 "
|
|
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
|
-
|
|
118
|
-
@build.reload
|
|
116
|
+
post "/build/#{@build.id}/retest"
|
|
119
117
|
end
|
|
120
118
|
it "queues the build for retesting" do
|
|
121
|
-
@build.should
|
|
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
|
data/spec/lib/bait/build_spec.rb
CHANGED
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
|
2
2
|
require 'bait/build'
|
|
3
3
|
|
|
4
4
|
describe Bait::Build do
|
|
5
|
-
|
|
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 "
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
build
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 "#
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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 "#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
100
|
-
before
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|