rbuilder 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +47 -0
- data/bin/rbuilder +65 -0
- data/conf/example.conf +2 -0
- data/rbuilder/build.rb +50 -0
- data/rbuilder/build_command.rb +33 -0
- data/rbuilder/build_configuration.rb +21 -0
- data/rbuilder/build_description.rb +46 -0
- data/rbuilder/build_report.rb +40 -0
- data/rbuilder/build_status.rb +39 -0
- data/rbuilder/builder.rb +45 -0
- data/rbuilder/gui.rb +6 -0
- data/rbuilder/gui/build_details_pane.rb +105 -0
- data/rbuilder/gui/build_list_view_entry.rb +33 -0
- data/rbuilder/gui/builder_list_view.rb +92 -0
- data/rbuilder/gui/builder_monitor_window.rb +112 -0
- data/rbuilder/gui/images.rb +5 -0
- data/rbuilder/gui/images/broken_builder.gif +0 -0
- data/rbuilder/gui/images/building.gif +0 -0
- data/rbuilder/gui/images/failed.gif +0 -0
- data/rbuilder/gui/images/ok.gif +0 -0
- data/rbuilder/gui/images/ok_builder.gif +0 -0
- data/rbuilder/lib/observer.rb +7 -0
- data/rbuilder/libs.rb +4 -0
- data/rbuilder/revision_committed.rb +21 -0
- data/rbuilder/source_control.rb +3 -0
- data/rbuilder/source_control/status.rb +23 -0
- data/rbuilder/source_control/svn.rb +64 -0
- data/rbuilder/source_control/tracker.rb +36 -0
- data/tests/gui/test_build_details_pane.rb +118 -0
- data/tests/gui/test_builder_list_view.rb +202 -0
- data/tests/gui/test_builder_list_view_entry.rb +79 -0
- data/tests/gui/test_builder_monitor_window.rb +81 -0
- data/tests/source_control/test_source_control.rb +52 -0
- data/tests/source_control/test_svn.rb +60 -0
- data/tests/test_build.rb +111 -0
- data/tests/test_build_command.rb +57 -0
- data/tests/test_build_configuration.rb +31 -0
- data/tests/test_build_description.rb +106 -0
- data/tests/test_build_report.rb +56 -0
- data/tests/test_builder.rb +54 -0
- data/tests/test_revision_comitted.rb +30 -0
- metadata +104 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
module Gui
|
2
|
+
class BuildListViewEntry
|
3
|
+
attr_reader :iterator, :build
|
4
|
+
def self.create(build, iterator)
|
5
|
+
entry = BuildListViewEntry.new(build)
|
6
|
+
entry.attach_to(iterator)
|
7
|
+
return entry
|
8
|
+
end
|
9
|
+
def initialize(build)
|
10
|
+
@build = build
|
11
|
+
build.add_observer(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def attach_to(iterator)
|
15
|
+
@iterator = iterator
|
16
|
+
update
|
17
|
+
end
|
18
|
+
|
19
|
+
def update
|
20
|
+
iterator[0] = self
|
21
|
+
iterator[1] = build.archive
|
22
|
+
iterator[2] = build.revision.to_s
|
23
|
+
iterator[3] = build.committer.to_s
|
24
|
+
iterator[4] = build.status.to_s
|
25
|
+
iterator[5] = build.summary.to_s
|
26
|
+
end
|
27
|
+
Colors = {BuildStatus.ok => 'green', BuildStatus.failed => 'red', BuildStatus.building => 'yellow'}
|
28
|
+
def update_renderer_style(renderer)
|
29
|
+
color = Colors[build.status]
|
30
|
+
renderer.background=color if color
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Gui
|
2
|
+
class BuilderListView
|
3
|
+
attr_reader :gtk_container,:tree_store, :tree_view
|
4
|
+
attr_accessor :monitor_window
|
5
|
+
|
6
|
+
def initialize(builder)
|
7
|
+
initialize_ui_elements
|
8
|
+
builder.each_description {|build_description| create_build_description_list_view(build_description)}
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize_ui_elements
|
12
|
+
@tree_store = Gtk::TreeStore.new(BuildListViewEntry,String,String,String,String,String)
|
13
|
+
@tree_view = Gtk::TreeView.new(tree_store)
|
14
|
+
@gtk_container = Gtk::ScrolledWindow.new
|
15
|
+
gtk_container << tree_view
|
16
|
+
|
17
|
+
renderer = Gtk::CellRendererText.new
|
18
|
+
%w{Archive Revision Committer Status Summary}.each_with_index do |column, index|
|
19
|
+
col = Gtk::TreeViewColumn.new(column, renderer, :text => index+1)
|
20
|
+
col.resizable=true
|
21
|
+
col.set_cell_data_func(renderer) do |col, renderer, model, iter|
|
22
|
+
iter[0].update_renderer_style(renderer)
|
23
|
+
end
|
24
|
+
|
25
|
+
tree_view.append_column(col)
|
26
|
+
end
|
27
|
+
tree_view.selection.set_select_function do |selection, model, path, currently_selected|
|
28
|
+
self.select_build(self.build_for_iterator(model.get_iter(path)))
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_build_description_list_view(build_description)
|
34
|
+
BuildDescriptionListView.new(build_description, self, tree_store)
|
35
|
+
end
|
36
|
+
|
37
|
+
def select_build(build)
|
38
|
+
@selected_build = build
|
39
|
+
monitor_window.build_selected(build)
|
40
|
+
end
|
41
|
+
|
42
|
+
def replace_build_selection(new_selected_build, build_selection_to_replace)
|
43
|
+
select_build(new_selected_build) if build_selection_to_replace == selected_build || selected_build == nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_for_iterator(iterator)
|
47
|
+
iterator[0].build
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
attr_reader :selected_build
|
52
|
+
end
|
53
|
+
|
54
|
+
class BuildDescriptionListView
|
55
|
+
attr_reader :tree_store, :current_build_view_entry, :build_description, :builder_list_view
|
56
|
+
def initialize(builder, builder_list_view, tree_store = nil)
|
57
|
+
@listeners = []
|
58
|
+
@build_description = @builder = builder
|
59
|
+
@builder_list_view = builder_list_view
|
60
|
+
@builder.add_observer(self)
|
61
|
+
@tree_store = tree_store
|
62
|
+
end
|
63
|
+
|
64
|
+
def update(options = {})
|
65
|
+
replace_build_selection(options[:new_build])
|
66
|
+
new_build_list_view_entry(options[:new_build])
|
67
|
+
end
|
68
|
+
|
69
|
+
def replace_build_selection(new_build)
|
70
|
+
return unless builder_list_view
|
71
|
+
builder_list_view.replace_build_selection(new_build, current_build)
|
72
|
+
end
|
73
|
+
|
74
|
+
def current_build
|
75
|
+
return current_build_view_entry.build if current_build_view_entry
|
76
|
+
end
|
77
|
+
|
78
|
+
def new_build_list_view_entry(build)
|
79
|
+
if current_build_view_entry
|
80
|
+
current_build_view_entry
|
81
|
+
new_build_list_entry = BuildListViewEntry.create(build,current_build_view_entry.iterator)
|
82
|
+
current_build_view_entry.attach_to(tree_store.prepend(current_build_view_entry.iterator))
|
83
|
+
@current_build_view_entry = new_build_list_entry
|
84
|
+
else
|
85
|
+
@current_build_view_entry = BuildListViewEntry.create(build,tree_store.append(nil))
|
86
|
+
end
|
87
|
+
current_build_view_entry
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'gui/images'
|
2
|
+
module Gui
|
3
|
+
class BuilderMonitorWindow
|
4
|
+
|
5
|
+
class StatusIcon
|
6
|
+
attr_reader :status
|
7
|
+
def initialize(menu)
|
8
|
+
@icon = Gtk::StatusIcon.new
|
9
|
+
|
10
|
+
@icon.signal_connect('popup_menu', menu) do |widget,button, time|
|
11
|
+
if button == 3
|
12
|
+
menu.popup(nil,nil,button, time)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
self.ok
|
16
|
+
end
|
17
|
+
|
18
|
+
def ok
|
19
|
+
self.status ='ok_builder'
|
20
|
+
end
|
21
|
+
|
22
|
+
def broken
|
23
|
+
self.status ='broken_builder'
|
24
|
+
end
|
25
|
+
|
26
|
+
def status=(new_status)
|
27
|
+
@status = new_status
|
28
|
+
@icon.file=Images.file("#{status}.gif")
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_activate(&block)
|
33
|
+
@icon.signal_connect('activate',&block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
attr_reader :window, :status_icon, :builder
|
39
|
+
|
40
|
+
def initialize(builder, builder_list_view, build_details_pane)
|
41
|
+
@builder = builder
|
42
|
+
builder.add_observer(self)
|
43
|
+
@build_details_pane = build_details_pane
|
44
|
+
builder_list_view.monitor_window = self
|
45
|
+
initialize_widgets(builder_list_view)
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize_widgets(builder_list_view)
|
49
|
+
initialize_status_icon initialize_menu
|
50
|
+
@window = Gtk::Window.new(Gtk::Window::TOPLEVEL)
|
51
|
+
panes = Gtk::VPaned.new
|
52
|
+
panes.pack1(builder_list_view.gtk_container,false, false)
|
53
|
+
panes.pack2(build_details_pane.gtk_container,false, false)
|
54
|
+
window.signal_connect("delete_event") { self.hide }
|
55
|
+
window.add(panes)
|
56
|
+
window.title = "rbuilder monitor"
|
57
|
+
window.width_request = 600
|
58
|
+
window.height_request = 500
|
59
|
+
window.icon_list = [Gdk::Pixbuf.new(Images.file('ok_builder.gif'))]
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize_menu
|
63
|
+
menu = Gtk::Menu.new
|
64
|
+
menu.append(show = Gtk::MenuItem.new("Show"))
|
65
|
+
menu.append(hide = Gtk::MenuItem.new("Hide"))
|
66
|
+
menu.append(exit = Gtk::MenuItem.new("Exit"))
|
67
|
+
menu.show_all
|
68
|
+
show.signal_connect('activate') {self.show}
|
69
|
+
hide.signal_connect('activate') {self.hide}
|
70
|
+
exit.signal_connect('activate') {Gtk.main_quit}
|
71
|
+
menu
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize_status_icon(menu)
|
75
|
+
@status_icon = StatusIcon.new(menu)
|
76
|
+
status_icon.on_activate do
|
77
|
+
toggle
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_selected(build)
|
82
|
+
@selected_build = build
|
83
|
+
@build_details_pane.attach_to build
|
84
|
+
end
|
85
|
+
|
86
|
+
def show
|
87
|
+
window.show_all
|
88
|
+
end
|
89
|
+
|
90
|
+
def hide
|
91
|
+
window.hide_all
|
92
|
+
end
|
93
|
+
|
94
|
+
def toggle
|
95
|
+
if window.visible?
|
96
|
+
window.hide_all
|
97
|
+
else
|
98
|
+
window.show_all
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def update
|
103
|
+
if builder.status == BuilderState.ok
|
104
|
+
status_icon.ok
|
105
|
+
else
|
106
|
+
status_icon.broken
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_reader :build_details_pane
|
111
|
+
end
|
112
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/rbuilder/libs.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
class RevisionCommitted
|
3
|
+
attr_reader :committer, :revision
|
4
|
+
def initialize(revision, committer)
|
5
|
+
@revision = revision
|
6
|
+
@committer = committer
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.from_source_control_status(status)
|
10
|
+
self.new(status.revision, status.committer)
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
return false unless other.class == self.class
|
15
|
+
self.revision == other.revision && self.committer == other.committer
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"Revision #{revision} committed by #{committer}"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SourceControl
|
2
|
+
class Status
|
3
|
+
attr_reader :revision, :committer
|
4
|
+
def initialize(revision, committer)
|
5
|
+
@revision = revision
|
6
|
+
@committer = committer
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
other.class == self.class &&
|
12
|
+
other.revision == self.revision &&
|
13
|
+
other.committer == self.committer
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"(#{revision},#{committer})"
|
18
|
+
end
|
19
|
+
def inspect
|
20
|
+
"Status#{to_s}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
module SourceControl
|
3
|
+
def self.svn(working_dir)
|
4
|
+
new(svn_backend(working_dir))
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.svn_backend(working_dir)
|
8
|
+
Svn.new(working_dir)
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
class Svn
|
13
|
+
attr_reader :working_dir
|
14
|
+
def initialize(working_dir)
|
15
|
+
@working_dir = working_dir
|
16
|
+
validate_working_dir
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_working_dir
|
20
|
+
unless File.directory?(working_dir)
|
21
|
+
raise DirectoryInvalidException.new("directory does not exist please create a valid checkout first")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def archive
|
26
|
+
return @archive if @archive
|
27
|
+
@archive = parse_archive(run_info)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_archive(repo_info)
|
31
|
+
repo_info =~ /^(Repository Root: )(.*)$/
|
32
|
+
line = $2
|
33
|
+
line = strip_server(line)
|
34
|
+
return strip_trunk(line)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_info
|
38
|
+
info_output = run_info
|
39
|
+
revision = info_output[/^Revision:(.*)$/].split(':')[1]
|
40
|
+
committer = info_output[/^Last Changed Author:(.*)$/].split(':')[1].strip
|
41
|
+
Status.new(revision.to_i, committer)
|
42
|
+
end
|
43
|
+
|
44
|
+
def strip_server(line)
|
45
|
+
return line unless line =~ /(svn.*:\/\/[^\/]*\/)(.*)/
|
46
|
+
return $2
|
47
|
+
end
|
48
|
+
|
49
|
+
def strip_trunk(line)
|
50
|
+
return line unless line =~ /(.*)(\/trunk)/
|
51
|
+
return $1
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
def run_info
|
56
|
+
FileUtils.cd @working_dir do
|
57
|
+
`svn up`
|
58
|
+
return `svn info`
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SourceControl
|
2
|
+
class DirectoryInvalidException < Exception; end
|
3
|
+
|
4
|
+
class Tracker
|
5
|
+
attr_reader :backend
|
6
|
+
|
7
|
+
def initialize(backend)
|
8
|
+
@backend = backend
|
9
|
+
@current_revision = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def report_change_to(builder)
|
13
|
+
source_control_info = backend.get_info
|
14
|
+
if source_control_info.revision != current_revision
|
15
|
+
builder.new_revision_committed(source_control_info)
|
16
|
+
@current_revision = source_control_info.revision
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def working_dir
|
21
|
+
backend.working_dir
|
22
|
+
end
|
23
|
+
|
24
|
+
def archive
|
25
|
+
backend.archive
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
attr_reader :current_revision
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.new(source_control_backend)
|
33
|
+
Tracker.new(source_control_backend)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'mocha'
|
4
|
+
require File.join(File.dirname(__FILE__), '..', 'builder_test_env')
|
5
|
+
require 'rbuilder/gui'
|
6
|
+
|
7
|
+
module Gui
|
8
|
+
class TestBuildDetailsPane < Test::Unit::TestCase
|
9
|
+
attr_reader :pane, :build, :build_report, :status_widget, :log_widget
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@build_report = stub('initial_build_report', :delete_observer => nil, :add_observer => nil, :log => '')
|
13
|
+
@build = stub_everything('initial_build')
|
14
|
+
build.stubs(:report).returns(build_report)
|
15
|
+
build.stubs(:archive).returns "the_archive"
|
16
|
+
build.stubs(:status).returns BuildStatus.ok
|
17
|
+
build.stubs(:reason).returns "some_reason"
|
18
|
+
@status_widget = stub('status_widget', :gtk_container => Gtk::Label.new, :status= => nil)
|
19
|
+
@log_widget = stub('log_widget', :attach_to => nil, :gtk_container => Gtk::Label.new)
|
20
|
+
@pane = BuildDetailsPane.new(build, status_widget, log_widget)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_contains_label_with_archive_name
|
24
|
+
assert_equal 'the_archive', pane.archive_label.text
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_contains_label_with_reason
|
28
|
+
assert_equal 'some_reason', pane.reason_label.text
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_observes_build_upon_creation
|
32
|
+
build.expects(:add_observer).with(instance_of(BuildDetailsPane))
|
33
|
+
pane = BuildDetailsPane.new(build)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_observes_build_report_and_build_upon_attach_to
|
37
|
+
build.expects(:delete_observer).with(pane)
|
38
|
+
other_build_report = mock('other_build_report')
|
39
|
+
other_build = stub('other_build', :report => other_build_report, :archive => 'other_archive', :reason => 'some_other_reason')
|
40
|
+
|
41
|
+
other_build.expects(:add_observer).with(pane)
|
42
|
+
log_widget.expects(:attach_to).with(other_build_report)
|
43
|
+
pane.expects :update
|
44
|
+
pane.attach_to(other_build)
|
45
|
+
assert_equal 'other_archive', pane.archive_label.text
|
46
|
+
assert_equal 'some_other_reason', pane.reason_label.text
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def test_update_pushes_status_information_to_attached_widgets
|
51
|
+
status_widget.expects(:status=).with(BuildStatus.ok)
|
52
|
+
pane.update
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class TestStatusWidget < Test::Unit::TestCase
|
58
|
+
attr_reader :widget
|
59
|
+
def setup
|
60
|
+
@widget = StatusWidget.new(BuildStatus.ok)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_sets_status_in_image
|
64
|
+
assert_equal Gtk::Image, widget.gtk_container.class
|
65
|
+
assert_equal "ok.gif", File.basename(widget.gtk_container.file)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_changing_status_changes_the_file_not_the_label
|
69
|
+
expected_gtk_container = widget.gtk_container
|
70
|
+
|
71
|
+
widget.status = BuildStatus.failed
|
72
|
+
assert_equal "failed.gif", File.basename(widget.gtk_container.file)
|
73
|
+
assert_equal expected_gtk_container, widget.gtk_container
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class TestLogWidget < Test::Unit::TestCase
|
78
|
+
attr_reader :widget, :build_report
|
79
|
+
def setup
|
80
|
+
@build_report = stub_everything
|
81
|
+
@widget = LogWidget.new(build_report)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_obseves_build_erport_upon_creation
|
85
|
+
build_report.expects(:add_observer).with(instance_of(LogWidget))
|
86
|
+
widget = LogWidget.new(build_report)
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_obseves_build_erport_upon_attach
|
90
|
+
new_build_report = mock
|
91
|
+
build_report.expects(:delete_observer).with(widget)
|
92
|
+
new_build_report.expects(:add_observer).with(widget)
|
93
|
+
widget.expects :update
|
94
|
+
widget.attach_to(new_build_report)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_update_log_line_is_added_to_text
|
98
|
+
new_log_line = "bla di bla"
|
99
|
+
another_log_line = "rob is gek"
|
100
|
+
|
101
|
+
widget.update(:log_line => new_log_line)
|
102
|
+
assert_match new_log_line, widget.text
|
103
|
+
assert_no_match(/#{another_log_line}/, widget.text)
|
104
|
+
|
105
|
+
widget.update(:log_line => another_log_line)
|
106
|
+
assert_match another_log_line, widget.text
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_update_sets_complete_text
|
110
|
+
the_log = "some\nlog\nlines"
|
111
|
+
build_report.stubs :log => the_log
|
112
|
+
widget.update
|
113
|
+
assert_equal the_log, widget.text
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
end
|