gorgon 0.0.1

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 (43) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +52 -0
  4. data/README.md +53 -0
  5. data/Rakefile +1 -0
  6. data/bin/gorgon +41 -0
  7. data/gorgon.gemspec +33 -0
  8. data/lib/gorgon.rb +6 -0
  9. data/lib/gorgon/amqp_service.rb +39 -0
  10. data/lib/gorgon/callback_handler.rb +21 -0
  11. data/lib/gorgon/configuration.rb +9 -0
  12. data/lib/gorgon/failures_printer.rb +37 -0
  13. data/lib/gorgon/g_logger.rb +22 -0
  14. data/lib/gorgon/host_state.rb +31 -0
  15. data/lib/gorgon/job.rb +26 -0
  16. data/lib/gorgon/job_definition.rb +24 -0
  17. data/lib/gorgon/job_state.rb +119 -0
  18. data/lib/gorgon/listener.rb +147 -0
  19. data/lib/gorgon/originator.rb +120 -0
  20. data/lib/gorgon/originator_logger.rb +36 -0
  21. data/lib/gorgon/originator_protocol.rb +65 -0
  22. data/lib/gorgon/pipe_manager.rb +55 -0
  23. data/lib/gorgon/progress_bar_view.rb +121 -0
  24. data/lib/gorgon/source_tree_syncer.rb +37 -0
  25. data/lib/gorgon/testunit_runner.rb +50 -0
  26. data/lib/gorgon/version.rb +3 -0
  27. data/lib/gorgon/worker.rb +103 -0
  28. data/lib/gorgon/worker_manager.rb +148 -0
  29. data/lib/gorgon/worker_watcher.rb +22 -0
  30. data/spec/callback_handler_spec.rb +77 -0
  31. data/spec/failures_printer_spec.rb +66 -0
  32. data/spec/host_state_spec.rb +65 -0
  33. data/spec/job_definition_spec.rb +20 -0
  34. data/spec/job_state_spec.rb +231 -0
  35. data/spec/listener_spec.rb +194 -0
  36. data/spec/originator_logger_spec.rb +40 -0
  37. data/spec/originator_protocol_spec.rb +134 -0
  38. data/spec/originator_spec.rb +134 -0
  39. data/spec/progress_bar_view_spec.rb +98 -0
  40. data/spec/source_tree_syncer_spec.rb +65 -0
  41. data/spec/worker_manager_spec.rb +23 -0
  42. data/spec/worker_spec.rb +114 -0
  43. metadata +270 -0
@@ -0,0 +1,66 @@
1
+ require 'gorgon/failures_printer'
2
+
3
+ describe FailuresPrinter do
4
+ let(:job_state) { stub("Job State", :add_observer => nil,
5
+ :is_job_complete? => true, :is_job_cancelled? => false,
6
+ :each_failed_test => nil,
7
+ :each_running_file => nil)}
8
+ let(:fd) {stub("File descriptor", :write => nil)}
9
+
10
+ subject do
11
+ FailuresPrinter.new(job_state)
12
+ end
13
+
14
+ it { should respond_to :update }
15
+
16
+ describe "#initialize" do
17
+ it "add its self to observers of job_state" do
18
+ job_state.should_receive(:add_observer)
19
+ FailuresPrinter.new job_state
20
+ end
21
+ end
22
+
23
+ describe "#update" do
24
+ before do
25
+ @printer = FailuresPrinter.new job_state
26
+ end
27
+
28
+ context "job is not completed nor cancelled" do
29
+ it "doesn't output anything" do
30
+ job_state.stub!(:is_job_complete? => false)
31
+ File.should_not_receive(:open)
32
+ @printer.update({})
33
+ end
34
+ end
35
+
36
+ context "job is completed" do
37
+ it "outputs failed tests return by job_state#each_failed_test" do
38
+ job_state.stub!(:each_failed_test).and_yield({:filename => "file1.rb"}).and_yield({:filename => "file2.rb"})
39
+ File.should_receive(:open).with(FailuresPrinter::OUTPUT_FILE, 'w+').and_yield fd
40
+ fd.should_receive(:write).with(Yajl::Encoder.encode(["file1.rb", "file2.rb"]))
41
+ @printer.update({})
42
+ end
43
+ end
44
+
45
+ context "job is cancelled" do
46
+ before do
47
+ job_state.stub!(:is_job_complete?).and_return(false)
48
+ job_state.stub!(:is_job_cancelled?).and_return(true)
49
+ end
50
+
51
+ it "outputs failed tests return by job_state#each_failed_test" do
52
+ job_state.stub!(:each_failed_test).and_yield({:filename => "file1.rb"}).and_yield({:filename => "file2.rb"})
53
+ File.should_receive(:open).with(FailuresPrinter::OUTPUT_FILE, 'w+').and_yield fd
54
+ fd.should_receive(:write).once.with(Yajl::Encoder.encode(["file1.rb", "file2.rb"]))
55
+ @printer.update({})
56
+ end
57
+
58
+ it "outputs still-running files returns by job_state#each_running_file" do
59
+ job_state.stub!(:each_running_file).and_yield("host1", "file1.rb").and_yield("host2", "file2.rb")
60
+ File.stub!(:open).and_yield fd
61
+ fd.should_receive(:write).once.with(Yajl::Encoder.encode(["file1.rb", "file2.rb"]))
62
+ @printer.update({})
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,65 @@
1
+ require 'gorgon/host_state'
2
+
3
+ describe HostState do
4
+ it { should respond_to(:file_started).with(2).arguments }
5
+ it { should respond_to(:file_finished).with(2).arguments }
6
+ it { should respond_to(:each_running_file).with(0).argument }
7
+ it { should respond_to(:total_running_workers).with(0).argument }
8
+
9
+ before do
10
+ @host_state = HostState.new
11
+ end
12
+
13
+ describe "#total_workers_running" do
14
+ it "returns 0 if there are no worker running files" do
15
+ @host_state.total_running_workers.should == 0
16
+ end
17
+
18
+ it "returns 1 if #file_started was called, but #file_finished has not been called with such a worker id" do
19
+ @host_state.file_started "worker1", "path/to/file.rb"
20
+ @host_state.total_running_workers.should == 1
21
+ end
22
+
23
+ it "returns 0 if #file_started and #file_finished were called for the same worker_id" do
24
+ @host_state.file_started "worker1", "path/to/file.rb"
25
+ @host_state.file_finished "worker1", "path/to/file.rb"
26
+ @host_state.total_running_workers.should == 0
27
+ end
28
+
29
+ it "returns 1 if #file_started and #file_finished were called for different worker id (worker1)" do
30
+ @host_state.file_started "worker1", "path/to/file.rb"
31
+ @host_state.file_started "worker2", "path/to/file2.rb"
32
+ @host_state.file_finished "worker2", "path/to/file2.rb"
33
+ @host_state.total_running_workers.should == 1
34
+ end
35
+ end
36
+
37
+ describe "#each_running_file" do
38
+ before do
39
+ @host_state.file_started "worker1", "path/to/file1.rb"
40
+ @host_state.file_started "worker2", "path/to/file2.rb"
41
+ end
42
+
43
+ context "when no #file_finished has been called" do
44
+ it "yields each currently running file" do
45
+ files = []
46
+ @host_state.each_running_file do |file|
47
+ files << file
48
+ end
49
+ files.should == ["path/to/file1.rb", "path/to/file2.rb"]
50
+ end
51
+ end
52
+
53
+ context "when #file_finished has been called for one of the workers" do
54
+ it "yields each currently running file" do
55
+ @host_state.file_finished "worker2", "path/to/file2.rb"
56
+
57
+ files = []
58
+ @host_state.each_running_file do |file|
59
+ files << file
60
+ end
61
+ files.should == ["path/to/file1.rb"]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,20 @@
1
+ require 'gorgon/job_definition'
2
+ require 'yajl'
3
+
4
+ describe JobDefinition do
5
+ before(:all) do
6
+ @json_parser = Yajl::Parser.new(:symbolize_keys => true)
7
+ end
8
+
9
+ describe "#to_json" do
10
+ it "should serialize itself to json" do
11
+ expected_hash = {:file_queue_name => "string 1", :reply_exchange_name => "string 2",
12
+ :source_tree_path => "string 3", :sync_exclude => "string 4", :callbacks => {}}
13
+
14
+ jd = JobDefinition.new(expected_hash)
15
+
16
+ @json_parser.parse(jd.to_json).should == expected_hash
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,231 @@
1
+ require 'gorgon/job_state'
2
+
3
+ describe JobState do
4
+ let(:payload) {
5
+ {:hostname => "host-name", :worker_id => "worker1", :filename => "path/file.rb",
6
+ :type => "pass", :failures => []}
7
+ }
8
+
9
+ let (:host_state){ stub("Host State", :file_started => nil, :file_finished => nil)}
10
+
11
+ subject { JobState.new 5 }
12
+ it { should respond_to :failed_files_count }
13
+ it { should respond_to :finished_files_count }
14
+ it { should respond_to(:file_started).with(1).argument }
15
+ it { should respond_to(:file_finished).with(1).argument }
16
+ it { should respond_to :cancel }
17
+ it { should respond_to :each_failed_test }
18
+ it { should respond_to :each_running_file }
19
+ it { should respond_to :total_running_hosts }
20
+ it { should respond_to :total_running_workers }
21
+ it { should respond_to :is_job_complete? }
22
+ it { should respond_to :is_job_cancelled? }
23
+
24
+ before do
25
+ @job_state = JobState.new 5
26
+ end
27
+
28
+ describe "#initialize" do
29
+ it "sets total files for job" do
30
+ @job_state.total_files.should be 5
31
+ end
32
+
33
+ it "sets remaining_files_count" do
34
+ @job_state.remaining_files_count.should be 5
35
+ end
36
+
37
+ it "sets failed_files_count to 0" do
38
+ @job_state.failed_files_count.should be 0
39
+ end
40
+
41
+ it "set state to starting" do
42
+ @job_state.state.should be :starting
43
+ end
44
+ end
45
+
46
+ describe "#finished_files_count" do
47
+ it "returns total_files - remaining_files_count" do
48
+ @job_state.finished_files_count.should be 0
49
+ end
50
+ end
51
+
52
+ describe "#file_started" do
53
+ it "change state to running after first start_file_message is received" do
54
+ @job_state.file_started({})
55
+ @job_state.state.should be :running
56
+ end
57
+
58
+ it "creates a new HostState object if this is the first file started by 'hostname'" do
59
+ HostState.should_receive(:new).and_return host_state
60
+ @job_state.file_started(payload)
61
+ end
62
+
63
+ it "doesn't create a new HostState object if this is not the first file started by 'hostname'" do
64
+ HostState.stub!(:new).and_return host_state
65
+ @job_state.file_started(payload)
66
+ HostState.should_not_receive(:new)
67
+ @job_state.file_started(payload)
68
+ end
69
+
70
+ it "calls #file_started on HostState object representing 'hostname'" do
71
+ HostState.stub!(:new).and_return host_state
72
+ host_state.should_receive(:file_started).with("worker_id", "file_name")
73
+ @job_state.file_started({:hostname => "hostname",
74
+ :worker_id => "worker_id",
75
+ :filename => "file_name"})
76
+ end
77
+
78
+ it "notify observers" do
79
+ @job_state.should_receive :notify_observers
80
+ @job_state.should_receive :changed
81
+ @job_state.file_started({})
82
+ end
83
+ end
84
+
85
+ describe "#file_finished" do
86
+ before do
87
+ HostState.stub!(:new).and_return host_state
88
+ @job_state.file_started payload
89
+ end
90
+
91
+ it "decreases remaining_files_count" do
92
+ lambda do
93
+ @job_state.file_finished payload
94
+ end.should(change(@job_state, :remaining_files_count).by(-1))
95
+
96
+ @job_state.total_files.should be 5
97
+ end
98
+
99
+ it "doesn't change failed_files_count if type test result is pass" do
100
+ lambda do
101
+ @job_state.file_finished payload
102
+ end.should_not change(@job_state, :failed_files_count)
103
+ @job_state.failed_files_count.should be 0
104
+ end
105
+
106
+ it "increments failed_files_count if type is failed" do
107
+ lambda do
108
+ @job_state.file_finished payload.merge({:type => "fail", :failures => ["Failure messages"]})
109
+ end.should change(@job_state, :failed_files_count).by(1)
110
+ end
111
+
112
+ it "notify observers" do
113
+ @job_state.should_receive :notify_observers
114
+ @job_state.should_receive :changed
115
+ @job_state.file_finished payload
116
+ end
117
+
118
+ it "raises if job already complete" do
119
+ finish_job
120
+ lambda do
121
+ @job_state.file_finished payload
122
+ end.should raise_error
123
+ end
124
+
125
+ it "raises if job was cancelled" do
126
+ @job_state.cancel
127
+ lambda do
128
+ @job_state.file_finished payload
129
+ end.should raise_error
130
+ end
131
+
132
+ it "tells to the proper HostState object that a file finished in that host" do
133
+ HostState.stub!(:new).and_return host_state
134
+ @job_state.file_started({:hostname => "hostname",
135
+ :worker_id => "worker_id",
136
+ :filename => "file_name"})
137
+ host_state.should_receive(:file_finished).with("worker_id", "file_name")
138
+ @job_state.file_finished({:hostname => "hostname",
139
+ :worker_id => "worker_id",
140
+ :filename => "file_name"})
141
+ end
142
+ end
143
+
144
+ describe "#is_job_complete?" do
145
+ it "returns false if remaining_files_count != 0" do
146
+ @job_state.is_job_complete?.should be_false
147
+ end
148
+
149
+ it "returns true if remaining_files_count == 0" do
150
+ finish_job
151
+ @job_state.is_job_complete?.should be_true
152
+ end
153
+ end
154
+
155
+ describe "#cancel and is_job_cancelled?" do
156
+ it "cancels job" do
157
+ @job_state.is_job_cancelled?.should be_false
158
+ @job_state.cancel
159
+ @job_state.is_job_cancelled?.should be_true
160
+ end
161
+
162
+ it "notify observers when cancelling" do
163
+ @job_state.should_receive :changed
164
+ @job_state.should_receive :notify_observers
165
+ @job_state.cancel
166
+ end
167
+ end
168
+
169
+ describe "#each_failed_test" do
170
+ before do
171
+ @job_state.file_started payload
172
+ end
173
+
174
+ it "returns failed tests info" do
175
+ @job_state.file_finished payload.merge({:type => "fail", :failures => ["Failure messages"]})
176
+ @job_state.each_failed_test do |test|
177
+ test[:failures].should == ["Failure messages"]
178
+ end
179
+ end
180
+ end
181
+
182
+ describe "#each_running_file" do
183
+ before do
184
+ @job_state.file_started payload
185
+ @job_state.file_started payload.merge({ :hostname => "host2",
186
+ :filename => "path/file2.rb",
187
+ :worker_id => "worker2"})
188
+ end
189
+
190
+ it "returns each running file" do
191
+ hosts_files = {}
192
+ @job_state.each_running_file do |hostname, filename|
193
+ hosts_files[hostname] = filename
194
+ end
195
+ hosts_files.size.should == 2
196
+ hosts_files["host-name"].should == "path/file.rb"
197
+ hosts_files["host2"].should == "path/file2.rb"
198
+ end
199
+ end
200
+
201
+ describe "#total_running_hosts" do
202
+ it "returns total number of hosts that has workers running files" do
203
+ @job_state.file_started payload
204
+ @job_state.file_started payload.merge({:worker_id => "worker2"})
205
+ @job_state.file_started payload.merge({ :hostname => "host2",
206
+ :filename => "path/file2.rb",
207
+ :worker_id => "worker1"})
208
+ @job_state.total_running_hosts.should == 2
209
+ end
210
+ end
211
+
212
+ describe "#total_running_workers" do
213
+ it "returns total number of workers running accross all hosts" do
214
+ @job_state.file_started payload
215
+ @job_state.file_started payload.merge({:worker_id => "worker2"})
216
+ @job_state.file_started payload.merge({ :hostname => "host2",
217
+ :filename => "path/file2.rb",
218
+ :worker_id => "worker1"})
219
+ @job_state.total_running_workers.should == 3
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def finish_job
226
+ 5.times do
227
+ @job_state.file_started payload
228
+ @job_state.file_finished payload
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,194 @@
1
+ require 'gorgon/listener'
2
+
3
+ describe Listener do
4
+ let(:connection_information) { double }
5
+ let(:queue) { stub("Bunny Queue", :bind => nil) }
6
+ let(:exchange) { stub("Bunny Exchange") }
7
+ let(:bunny) { stub("Bunny", :start => nil, :queue => queue, :exchange => exchange) }
8
+
9
+ before do
10
+ Bunny.stub(:new).and_return(bunny)
11
+ Listener.any_instance.stub(:configuration => {})
12
+ Listener.any_instance.stub(:connection_information => connection_information)
13
+ @stub_logger = stub :info => true, :datetime_format= => ""
14
+ Logger.stub(:new).and_return(@stub_logger)
15
+ end
16
+
17
+ describe "initialization" do
18
+
19
+ before do
20
+ Listener.any_instance.stub(:connect => nil, :initialize_personal_job_queue => nil)
21
+ end
22
+
23
+ it "connects" do
24
+ Listener.any_instance.should_receive(:connect)
25
+ Listener.new
26
+ end
27
+
28
+ it "initializes the personal job queue" do
29
+ Listener.any_instance.should_receive(:initialize_personal_job_queue)
30
+ Listener.new
31
+ end
32
+ end
33
+
34
+ describe "logging to a file" do
35
+ context "passing a log file path in the configuration" do
36
+ before do
37
+ Listener.any_instance.stub(:configuration).and_return({:log_file => 'listener.log'})
38
+ end
39
+
40
+ it "should use 'log_file' from the configuration as the log file" do
41
+ Logger.should_receive(:new).with('listener.log')
42
+ Listener.new
43
+ end
44
+
45
+ it "should log to 'log_file'" do
46
+ @stub_logger.should_receive(:info).with("Listener initialized")
47
+
48
+ Listener.new
49
+ end
50
+ end
51
+
52
+ context "passing a literal '-'' as the path in the configuration" do
53
+ before do
54
+ Listener.any_instance.stub(:configuration).and_return({:log_file => "-"})
55
+ end
56
+
57
+ it "logs to stdout" do
58
+ Logger.should_receive(:new).with($stdout)
59
+ Listener.new
60
+ end
61
+ end
62
+
63
+ context "without specifying a log file path" do
64
+ it "should not log" do
65
+ Logger.should_not_receive(:new)
66
+ @stub_logger.should_not_receive(:info)
67
+
68
+ Listener.new
69
+ end
70
+ end
71
+ end
72
+
73
+ context "initialized" do
74
+ let(:listener) { Listener.new }
75
+
76
+ describe "#connect" do
77
+ it "connects" do
78
+ Bunny.should_receive(:new).with(connection_information).and_return(bunny)
79
+ bunny.should_receive(:start)
80
+
81
+ listener.connect
82
+ end
83
+ end
84
+
85
+ describe "#initialize_personal_job_queue" do
86
+ it "creates the job queue" do
87
+ bunny.should_receive(:queue).with("", :exclusive => true)
88
+ listener.initialize_personal_job_queue
89
+ end
90
+
91
+ it "binds the exchange to the queue" do
92
+ bunny.should_receive(:exchange).with("gorgon.jobs", :type => :fanout).and_return(exchange)
93
+ queue.should_receive(:bind).with(exchange)
94
+ listener.initialize_personal_job_queue
95
+ end
96
+ end
97
+
98
+ describe "#poll" do
99
+
100
+ let(:empty_queue) { {:payload => :queue_empty} }
101
+ let(:job_payload) { {:payload => "Job"} }
102
+ before do
103
+ listener.stub(:run_job)
104
+ end
105
+
106
+ context "empty queue" do
107
+ before do
108
+ queue.stub(:pop => empty_queue)
109
+ end
110
+
111
+ it "checks the job queue" do
112
+ queue.should_receive(:pop).and_return(empty_queue)
113
+ listener.poll
114
+ end
115
+
116
+ it "returns false" do
117
+ listener.poll.should be_false
118
+ end
119
+ end
120
+
121
+ context "job pending on queue" do
122
+ before do
123
+ queue.stub(:pop => job_payload)
124
+ end
125
+
126
+ it "starts a new job when there is a job payload" do
127
+ queue.should_receive(:pop).and_return(job_payload)
128
+ listener.should_receive(:run_job).with(job_payload[:payload])
129
+ listener.poll
130
+ end
131
+
132
+ it "returns true" do
133
+ listener.poll.should be_true
134
+ end
135
+ end
136
+ end
137
+
138
+ describe "#run_job" do
139
+ let(:payload) {{
140
+ :source_tree_path => "path/to/source",
141
+ :sync_exclude => ["log"], :callbacks => {:a_callback => "path/to/callback"}
142
+ }}
143
+
144
+ let(:syncer) { stub("SourceTreeSyncer", :sync => nil, :exclude= => nil,
145
+ :remove_temp_dir => nil, :sys_command => "rsync ...")}
146
+
147
+ let(:io) { stub("IO object", :write => nil, :close => nil)}
148
+ let(:process_status) { stub("Process Status", :exitstatus => 0)}
149
+ let(:callback_handler) { stub("Callback Handler", :after_sync => nil) }
150
+
151
+ before do
152
+ @listener = Listener.new
153
+ @json_payload = Yajl::Encoder.encode(payload)
154
+ stub_classes
155
+ end
156
+
157
+ it "copy source tree" do
158
+ SourceTreeSyncer.should_receive(:new).once.with("path/to/source").and_return syncer
159
+ syncer.should_receive(:exclude=).with(["log"])
160
+ syncer.should_receive(:sync)
161
+ @listener.run_job(@json_payload)
162
+ end
163
+
164
+ it "remove temp source directory when complete" do
165
+ syncer.should_receive(:remove_temp_dir)
166
+ @listener.run_job(@json_payload)
167
+ end
168
+
169
+ it "creates a CallbackHandler object using callbacks passed in payload" do
170
+ CallbackHandler.should_receive(:new).once.with({:a_callback => "path/to/callback"}).and_return(callback_handler)
171
+ @listener.run_job(@json_payload)
172
+ end
173
+
174
+ it "calls after_sync callback" do
175
+ callback_handler.should_receive(:after_sync).once
176
+ @listener.run_job(@json_payload)
177
+ end
178
+
179
+ it "uses Bundler#with_clean_env so the workers load new gems that could have been installed in after_sync" do
180
+ Bundler.should_receive(:with_clean_env).and_yield
181
+ @listener.run_job(@json_payload)
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def stub_classes
188
+ SourceTreeSyncer.stub!(:new).and_return syncer
189
+ CallbackHandler.stub!(:new).and_return callback_handler
190
+ Open4.stub!(:popen4).and_return([1, io])
191
+ Process.stub!(:waitpid2).and_return([0, process_status])
192
+ end
193
+ end
194
+ end