gorgon 0.0.2 → 0.1.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.
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