gorgon 0.2.0 → 0.3.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/.gitignore +3 -1
- data/Gemfile.lock +9 -9
- data/README.md +33 -9
- data/bin/gorgon +9 -0
- data/gorgon.json.sample +29 -0
- data/gorgon_listener.json.sample +8 -0
- data/lib/gorgon/colors.rb +1 -0
- data/lib/gorgon/crash_reporter.rb +19 -0
- data/lib/gorgon/gem_command_handler.rb +46 -0
- data/lib/gorgon/gem_service.rb +77 -0
- data/lib/gorgon/job_state.rb +4 -4
- data/lib/gorgon/listener.rb +26 -21
- data/lib/gorgon/originator_protocol.rb +9 -8
- data/lib/gorgon/ping_service.rb +2 -2
- data/lib/gorgon/pipe_forker.rb +22 -0
- data/lib/gorgon/source_tree_syncer.rb +2 -1
- data/lib/gorgon/version.rb +1 -1
- data/lib/gorgon/worker.rb +64 -30
- data/lib/gorgon/worker_manager.rb +42 -12
- data/spec/crash_reporter_spec.rb +46 -0
- data/spec/gem_command_handler_spec.rb +84 -0
- data/spec/gem_service_spec.rb +74 -0
- data/spec/job_state_spec.rb +0 -7
- data/spec/listener_spec.rb +37 -19
- data/spec/originator_protocol_spec.rb +12 -9
- data/spec/ping_service_spec.rb +3 -3
- data/spec/pipe_forker_spec.rb +53 -0
- data/spec/worker_manager_spec.rb +34 -5
- data/spec/worker_spec.rb +63 -1
- metadata +12 -4
- data/lib/gorgon/pipe_manager.rb +0 -55
- data/lib/gorgon/worker_watcher.rb +0 -22
@@ -16,7 +16,7 @@ class OriginatorProtocol
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def publish_files files
|
19
|
-
@file_queue = @channel.queue(UUIDTools::UUID.timestamp_create.to_s)
|
19
|
+
@file_queue = @channel.queue("file_queue_" + UUIDTools::UUID.timestamp_create.to_s)
|
20
20
|
|
21
21
|
files.each do |file|
|
22
22
|
@channel.default_exchange.publish(file, :routing_key => @file_queue.name)
|
@@ -30,9 +30,9 @@ class OriginatorProtocol
|
|
30
30
|
@channel.fanout("gorgon.jobs").publish(job_definition.to_json)
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
34
|
-
# TODO: we probably want to use a different exchange for
|
35
|
-
message = {:type =>
|
33
|
+
def send_message_to_listeners type, body={}
|
34
|
+
# TODO: we probably want to use a different exchange for this type of messages
|
35
|
+
message = {:type => type, :reply_exchange_name => @reply_exchange.name, :body => body}
|
36
36
|
@channel.fanout("gorgon.jobs").publish(Yajl::Encoder.encode(message))
|
37
37
|
end
|
38
38
|
|
@@ -49,21 +49,22 @@ class OriginatorProtocol
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def disconnect
|
52
|
-
|
52
|
+
cleanup_queues_and_exchange
|
53
53
|
@connection.disconnect
|
54
54
|
end
|
55
55
|
|
56
56
|
private
|
57
57
|
|
58
58
|
def open_queues
|
59
|
-
@reply_queue = @channel.queue(UUIDTools::UUID.timestamp_create.to_s)
|
60
|
-
@reply_exchange = @channel.direct(UUIDTools::UUID.timestamp_create.to_s)
|
59
|
+
@reply_queue = @channel.queue("reply_queue_" + UUIDTools::UUID.timestamp_create.to_s)
|
60
|
+
@reply_exchange = @channel.direct("reply_exchange_" + UUIDTools::UUID.timestamp_create.to_s)
|
61
61
|
@reply_queue.bind(@reply_exchange)
|
62
62
|
end
|
63
63
|
|
64
|
-
def
|
64
|
+
def cleanup_queues_and_exchange
|
65
65
|
@reply_queue.delete if @reply_queue
|
66
66
|
@file_queue.delete if @file_queue
|
67
|
+
@reply_exchange.delete if @reply_exchange
|
67
68
|
end
|
68
69
|
|
69
70
|
def cancel_message
|
data/lib/gorgon/ping_service.rb
CHANGED
@@ -26,7 +26,7 @@ class PingService
|
|
26
26
|
@protocol.connect @configuration[:connection], :on_closed => proc {EM.stop}
|
27
27
|
|
28
28
|
@logger.log "Pinging Listeners..."
|
29
|
-
@protocol.
|
29
|
+
@protocol.send_message_to_listeners :ping
|
30
30
|
|
31
31
|
EM.add_timer(TIMEOUT) { disconnect }
|
32
32
|
|
@@ -53,7 +53,7 @@ class PingService
|
|
53
53
|
|
54
54
|
@listeners << payload
|
55
55
|
hostname = payload[:hostname].colorize(Colors::HOST)
|
56
|
-
puts "#{hostname} is running Listener version #{payload[:version]}"
|
56
|
+
puts "#{hostname} is running Listener version #{payload[:version]} and uses #{payload[:worker_slots]} workers"
|
57
57
|
end
|
58
58
|
|
59
59
|
def print_summary
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module PipeForker
|
2
|
+
def pipe_fork
|
3
|
+
stdin = Pipe.new(*IO.pipe)
|
4
|
+
pid = fork do
|
5
|
+
stdin.write.close
|
6
|
+
STDIN.reopen(stdin.read)
|
7
|
+
stdin.read.close
|
8
|
+
|
9
|
+
yield
|
10
|
+
|
11
|
+
exit
|
12
|
+
end
|
13
|
+
|
14
|
+
stdin.read.close
|
15
|
+
|
16
|
+
return pid, stdin.write
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
Pipe = Struct.new(:read, :write)
|
22
|
+
end
|
@@ -25,9 +25,10 @@ class SourceTreeSyncer
|
|
25
25
|
pid, stdin, stdout, stderr = Open4::popen4 @sys_command
|
26
26
|
stdin.close
|
27
27
|
|
28
|
+
ignore, status = Process.waitpid2 pid
|
29
|
+
|
28
30
|
@output, @errors = [stdout, stderr].map { |p| begin p.read ensure p.close end }
|
29
31
|
|
30
|
-
ignore, status = Process.waitpid2 pid
|
31
32
|
@exitstatus = status.exitstatus
|
32
33
|
end
|
33
34
|
|
data/lib/gorgon/version.rb
CHANGED
data/lib/gorgon/worker.rb
CHANGED
@@ -2,6 +2,8 @@ require "gorgon/configuration"
|
|
2
2
|
require "gorgon/amqp_service"
|
3
3
|
require 'gorgon/callback_handler'
|
4
4
|
require "gorgon/g_logger"
|
5
|
+
require 'gorgon/job_definition'
|
6
|
+
require "gorgon/testunit_runner"
|
5
7
|
|
6
8
|
require "uuidtools"
|
7
9
|
require "awesome_print"
|
@@ -9,7 +11,6 @@ require "socket"
|
|
9
11
|
|
10
12
|
module WorkUnit
|
11
13
|
def self.run_file filename
|
12
|
-
require "gorgon/testunit_runner"
|
13
14
|
start_t = Time.now
|
14
15
|
|
15
16
|
begin
|
@@ -31,29 +32,44 @@ end
|
|
31
32
|
class Worker
|
32
33
|
include GLogger
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
class << self
|
36
|
+
def build(worker_id, config)
|
37
|
+
redirect_output_to_files worker_id
|
37
38
|
|
38
|
-
|
39
|
-
|
39
|
+
payload = Yajl::Parser.new(:symbolize_keys => true).parse($stdin.read)
|
40
|
+
job_definition = JobDefinition.new(payload)
|
40
41
|
|
41
|
-
|
42
|
+
connection_config = config[:connection]
|
43
|
+
amqp = AmqpService.new connection_config
|
42
44
|
|
43
|
-
|
44
|
-
ENV["GORGON_WORKER_ID"] = worker_id
|
45
|
+
callback_handler = CallbackHandler.new(job_definition.callbacks)
|
45
46
|
|
46
|
-
|
47
|
-
:amqp => amqp,
|
48
|
-
:file_queue_name => job_definition.file_queue_name,
|
49
|
-
:reply_exchange_name => job_definition.reply_exchange_name,
|
50
|
-
:worker_id => worker_id,
|
51
|
-
:test_runner => WorkUnit,
|
52
|
-
:callback_handler => callback_handler,
|
53
|
-
:log_file => config[:log_file]
|
54
|
-
}
|
47
|
+
ENV["GORGON_WORKER_ID"] = worker_id.to_s
|
55
48
|
|
56
|
-
|
49
|
+
params = {
|
50
|
+
:amqp => amqp,
|
51
|
+
:file_queue_name => job_definition.file_queue_name,
|
52
|
+
:reply_exchange_name => job_definition.reply_exchange_name,
|
53
|
+
:worker_id => worker_id,
|
54
|
+
:test_runner => WorkUnit,
|
55
|
+
:callback_handler => callback_handler,
|
56
|
+
:log_file => config[:log_file]
|
57
|
+
}
|
58
|
+
|
59
|
+
new(params)
|
60
|
+
end
|
61
|
+
|
62
|
+
def output_file id, stream
|
63
|
+
"/tmp/gorgon-worker-#{id}.#{stream.to_s}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def redirect_output_to_files worker_id
|
67
|
+
STDOUT.reopen(File.open(output_file(worker_id, :out), 'w'))
|
68
|
+
STDOUT.sync = true
|
69
|
+
|
70
|
+
STDERR.reopen(File.open(output_file(worker_id, :err), 'w'))
|
71
|
+
STDERR.sync = true
|
72
|
+
end
|
57
73
|
end
|
58
74
|
|
59
75
|
def initialize(params)
|
@@ -68,27 +84,35 @@ class Worker
|
|
68
84
|
end
|
69
85
|
|
70
86
|
def work
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
87
|
+
begin
|
88
|
+
log "Running before_start callback..."
|
89
|
+
register_trap_ints # do it before calling before_start callback!
|
90
|
+
@callback_handler.before_start
|
91
|
+
@cleaned = false
|
92
|
+
|
93
|
+
log "Running files ..."
|
94
|
+
@amqp.start_worker @file_queue_name, @reply_exchange_name do |queue, exchange|
|
95
|
+
while filename = queue.pop
|
96
|
+
exchange.publish make_start_message(filename)
|
97
|
+
log "Running '#{filename}'"
|
98
|
+
test_results = run_file(filename)
|
99
|
+
exchange.publish make_finish_message(filename, test_results)
|
100
|
+
end
|
81
101
|
end
|
102
|
+
rescue Exception => e
|
103
|
+
clean_up
|
104
|
+
raise e # So worker manager can catch it
|
82
105
|
end
|
83
|
-
ensure # this 'ensure' that we run after_complete even after an 'INT' signal
|
84
106
|
clean_up
|
85
107
|
end
|
86
108
|
|
87
109
|
private
|
88
110
|
|
89
111
|
def clean_up
|
112
|
+
return if @cleaned
|
90
113
|
log "Running after_complete callback"
|
91
114
|
@callback_handler.after_complete
|
115
|
+
@cleaned = true
|
92
116
|
end
|
93
117
|
|
94
118
|
def run_file(filename)
|
@@ -102,4 +126,14 @@ class Worker
|
|
102
126
|
def make_finish_message(filename, results)
|
103
127
|
{:action => :finish, :hostname => Socket.gethostname, :worker_id => @worker_id, :filename => filename}.merge(results)
|
104
128
|
end
|
129
|
+
|
130
|
+
def register_trap_ints
|
131
|
+
Signal.trap("INT") { ctrl_c }
|
132
|
+
Signal.trap("TERM") { ctrl_c }
|
133
|
+
end
|
134
|
+
|
135
|
+
def ctrl_c
|
136
|
+
clean_up
|
137
|
+
exit
|
138
|
+
end
|
105
139
|
end
|
@@ -1,22 +1,37 @@
|
|
1
1
|
require "gorgon/worker"
|
2
2
|
require "gorgon/g_logger"
|
3
3
|
require 'gorgon/callback_handler'
|
4
|
-
require 'gorgon/
|
4
|
+
require 'gorgon/pipe_forker'
|
5
5
|
require 'gorgon/job_definition'
|
6
|
+
require "gorgon/crash_reporter"
|
6
7
|
|
7
8
|
require 'eventmachine'
|
8
9
|
|
9
10
|
class WorkerManager
|
10
|
-
include
|
11
|
+
include PipeForker
|
11
12
|
include GLogger
|
13
|
+
include CrashReporter
|
14
|
+
|
15
|
+
STDOUT_FILE='/tmp/gorgon-worker-mgr.out'
|
16
|
+
STDERR_FILE='/tmp/gorgon-worker-mgr.err'
|
12
17
|
|
13
18
|
def self.build listener_config_file
|
14
19
|
@listener_config_file = listener_config_file
|
15
20
|
config = Configuration.load_configuration_from_file(listener_config_file)
|
16
21
|
|
22
|
+
redirect_output_to_files
|
23
|
+
|
17
24
|
new config
|
18
25
|
end
|
19
26
|
|
27
|
+
def self.redirect_output_to_files
|
28
|
+
STDOUT.reopen(File.open(STDOUT_FILE, 'w'))
|
29
|
+
STDOUT.sync = true
|
30
|
+
|
31
|
+
STDERR.reopen(File.open(STDERR_FILE, 'w'))
|
32
|
+
STDERR.sync = true
|
33
|
+
end
|
34
|
+
|
20
35
|
def initialize config
|
21
36
|
initialize_logger config[:log_file]
|
22
37
|
log "Worker Manager #{Gorgon::VERSION} initializing"
|
@@ -68,7 +83,13 @@ class WorkerManager
|
|
68
83
|
@available_worker_slots -= 1
|
69
84
|
ENV["GORGON_CONFIG_PATH"] = @listener_config_filename
|
70
85
|
|
71
|
-
|
86
|
+
worker_id = get_worker_id
|
87
|
+
log "Forking Worker #{worker_id}"
|
88
|
+
pid, stdin = pipe_fork do
|
89
|
+
worker = Worker.build(worker_id, @config)
|
90
|
+
worker.work
|
91
|
+
end
|
92
|
+
|
72
93
|
@worker_pids << pid
|
73
94
|
stdin.write(@job_definition.to_json)
|
74
95
|
stdin.close
|
@@ -82,17 +103,18 @@ class WorkerManager
|
|
82
103
|
|
83
104
|
worker_complete = proc do |status|
|
84
105
|
if status.exitstatus != 0
|
85
|
-
|
86
|
-
|
87
|
-
log_error "ERROR MSG: #{error_msg}"
|
106
|
+
exitstatus = status.exitstatus
|
107
|
+
log_error "Worker #{pid} crashed with exit status #{exitstatus}!"
|
88
108
|
|
89
109
|
# originator may have cancel job and exit, so only try to send message
|
90
110
|
begin
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
111
|
+
out_file = Worker.output_file(worker_id, :out)
|
112
|
+
err_file = Worker.output_file(worker_id, :err)
|
113
|
+
|
114
|
+
msg = report_crash @reply_exchange, :out_file => out_file,
|
115
|
+
:err_file => err_file, :footer_text => footer_text(err_file, out_file)
|
116
|
+
log_error "Process output:\n#{msg}"
|
117
|
+
|
96
118
|
# TODO: find a way to stop the whole system when a worker crashes or do something more clever
|
97
119
|
rescue Exception => e
|
98
120
|
log_error "Exception raised when trying to report crash to originator:"
|
@@ -105,6 +127,10 @@ class WorkerManager
|
|
105
127
|
EventMachine.defer(watcher, worker_complete)
|
106
128
|
end
|
107
129
|
|
130
|
+
def get_worker_id
|
131
|
+
@worker_id_count = @worker_id_count.nil? ? 1 : @worker_id_count + 1
|
132
|
+
end
|
133
|
+
|
108
134
|
def on_worker_complete
|
109
135
|
@available_worker_slots += 1
|
110
136
|
on_current_job_complete if current_job_complete?
|
@@ -125,7 +151,7 @@ class WorkerManager
|
|
125
151
|
@bunny.stop
|
126
152
|
end
|
127
153
|
|
128
|
-
CANCEL_TIMEOUT =
|
154
|
+
CANCEL_TIMEOUT = 20
|
129
155
|
def subscribe_to_originator_queue
|
130
156
|
|
131
157
|
originator_watcher = proc do
|
@@ -154,4 +180,8 @@ class WorkerManager
|
|
154
180
|
|
155
181
|
EventMachine.defer(originator_watcher, handle_message)
|
156
182
|
end
|
183
|
+
|
184
|
+
def footer_text err_file, out_file
|
185
|
+
"\n***** See #{err_file} and #{out_file} at '#{Socket.gethostname}' for more details *****\n"
|
186
|
+
end
|
157
187
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'gorgon/crash_reporter'
|
2
|
+
|
3
|
+
describe "CrashReporter" do
|
4
|
+
let(:exchange) { stub("Bunny Exchange", :publish => nil) }
|
5
|
+
let(:info) { {
|
6
|
+
:out_file => "stdout_file", :err_file => "stderr_file", :footer_text => "Text"
|
7
|
+
} }
|
8
|
+
|
9
|
+
let(:container_class) do
|
10
|
+
Class.new do
|
11
|
+
extend(CrashReporter)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#report_crash" do
|
16
|
+
it "tails output files to get last few lines" do
|
17
|
+
container_class.should_receive(:'`').once.
|
18
|
+
with(/tail.*stdout_file/).and_return ""
|
19
|
+
container_class.should_receive(:'`').once.
|
20
|
+
with(/tail.*stderr_file/).and_return ""
|
21
|
+
container_class.report_crash exchange, info
|
22
|
+
end
|
23
|
+
|
24
|
+
it "calls send_crash_message" do
|
25
|
+
container_class.stub!(:'`').and_return "stdout text", "stderr text "
|
26
|
+
container_class.should_receive(:send_crash_message).with(exchange, "stdout text", "stderr text Text")
|
27
|
+
container_class.report_crash exchange, info
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns last lines of output from stderr message and footer text" do
|
31
|
+
container_class.stub!(:'`').and_return "stdout text", "stderr text "
|
32
|
+
container_class.stub!(:send_crash_message)
|
33
|
+
result = container_class.report_crash exchange, info
|
34
|
+
result.should == "stdout text\nstderr text Text"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#send_crash_message" do
|
39
|
+
it "sends message with output and errors from syncer using reply_exchange " do
|
40
|
+
reply = {:type => :crash, :hostname => Socket.gethostname, :stdout => "some output",
|
41
|
+
:stderr => "some errors"}
|
42
|
+
exchange.should_receive(:publish).with(Yajl::Encoder.encode(reply))
|
43
|
+
container_class.send_crash_message exchange, "some output", "some errors"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'gorgon/gem_command_handler'
|
2
|
+
|
3
|
+
describe GemCommandHandler do
|
4
|
+
let(:exchange) { stub("Bunny Exchange", :publish => nil) }
|
5
|
+
let(:bunny) { stub("Bunny", :exchange => exchange, :stop => nil) }
|
6
|
+
|
7
|
+
let(:payload) {
|
8
|
+
{:type => :update, :reply_exchange_name => "name",
|
9
|
+
:body => {:gem_command => "cmd"}}
|
10
|
+
}
|
11
|
+
let(:stdin) { stub("IO object", :close => nil)}
|
12
|
+
let(:stdout) { stub("IO object", :read => "output", :close => nil)}
|
13
|
+
let(:stderr) { stub("IO object", :read => "errors", :close => nil)}
|
14
|
+
let(:status) { stub("Process Status", :exitstatus => 0)}
|
15
|
+
|
16
|
+
describe "#handle" do
|
17
|
+
before do
|
18
|
+
@handler = GemCommandHandler.new bunny
|
19
|
+
@running_response = {:type => :running_command, :hostname => Socket.gethostname}
|
20
|
+
stub_methods
|
21
|
+
end
|
22
|
+
|
23
|
+
it "publishes 'running_command' message" do
|
24
|
+
bunny.should_receive(:exchange).with("name", anything).and_return(exchange)
|
25
|
+
Yajl::Encoder.should_receive(:encode).with(@running_response).and_return :json_msg
|
26
|
+
exchange.should_receive(:publish).with(:json_msg)
|
27
|
+
@handler.handle payload, {}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "runs 'gem <command> gorgon' using popen4" do
|
31
|
+
Open4.should_receive(:popen4).with("gem cmd gorgon").and_return([1, stdin, stdout, stderr])
|
32
|
+
@handler.handle payload, {}
|
33
|
+
end
|
34
|
+
|
35
|
+
it "uses binary gem from configuration if this was specified" do
|
36
|
+
Open4.should_receive(:popen4).with("path/to/gem cmd gorgon").and_return([1, stdin, stdout, stderr])
|
37
|
+
@handler.handle payload, {:bin_gem_path => "path/to/gem"}
|
38
|
+
end
|
39
|
+
|
40
|
+
it "waits for the command to finish" do
|
41
|
+
Open4.should_receive(:popen4).ordered
|
42
|
+
Process.should_receive(:waitpid2).with(1).ordered.and_return([nil, status])
|
43
|
+
@handler.handle payload, {}
|
44
|
+
end
|
45
|
+
|
46
|
+
it "closes stding and reads stdout and stderr from process" do
|
47
|
+
stdin.should_receive(:close)
|
48
|
+
stdout.should_receive(:read)
|
49
|
+
stderr.should_receive(:read)
|
50
|
+
@handler.handle payload, {}
|
51
|
+
end
|
52
|
+
|
53
|
+
it "sends 'command_completed' message when exitstatus is 0" do
|
54
|
+
response = {:type => :command_completed, :hostname => Socket.gethostname,
|
55
|
+
:command => "gem cmd gorgon", :stdout => "output", :stderr => "errors" }
|
56
|
+
Yajl::Encoder.should_receive(:encode).once.ordered.with(@running_response)
|
57
|
+
Yajl::Encoder.should_receive(:encode).once.ordered.with(response).and_return :json_msg
|
58
|
+
exchange.should_receive(:publish).with(:json_msg)
|
59
|
+
@handler.handle payload, {}
|
60
|
+
end
|
61
|
+
|
62
|
+
it "stops bunny and exit if exitstatus is 0" do
|
63
|
+
bunny.should_receive(:stop).once.ordered
|
64
|
+
@handler.should_receive(:exit).once.ordered
|
65
|
+
@handler.handle payload, {}
|
66
|
+
end
|
67
|
+
|
68
|
+
it "sends 'command_failed' message when exitstatus is not 0" do
|
69
|
+
status.should_receive(:exitstatus).and_return(99)
|
70
|
+
response = {:type => :command_failed, :hostname => Socket.gethostname,
|
71
|
+
:command => "gem cmd gorgon", :stdout => "output", :stderr => "errors" }
|
72
|
+
Yajl::Encoder.should_receive(:encode).with(response).and_return :json_msg
|
73
|
+
exchange.should_receive(:publish).with(:json_msg)
|
74
|
+
@handler.handle payload, {}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stub_methods
|
79
|
+
Open4.stub!(:popen4).and_return([1, stdin, stdout, stderr])
|
80
|
+
Process.stub!(:waitpid2).and_return([nil, status])
|
81
|
+
Yajl::Encoder.stub!(:encode).and_return :json_msg
|
82
|
+
@handler.stub!(:exit)
|
83
|
+
end
|
84
|
+
end
|