gorgon 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gorgon (0.0.1)
4
+ gorgon (0.0.2)
5
5
  amqp (~> 0.9.7)
6
6
  awesome_print
7
7
  bunny (~> 0.8.0)
data/bin/gorgon CHANGED
@@ -3,6 +3,8 @@ require 'gorgon/originator'
3
3
  require 'gorgon/listener'
4
4
  require 'gorgon/worker_manager'
5
5
 
6
+ WELCOME_MSG = "Welcome to Gorgon #{Gorgon::VERSION}"
7
+
6
8
  def start
7
9
  o = Originator.new
8
10
  o.originate
@@ -25,8 +27,12 @@ end
25
27
 
26
28
  def usage
27
29
  #print instructions on how to use gorgon
30
+ puts "\tstart - remotely runs all tests specified in gorgon.json"
31
+ puts "\tlisten - starts a listener process using the settings in gorgon_listener.json"
28
32
  end
29
33
 
34
+ puts WELCOME_MSG
35
+
30
36
  case ARGV[0]
31
37
  when nil
32
38
  start
@@ -36,6 +42,10 @@ when "listen"
36
42
  listen
37
43
  when "manage_workers"
38
44
  manage_workers
45
+ when "help"
46
+ usage
39
47
  else
48
+ puts "Unknown command!"
40
49
  usage
50
+ exit 1
41
51
  end
@@ -5,12 +5,13 @@ require 'observer'
5
5
  class JobState
6
6
  include Observable
7
7
 
8
- attr_reader :total_files, :remaining_files_count, :state
8
+ attr_reader :total_files, :remaining_files_count, :state, :crashed_hosts
9
9
 
10
10
  def initialize total_files
11
11
  @total_files = total_files
12
12
  @remaining_files_count = total_files
13
13
  @failed_tests = []
14
+ @crashed_hosts = []
14
15
  @hosts = {}
15
16
 
16
17
  if @remaining_files_count > 0
@@ -55,6 +56,12 @@ class JobState
55
56
  notify_observers payload
56
57
  end
57
58
 
59
+ def gorgon_crash_message payload
60
+ @crashed_hosts << payload[:hostname]
61
+ changed
62
+ notify_observers payload
63
+ end
64
+
58
65
  def cancel
59
66
  @remaining_files_count = 0
60
67
  @state = :cancelled
@@ -114,6 +121,6 @@ class JobState
114
121
  end
115
122
 
116
123
  def failed_test? payload
117
- payload[:type] == "fail"
124
+ payload[:type] == "fail" || payload[:type] == "crash"
118
125
  end
119
126
  end
@@ -10,6 +10,7 @@ require "awesome_print"
10
10
  require "open4"
11
11
  require "tmpdir"
12
12
  require "socket"
13
+ require "bundler"
13
14
 
14
15
  class Listener
15
16
  include Configuration
@@ -25,6 +26,7 @@ class Listener
25
26
  end
26
27
 
27
28
  def listen
29
+ at_exit_hook
28
30
  log "Waiting for jobs..."
29
31
  while true
30
32
  sleep 2 unless poll
@@ -61,7 +63,7 @@ class Listener
61
63
  @callback_handler = CallbackHandler.new(@job_definition.callbacks)
62
64
  copy_source_tree(@job_definition.source_tree_path, @job_definition.sync_exclude)
63
65
 
64
- if !run_after_sync
66
+ if !@syncer.success? || !run_after_sync
65
67
  clean_up
66
68
  return
67
69
  end
@@ -73,10 +75,14 @@ class Listener
73
75
  clean_up
74
76
  end
75
77
 
78
+ def at_exit_hook
79
+ at_exit { log "Listener will exit!"}
80
+ end
81
+
76
82
  private
77
83
 
78
84
  def run_after_sync
79
- log "Running after_sync callback"
85
+ log "Running after_sync callback..."
80
86
  begin
81
87
  @callback_handler.after_sync
82
88
  rescue Exception => e
@@ -84,7 +90,7 @@ class Listener
84
90
  log_error e.message
85
91
  log_error "\n" + e.backtrace.join("\n")
86
92
 
87
- reply = {:type => :crash,
93
+ reply = {:type => :exception,
88
94
  :hostname => Socket.gethostname,
89
95
  :message => "after_sync callback failed. Please, check your script in #{@job_definition.callbacks[:after_sync]}. Message: #{e.message}",
90
96
  :backtrace => e.backtrace.join("\n")
@@ -99,14 +105,14 @@ class Listener
99
105
  log "Downloading source tree to temp directory..."
100
106
  @syncer = SourceTreeSyncer.new source_tree_path
101
107
  @syncer.exclude = exclude
102
- if @syncer.sync
108
+ @syncer.sync
109
+ if @syncer.success?
103
110
  log "Command '#{@syncer.sys_command}' completed successfully."
104
111
  else
105
- #TODO handle error:
106
- # - Discard job
107
- # - Let the originator know about the error
108
- # - Wait for the next job
112
+ send_crash_message @syncer.output, @syncer.errors
109
113
  log_error "Command '#{@syncer.sys_command}' failed!"
114
+ log_error "Stdout:\n#{@syncer.output}"
115
+ log_error "Stderr:\n#{@syncer.errors}"
110
116
  end
111
117
  end
112
118
 
@@ -115,7 +121,7 @@ class Listener
115
121
  end
116
122
 
117
123
  def fork_worker_manager
118
- log "Forking Worker Manager"
124
+ log "Forking Worker Manager..."
119
125
  ENV["GORGON_CONFIG_PATH"] = @listener_config_filename
120
126
  pid, stdin, stdout, stderr = Open4::popen4 "bundle exec gorgon manage_workers"
121
127
  stdin.write(@job_definition.to_json)
@@ -129,11 +135,7 @@ class Listener
129
135
  error_msg = stderr.read
130
136
  log_error "ERROR MSG: #{error_msg}"
131
137
 
132
- reply = {:type => :crash,
133
- :hostname => Socket.gethostname,
134
- :stdout => stdout.read,
135
- :stderr => error_msg}
136
- @reply_exchange.publish(Yajl::Encoder.encode(reply))
138
+ send_crash_message stdout.read, error_msg
137
139
  end
138
140
  end
139
141
 
@@ -144,4 +146,10 @@ class Listener
144
146
  def configuration
145
147
  @configuration ||= load_configuration_from_file("gorgon_listener.json")
146
148
  end
149
+
150
+ def send_crash_message output, error
151
+ reply = {:type => :crash, :hostname => Socket.gethostname,
152
+ :stdout => output, :stderr => error}
153
+ @reply_exchange.publish(Yajl::Encoder.encode(reply))
154
+ end
147
155
  end
@@ -6,6 +6,8 @@ require 'gorgon/originator_logger'
6
6
  require 'gorgon/failures_printer'
7
7
 
8
8
  require 'awesome_print'
9
+ require 'etc'
10
+ require 'socket'
9
11
 
10
12
  class Originator
11
13
  include Configuration
@@ -81,7 +83,15 @@ class Originator
81
83
  @job_state.file_finished payload
82
84
  elsif payload[:action] == "start"
83
85
  @job_state.file_started payload
86
+ elsif payload[:type] == "crash"
87
+ @job_state.gorgon_crash_message payload
88
+ elsif payload[:type] == "exception"
89
+ # TODO
90
+ ap payload
91
+ else
92
+ ap payload
84
93
  end
94
+
85
95
  @logger.log_message payload
86
96
  # Uncomment this to see each message received by originator
87
97
  # ap payload
@@ -111,7 +121,11 @@ class Originator
111
121
  end
112
122
 
113
123
  def job_definition
114
- JobDefinition.new(@configuration[:job])
124
+ job_config = configuration[:job]
125
+ if !job_config.has_key?(:source_tree_path)
126
+ job_config[:source_tree_path] = "#{Etc.getlogin}@#{Socket.gethostname}:#{Dir.pwd}"
127
+ end
128
+ JobDefinition.new(configuration[:job])
115
129
  end
116
130
 
117
131
  def configuration
@@ -12,6 +12,9 @@ class OriginatorLogger
12
12
  log("Started running '#{payload[:filename]}' at '#{payload[:hostname]}'")
13
13
  elsif payload[:action] == "finish"
14
14
  print_finish(payload)
15
+ elsif payload[:type] == "crash" || payload[:type] == "exception"
16
+ # TODO: improve logging of these messages
17
+ log(payload)
15
18
  else # to be removed
16
19
  ap payload
17
20
  end
@@ -20,6 +20,8 @@ class ProgressBarView
20
20
  end
21
21
 
22
22
  def update payload={}
23
+ output_gorgon_crash_message payload if gorgon_crashed? payload
24
+
23
25
  create_progress_bar_if_started_job_running
24
26
 
25
27
  return if @progress_bar.nil? || @finished
@@ -50,6 +52,19 @@ class ProgressBarView
50
52
  end
51
53
 
52
54
  private
55
+ def gorgon_crashed? payload
56
+ payload[:type] == "crash" && payload[:action] != "finish"
57
+ end
58
+
59
+ def output_gorgon_crash_message payload
60
+ $stderr.puts "\nA #{'crash'.red} occured at '#{payload[:hostname].colorize HOST_COLOR}':"
61
+ $stderr.puts payload[:stdout].yellow unless payload[:stdout].to_s.strip.length == 0
62
+ $stderr.puts payload[:stderr].yellow unless payload[:stderr].to_s.strip.length == 0
63
+ if @progress_bar.nil?
64
+ print LOADING_MSG # if still loading, print msg so user won't think the whole job crashed
65
+ end
66
+ end
67
+
53
68
  def format colors
54
69
  # TODO: decide what bar to use
55
70
  # bar = "%b>%i".colorize(colors[:bar])
@@ -1,9 +1,11 @@
1
+ require 'open4'
2
+
1
3
  class SourceTreeSyncer
2
4
  attr_accessor :exclude
3
- attr_reader :sys_command
5
+ attr_reader :sys_command, :output, :errors
4
6
 
5
7
  SYS_COMMAND = 'rsync'
6
- OPTS = '-az'
8
+ OPTS = "-azr --timeout=5 --rsh='ssh -o NumberOfPasswordPrompts=0 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'"
7
9
  EXCLUDE_OPT = "--exclude"
8
10
 
9
11
  def initialize source_tree_path
@@ -16,10 +18,19 @@ class SourceTreeSyncer
16
18
  Dir.chdir(@tempdir)
17
19
 
18
20
  exclude_opt = build_exclude_opt
19
- @sys_command = "#{SYS_COMMAND} #{OPTS} #{exclude_opt} -r --rsh=ssh #{@source_tree_path}/ ."
20
- system(@sys_command)
21
+ @sys_command = "#{SYS_COMMAND} #{OPTS} #{exclude_opt} #{@source_tree_path}/ ."
22
+
23
+ pid, stdin, stdout, stderr = Open4::popen4 @sys_command
24
+ stdin.close
25
+
26
+ @output, @errors = [stdout, stderr].map { |p| begin p.read ensure p.close end }
27
+
28
+ ignore, status = Process.waitpid2 pid
29
+ @exitstatus = status.exitstatus
30
+ end
21
31
 
22
- return $?.exitstatus == 0
32
+ def success?
33
+ @exitstatus == 0
23
34
  end
24
35
 
25
36
  def remove_temp_dir
@@ -1,3 +1,3 @@
1
1
  module Gorgon
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/gorgon/worker.rb CHANGED
@@ -11,14 +11,20 @@ module WorkUnit
11
11
  def self.run_file filename
12
12
  require "gorgon/testunit_runner"
13
13
  start_t = Time.now
14
- results = TestRunner.run_file(filename)
15
- length = Time.now - start_t
16
14
 
17
- if results.empty?
18
- {:failures => [], :type => :pass, :time => length}
19
- else
20
- {:failures => results, :type => :fail, :time => length}
15
+ begin
16
+ failures = TestRunner.run_file(filename)
17
+ length = Time.now - start_t
18
+
19
+ if failures.empty?
20
+ results = {:failures => [], :type => :pass, :time => length}
21
+ else
22
+ results = {:failures => failures, :type => :fail, :time => length}
23
+ end
24
+ rescue Exception => e
25
+ results = {:failures => ["Exception: #{e.message}\n#{e.backtrace.join("\n")}"], :type => :crash, :time => (Time.now - start_t)}
21
26
  end
27
+ return results
22
28
  end
23
29
  end
24
30
 
@@ -64,9 +70,10 @@ class Worker
64
70
  end
65
71
 
66
72
  def work
67
- log "Running before_start callback"
73
+ log "Running before_start callback..."
68
74
  @callback_handler.before_start
69
75
 
76
+ log "Running files ..."
70
77
  @amqp.start_worker @file_queue_name, @reply_exchange_name do |queue, exchange|
71
78
  while filename = queue.pop
72
79
  exchange.publish make_start_message(filename)
@@ -74,8 +81,8 @@ class Worker
74
81
  exchange.publish make_finish_message(filename, test_results)
75
82
  end
76
83
  end
77
- ensure # this 'ensure' that we run after_complete even after an 'INT' signal
78
- clean_up
84
+ ensure # this 'ensure' that we run after_complete even after an 'INT' signal
85
+ clean_up
79
86
  end
80
87
 
81
88
  private
@@ -90,7 +90,7 @@ class WorkerManager
90
90
  :hostname => Socket.gethostname,
91
91
  :stdout => stdout.read,
92
92
  :stderr => error_msg}
93
- @reply_ecxhange.publish(Yajl::Encoder.encode(reply))
93
+ @reply_exchange.publish(Yajl::Encoder.encode(reply))
94
94
  # TODO: find a way to stop the whole system when a worker crashes or do something more clever
95
95
  rescue Exception => e
96
96
  log_error "Exception raised when trying to report crash to originator:"
@@ -13,6 +13,7 @@ describe JobState do
13
13
  it { should respond_to :finished_files_count }
14
14
  it { should respond_to(:file_started).with(1).argument }
15
15
  it { should respond_to(:file_finished).with(1).argument }
16
+ it { should respond_to(:gorgon_crash_message).with(1).argument }
16
17
  it { should respond_to :cancel }
17
18
  it { should respond_to :each_failed_test }
18
19
  it { should respond_to :each_running_file }
@@ -141,6 +142,22 @@ describe JobState do
141
142
  end
142
143
  end
143
144
 
145
+ describe "#gorgon_crash_message" do
146
+ let(:crash_msg) {{:type => "crash", :hostname => "host",
147
+ :stdout => "some output", :stderr => "some errors"}}
148
+
149
+ it "adds crashed host to JobState#crashed_hosted" do
150
+ @job_state.gorgon_crash_message(crash_msg)
151
+ @job_state.crashed_hosts.should == ["host"]
152
+ end
153
+
154
+ it "notify observers" do
155
+ @job_state.should_receive :notify_observers
156
+ @job_state.should_receive :changed
157
+ @job_state.gorgon_crash_message crash_msg
158
+ end
159
+ end
160
+
144
161
  describe "#is_job_complete?" do
145
162
  it "returns false if remaining_files_count != 0" do
146
163
  @job_state.is_job_complete?.should be_false
@@ -3,15 +3,15 @@ require 'gorgon/listener'
3
3
  describe Listener do
4
4
  let(:connection_information) { double }
5
5
  let(:queue) { stub("Bunny Queue", :bind => nil) }
6
- let(:exchange) { stub("Bunny Exchange") }
6
+ let(:exchange) { stub("Bunny Exchange", :publish => nil) }
7
7
  let(:bunny) { stub("Bunny", :start => nil, :queue => queue, :exchange => exchange) }
8
+ let(:logger) { stub("Logger", :info => true, :datetime_format= => "")}
8
9
 
9
10
  before do
11
+ Logger.stub(:new).and_return(logger)
10
12
  Bunny.stub(:new).and_return(bunny)
11
13
  Listener.any_instance.stub(:configuration => {})
12
14
  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
15
  end
16
16
 
17
17
  describe "initialization" do
@@ -43,7 +43,7 @@ describe Listener do
43
43
  end
44
44
 
45
45
  it "should log to 'log_file'" do
46
- @stub_logger.should_receive(:info).with("Listener initialized")
46
+ logger.should_receive(:info).with("Listener initialized")
47
47
 
48
48
  Listener.new
49
49
  end
@@ -63,7 +63,7 @@ describe Listener do
63
63
  context "without specifying a log file path" do
64
64
  it "should not log" do
65
65
  Logger.should_not_receive(:new)
66
- @stub_logger.should_not_receive(:info)
66
+ logger.should_not_receive(:info)
67
67
 
68
68
  Listener.new
69
69
  end
@@ -141,26 +141,62 @@ describe Listener do
141
141
  :sync_exclude => ["log"], :callbacks => {:a_callback => "path/to/callback"}
142
142
  }}
143
143
 
144
- let(:syncer) { stub("SourceTreeSyncer", :sync => nil, :exclude= => nil,
144
+ let(:syncer) { stub("SourceTreeSyncer", :sync => nil, :exclude= => nil, :success? => true,
145
+ :output => "some output", :errors => "some errors",
145
146
  :remove_temp_dir => nil, :sys_command => "rsync ...")}
146
-
147
- let(:io) { stub("IO object", :write => nil, :close => nil)}
148
147
  let(:process_status) { stub("Process Status", :exitstatus => 0)}
149
148
  let(:callback_handler) { stub("Callback Handler", :after_sync => nil) }
149
+ let(:stdin) { stub("IO object", :write => nil, :close => nil)}
150
+ let(:stdout) { stub("IO object", :read => nil, :close => nil)}
151
+ let(:stderr) { stub("IO object", :read => nil, :close => nil)}
150
152
 
151
153
  before do
154
+ stub_classes
152
155
  @listener = Listener.new
153
156
  @json_payload = Yajl::Encoder.encode(payload)
154
- stub_classes
155
157
  end
156
158
 
157
159
  it "copy source tree" do
158
160
  SourceTreeSyncer.should_receive(:new).once.with("path/to/source").and_return syncer
159
161
  syncer.should_receive(:exclude=).with(["log"])
160
162
  syncer.should_receive(:sync)
163
+ syncer.should_receive(:success?).and_return(true)
161
164
  @listener.run_job(@json_payload)
162
165
  end
163
166
 
167
+ context "syncer#sync fails" do
168
+ before do
169
+ syncer.stub!(:success?).and_return false
170
+ syncer.stub!(:output).and_return "some output"
171
+ syncer.stub!(:errors).and_return "some errors"
172
+ end
173
+
174
+ it "aborts current job" do
175
+ callback_handler.should_not_receive(:after_sync)
176
+ @listener.run_job(@json_payload)
177
+ end
178
+
179
+ it "sends message to originator with output and errors from syncer" do
180
+ reply = {:type => :crash, :hostname => "hostname", :stdout => "some output", :stderr => "some errors"}
181
+ exchange.should_receive(:publish).with(Yajl::Encoder.encode(reply))
182
+ @listener.run_job(@json_payload)
183
+ end
184
+ end
185
+
186
+ context "Worker Manager crahes" do
187
+ before do
188
+ process_status.should_receive(:exitstatus).and_return 1
189
+ end
190
+
191
+ it "sends message to originator with output and errors from worker manager" do
192
+ stdout.should_receive(:read).and_return "some output"
193
+ stderr.should_receive(:read).and_return "some errors"
194
+ reply = {:type => :crash, :hostname => "hostname", :stdout => "some output", :stderr => "some errors"}
195
+ exchange.should_receive(:publish).with(Yajl::Encoder.encode(reply))
196
+ @listener.run_job(@json_payload)
197
+ end
198
+ end
199
+
164
200
  it "remove temp source directory when complete" do
165
201
  syncer.should_receive(:remove_temp_dir)
166
202
  @listener.run_job(@json_payload)
@@ -187,8 +223,9 @@ describe Listener do
187
223
  def stub_classes
188
224
  SourceTreeSyncer.stub!(:new).and_return syncer
189
225
  CallbackHandler.stub!(:new).and_return callback_handler
190
- Open4.stub!(:popen4).and_return([1, io])
226
+ Open4.stub!(:popen4).and_return([1, stdin, stdout, stderr])
191
227
  Process.stub!(:waitpid2).and_return([0, process_status])
228
+ Socket.stub!(:gethostname).and_return("hostname")
192
229
  end
193
230
  end
194
231
  end
@@ -5,7 +5,7 @@ describe Originator do
5
5
  :publish_job => nil, :receive_payloads => nil, :cancel_job => nil,
6
6
  :disconnect => nil)}
7
7
 
8
- let(:configuration){ {:files => ["some/file"]}}
8
+ let(:configuration){ {:job => {}, :files => ["some/file"]}}
9
9
  let(:job_state){ stub("JobState", :is_job_complete? => false, :file_finished => nil,
10
10
  :add_observer => nil)}
11
11
  let(:progress_bar_view){ stub("Progress Bar View", :show => nil)}
@@ -98,6 +98,33 @@ describe Originator do
98
98
  job_state.should_receive(:file_finished).with(payload)
99
99
  @originator.handle_reply(finish_payload)
100
100
  end
101
+
102
+ let(:gorgon_crash_message) {{:type => "crash", :hostname => "host",
103
+ :stdout => "some output", :stderr => "some errors"}}
104
+
105
+ it "calls JobState#gorgon_crash_message if payload[:type] is 'crash'" do
106
+ job_state.should_receive(:gorgon_crash_message).with(gorgon_crash_message)
107
+ @originator.handle_reply(Yajl::Encoder.encode(gorgon_crash_message))
108
+ end
109
+ end
110
+
111
+ describe "#job_definition" do
112
+ it "returns a JobDefinition object" do
113
+ @originator.stub!(:configuration).and_return configuration
114
+ job_definition = JobDefinition.new
115
+ JobDefinition.should_receive(:new).and_return job_definition
116
+ @originator.job_definition.should equal job_definition
117
+ end
118
+
119
+ it "builds source_tree_path if it was not specified in the configuration" do
120
+ @originator.stub!(:configuration).and_return({:job => {}})
121
+ @originator.job_definition.source_tree_path.should == "#{Etc.getlogin}@#{Socket.gethostname}:#{Dir.pwd}"
122
+ end
123
+
124
+ it "returns source_tree_path specified in configuration if it is present" do
125
+ @originator.stub!(:configuration).and_return({:job => {:source_tree_path => "login@host:path/to/dir"}})
126
+ @originator.job_definition.source_tree_path.should == "login@host:path/to/dir"
127
+ end
101
128
  end
102
129
 
103
130
  private
@@ -16,7 +16,7 @@ describe ProgressBarView do
16
16
  @progress_bar_view = ProgressBarView.new job_state
17
17
  end
18
18
 
19
- it "prints a message in console saying that is loading workers" do
19
+ it "prints in console gorgon's version and that is loading workers" do
20
20
  $stdout.should_receive(:write).with(/loading .*workers/i)
21
21
  ProgressBar.should_not_receive(:create)
22
22
  @progress_bar_view.show
@@ -94,5 +94,17 @@ describe ProgressBarView do
94
94
  @progress_bar_view.update
95
95
  end
96
96
  end
97
+
98
+ context "when payload is a crash message" do
99
+ let(:crash_message) {{:type => "crash", :hostname => "host",
100
+ :stdout => "some output", :stderr => "some errors"}}
101
+ it "prints info about crash in standard error" do
102
+ $stderr.stub!(:write)
103
+ $stderr.should_receive(:write).with(/crash.*host/i)
104
+ $stderr.should_receive(:write).with(/some output/i)
105
+ $stderr.should_receive(:write).with(/some errors/i)
106
+ @progress_bar_view.update crash_message
107
+ end
108
+ end
97
109
  end
98
110
  end
@@ -5,39 +5,93 @@ describe SourceTreeSyncer.new("") do
5
5
  it { should respond_to :sync }
6
6
  it { should respond_to :sys_command }
7
7
  it { should respond_to :remove_temp_dir }
8
+ it { should respond_to :success? }
9
+ it { should respond_to :output }
10
+ it { should respond_to :errors }
8
11
 
9
- describe "#sync" do
10
- before do
11
- @syncer = SourceTreeSyncer.new "path/to/source"
12
- stub_utilities_methods
13
- end
12
+ let(:stdin) { stub("IO object", :close => nil)}
13
+ let(:stdout) { stub("IO object", :read => nil, :close => nil)}
14
+ let(:stderr) { stub("IO object", :read => nil, :close => nil)}
15
+ let(:status) { stub("Process Status", :exitstatus => 0)}
14
16
 
17
+ before do
18
+ @syncer = SourceTreeSyncer.new "path/to/source"
19
+ stub_utilities_methods
20
+ end
21
+
22
+ describe "#sync" do
15
23
  it "makes tempdir and changes current dir to temdir" do
16
24
  Dir.should_receive(:mktmpdir).and_return("tmp/dir")
17
25
  Dir.should_receive(:chdir).with("tmp/dir")
18
26
  @syncer.sync
19
27
  end
20
28
 
21
- it "runs rsync system command with appropriate options" do
22
- cmd = /rsync.*-az.*-r --rsh=ssh path\/to\/source\/\ \./
23
- @syncer.should_receive(:system).with(cmd)
29
+ context "options" do
30
+ it "runs rsync system command with appropriate options" do
31
+ cmd = /rsync.*-azr .*path\/to\/source\/\ \./
32
+ Open4.should_receive(:popen4).with(cmd)
33
+ @syncer.sync
34
+ end
35
+
36
+ it "exclude files when they are specified" do
37
+ @syncer.exclude = ["log", ".git"]
38
+ Open4.should_receive(:popen4).with(/--exclude log --exclude .git/)
39
+ @syncer.sync
40
+ end
41
+
42
+ it "use NumberOfPasswordPrompts 0 as ssh option to avoid password prompts that will hang the listener" do
43
+ opt = /--rsh='ssh .*-o NumberOfPasswordPrompts=0.*'/
44
+ Open4.should_receive(:popen4).with(opt)
45
+ @syncer.sync
46
+ end
47
+
48
+ it "set UserKnownHostsFile to /dev/null so we avoid hosts id changes and eavesdropping warnings in futures connections" do
49
+ opt = /ssh .*-o UserKnownHostsFile=\/dev\/null/
50
+ Open4.should_receive(:popen4).with(opt)
51
+ @syncer.sync
52
+ end
53
+
54
+ it "set StrictHostKeyChecking to 'no' to avoid confirmation prompt of connection to unkown host" do
55
+ opt = /ssh .*-o StrictHostKeyChecking=no/
56
+ Open4.should_receive(:popen4).with(opt)
57
+ @syncer.sync
58
+ end
59
+
60
+ it "uses io timeout to avoid listener hanging forever in case rsync asks for any input" do
61
+ opt = /--timeout=5/
62
+ Open4.should_receive(:popen4).with(opt)
63
+ @syncer.sync
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#success?" do
69
+ it "returns true if sync execution was successful" do
70
+ status.should_receive(:exitstatus).and_return(0)
24
71
  @syncer.sync
72
+ @syncer.success?.should be_true
25
73
  end
26
74
 
27
- it "exclude files when they are specified" do
28
- @syncer.exclude = ["log", ".git"]
29
- @syncer.should_receive(:system).with(/--exclude log --exclude .git/)
75
+ it "returns false if sync execution failed" do
76
+ status.should_receive(:exitstatus).and_return(1)
30
77
  @syncer.sync
78
+ @syncer.success?.should be_false
31
79
  end
80
+ end
32
81
 
33
- it "returns true if sys command execution was successful" do
34
- $?.stub!(:exitstatus).and_return 0
35
- @syncer.sync.should be_true
82
+ describe "#output" do
83
+ it "returns standard output of rsync" do
84
+ stdout.should_receive(:read).and_return("some output")
85
+ @syncer.sync
86
+ @syncer.output.should == "some output"
36
87
  end
88
+ end
37
89
 
38
- it "returns false if sys command execution failed" do
39
- $?.stub!(:exitstatus).and_return 1
40
- @syncer.sync.should be_false
90
+ describe "#errors" do
91
+ it "returns standard error output of rsync" do
92
+ stderr.should_receive(:read).and_return("some errors")
93
+ @syncer.sync
94
+ @syncer.errors.should == "some errors"
41
95
  end
42
96
  end
43
97
 
@@ -59,6 +113,8 @@ describe SourceTreeSyncer.new("") do
59
113
  def stub_utilities_methods
60
114
  Dir.stub!(:mktmpdir).and_return("tmp/dir")
61
115
  Dir.stub!(:chdir)
116
+ Open4.stub!(:popen4).and_return([1, stdin, stdout, stderr])
117
+ Process.stub!(:waitpid2).and_return([nil, status])
62
118
  FileUtils.stub!(:remove_entry_secure)
63
119
  @syncer.stub!(:system)
64
120
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gorgon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2012-09-18 00:00:00.000000000 Z
16
+ date: 2012-09-24 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: rspec