vorhees 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -0
- data/Manifest +11 -0
- data/README +24 -0
- data/Rakefile +56 -0
- data/lib/vorhees.rb +2 -0
- data/lib/vorhees/client.rb +243 -0
- data/lib/vorhees/matchers.rb +78 -0
- data/spec/client_spec.rb +84 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/usage_spec.rb +165 -0
- data/vorhees.gemspec +30 -0
- metadata +82 -0
data/Gemfile
ADDED
data/Manifest
ADDED
data/README
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
http://github.com/davidlee/vorhees
|
2
|
+
|
3
|
+
Vorhees is a simple JSON client and accompanying rspec matcher.
|
4
|
+
|
5
|
+
It's designed to aid the authors of simple JSON socket protocols & servers.
|
6
|
+
|
7
|
+
It's opinionated, in that it expects you'll use a key (the default is
|
8
|
+
'command') to differentiate types of message; it assumes that the commands are
|
9
|
+
uppercase; and that you're using rspec -- other than that it should be
|
10
|
+
reasonably universal.
|
11
|
+
|
12
|
+
As an example, the following will assert that a message was received within 2
|
13
|
+
seconds, with the values {"command":"HELLO", "recipient":"world"}. Additional
|
14
|
+
values are allowed (i.e., a "body" field wouldn't cause the spec to fail):
|
15
|
+
|
16
|
+
@client = Vorhees::Client.new(:host => 'localhost', :port => 1234)
|
17
|
+
@client.should receive(:hello, :timeout => 2.0) { |msg|
|
18
|
+
msg['recipient].should == 'world'
|
19
|
+
}
|
20
|
+
|
21
|
+
If you're interested, let me know and I'll try to add spec coverage for some
|
22
|
+
of the other (currently undocumented) features.
|
23
|
+
|
24
|
+
Provided under the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'echoe'
|
10
|
+
Echoe.new('vorhees', '0.0.2') do |p|
|
11
|
+
p.description = "An opinionated JSON socket server client and matchers"
|
12
|
+
p.url = "http://github.com/davidlee/vorhees"
|
13
|
+
p.author = "David Lee"
|
14
|
+
p.email = "david at davelee.com.au"
|
15
|
+
p.ignore_pattern = ["tmp/*", "script/*"]
|
16
|
+
p.development_dependencies = []
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
end
|
20
|
+
|
21
|
+
begin
|
22
|
+
require 'cucumber/rake/task'
|
23
|
+
rescue LoadError
|
24
|
+
puts "cucumber is not installed."
|
25
|
+
end
|
26
|
+
|
27
|
+
require '.bundle/environment'
|
28
|
+
Bundler.setup()
|
29
|
+
|
30
|
+
if defined? Cucumber
|
31
|
+
namespace :features do
|
32
|
+
Cucumber::Rake::Task.new(:all) do |t|
|
33
|
+
t.cucumber_opts = %w{--format pretty --color}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
task :features => 'features:all'
|
37
|
+
end
|
38
|
+
|
39
|
+
namespace :spec do
|
40
|
+
desc "Run the code examples in spec/*"
|
41
|
+
Spec::Rake::SpecTask.new(:all) do |t|
|
42
|
+
t.spec_opts = ['-c', '--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
|
43
|
+
t.spec_files = FileList["spec/**/*_spec.rb"]
|
44
|
+
end
|
45
|
+
|
46
|
+
Dir['spec/**'].select {|f| File.directory? f }.map {|f| File.split(f).last }.each do |folder|
|
47
|
+
desc "Run the code examples in spec/#{folder}"
|
48
|
+
Spec::Rake::SpecTask.new(folder.to_sym) do |t|
|
49
|
+
t.spec_opts = ['-c', '--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
|
50
|
+
t.spec_files = FileList["spec/#{folder}/*_spec.rb"]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
task :spec => "spec:all"
|
56
|
+
task :default => :spec
|
data/lib/vorhees.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
unless Object.const_defined?('ActiveSupport')
|
5
|
+
class Hash
|
6
|
+
# Return a new hash with all keys converted to symbols.
|
7
|
+
def symbolize_keys
|
8
|
+
inject({}) do |options, (key, value)|
|
9
|
+
options[(key.to_sym rescue key) || key] = value
|
10
|
+
options
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Destructively convert all keys to symbols.
|
15
|
+
def symbolize_keys!
|
16
|
+
self.replace(self.symbolize_keys)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Vorhees
|
22
|
+
class Client
|
23
|
+
attr_accessor :socket, :buffer, :sent, :received, :options, :env
|
24
|
+
|
25
|
+
if RUBY_VERSION < '1.9'
|
26
|
+
require 'system_timer'
|
27
|
+
else
|
28
|
+
SystemTimer = Timeout
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
GOT_NOTHING = nil
|
33
|
+
GOT_DATA = 1
|
34
|
+
GOT_MESSAGE = 2
|
35
|
+
|
36
|
+
#cattr_accessor :defaults
|
37
|
+
def self.defaults
|
38
|
+
@@defaults
|
39
|
+
end
|
40
|
+
|
41
|
+
@@defaults = {
|
42
|
+
:timeout => 0.06,
|
43
|
+
:eof => "\n",
|
44
|
+
:key => 'command',
|
45
|
+
:host => 'localhost',
|
46
|
+
:port => 80,
|
47
|
+
:bufsize => 1024
|
48
|
+
}
|
49
|
+
|
50
|
+
def self.const_missing k
|
51
|
+
if k =~ /^DEFAULT_(.*)$/
|
52
|
+
@@defaults[$1.to_s.downcase.to_sym]
|
53
|
+
else super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.set_defaults(options={})
|
58
|
+
defaults.merge! options.symbolize_keys!
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(options={})
|
62
|
+
@options = Client.defaults.merge(options.symbolize_keys!)
|
63
|
+
@socket = TCPSocket.new(options[:host], options[:port])
|
64
|
+
@env = {}
|
65
|
+
@buffer = ''
|
66
|
+
clear
|
67
|
+
end
|
68
|
+
|
69
|
+
def eof
|
70
|
+
options[:eof]
|
71
|
+
end
|
72
|
+
|
73
|
+
alias :messages :received
|
74
|
+
|
75
|
+
# client.sends 'ERROR', :message => 'INVALID_RECORD'
|
76
|
+
# => '{"command":"ERROR", "message":"INVALID_RECORD"}'
|
77
|
+
def sends *args
|
78
|
+
if args.last.is_a?(Hash)
|
79
|
+
send_message(*args)
|
80
|
+
else
|
81
|
+
begin
|
82
|
+
JSON.unparse(args.flatten.first)
|
83
|
+
send_data(*args)
|
84
|
+
rescue
|
85
|
+
send_message(*args)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def send_message *args
|
91
|
+
values = args.last.is_a?(Hash) ? args.pop : {}
|
92
|
+
values[options[:key]] = args.shift if args.first.is_a?(String)
|
93
|
+
send_json values
|
94
|
+
end
|
95
|
+
|
96
|
+
def send_json hash
|
97
|
+
send_data hash.to_json # JSON.unparse(hash)
|
98
|
+
end
|
99
|
+
|
100
|
+
def send_data(data)
|
101
|
+
data = data.chomp(options[:eof]) + eof
|
102
|
+
sent << data
|
103
|
+
socket.print data
|
104
|
+
socket.flush
|
105
|
+
end
|
106
|
+
|
107
|
+
def wait_for_responses(opts={})
|
108
|
+
wait_for opts do
|
109
|
+
if options[:exactly]
|
110
|
+
received.length == options[:exactly]
|
111
|
+
else
|
112
|
+
received.length >=(options[:at_least] || 1)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
received
|
116
|
+
end
|
117
|
+
|
118
|
+
def wait_for_response opts={}
|
119
|
+
opts = options.merge(opts)
|
120
|
+
response = wait_for_responses opts
|
121
|
+
yield(response.first && response.first.parse) if block_given?
|
122
|
+
response.first
|
123
|
+
end
|
124
|
+
alias :response :wait_for_response
|
125
|
+
|
126
|
+
# FIXME use wait_for, clean this up
|
127
|
+
def discard_responses_until(value, opts={})
|
128
|
+
opts = options.merge(opts)
|
129
|
+
SystemTimer.timeout(opts[:timeout]) do
|
130
|
+
loop do
|
131
|
+
wait_for_response opts do |msg|
|
132
|
+
if msg && msg[Client::DEFAULT_KEY] == value
|
133
|
+
return msg
|
134
|
+
else
|
135
|
+
opts[:debug] ? p(received.shift) : received.shift
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def wait_for opts={}, &block
|
143
|
+
opts = options.merge(opts)
|
144
|
+
assertion_failed = nil
|
145
|
+
test = lambda do
|
146
|
+
begin
|
147
|
+
yield
|
148
|
+
rescue Spec::Expectations::ExpectationNotMetError => e
|
149
|
+
assertion_failed = true
|
150
|
+
# if there's a background server running, now is a good time
|
151
|
+
# to let it do it's thing.
|
152
|
+
Thread.pass
|
153
|
+
retry
|
154
|
+
end
|
155
|
+
end
|
156
|
+
SystemTimer.timeout(opts[:timeout]) do
|
157
|
+
until test.call do
|
158
|
+
receive_data options
|
159
|
+
end
|
160
|
+
end
|
161
|
+
rescue Timeout::Error
|
162
|
+
ensure
|
163
|
+
# let the error be thrown one more time, this time it won't be caught
|
164
|
+
yield if assertion_failed
|
165
|
+
end
|
166
|
+
|
167
|
+
def consume_message
|
168
|
+
received.shift.parse rescue nil
|
169
|
+
end
|
170
|
+
|
171
|
+
def consume
|
172
|
+
v = received.parse
|
173
|
+
@received = [].extend MessageList
|
174
|
+
v
|
175
|
+
end
|
176
|
+
|
177
|
+
def clear
|
178
|
+
@sent = [].extend MessageList
|
179
|
+
@received = [].extend MessageList
|
180
|
+
# @buffer = ""
|
181
|
+
end
|
182
|
+
|
183
|
+
def connected?
|
184
|
+
!disconnected?
|
185
|
+
end
|
186
|
+
|
187
|
+
# for flash XMLSocket
|
188
|
+
def request_policy_file
|
189
|
+
@socket.print "<policy-file-request/>\0"
|
190
|
+
self
|
191
|
+
end
|
192
|
+
|
193
|
+
def disconnected?
|
194
|
+
wait_for :timeout => 0.1 do
|
195
|
+
begin
|
196
|
+
return socket && socket.eof?
|
197
|
+
rescue Errno::ECONNRESET
|
198
|
+
return true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
return false
|
202
|
+
end
|
203
|
+
|
204
|
+
module MessageList
|
205
|
+
def parse
|
206
|
+
map { |json| JSON.parse json }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
module MessageString
|
211
|
+
def parse
|
212
|
+
JSON.parse self
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def receive_data opts={}
|
219
|
+
opts = options.merge(opts) # not really necessary here
|
220
|
+
start_time = Time.now
|
221
|
+
begin
|
222
|
+
data = socket.read_nonblock opts[:bufsize]
|
223
|
+
if data.match(opts[:eof] || '')
|
224
|
+
data, @buffer = (buffer + data).split(opts[:eof]), ''
|
225
|
+
data.each {|str| str.extend MessageString }
|
226
|
+
@received += data
|
227
|
+
@received.extend MessageList
|
228
|
+
GOT_MESSAGE
|
229
|
+
else
|
230
|
+
@buffer += data
|
231
|
+
GOT_DATA
|
232
|
+
end
|
233
|
+
elapsed = (Time.now.to_f - start_time.to_f)
|
234
|
+
elapsed = (elapsed * 10000).round / 10000
|
235
|
+
rescue Errno::EAGAIN, EOFError
|
236
|
+
# yield to background thread if there is one
|
237
|
+
Thread.pass
|
238
|
+
GOT_NOTHING
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Vorhees
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
def receive_nothing(options={})
|
5
|
+
Spec::Matchers::Matcher.new :receive_nothing do
|
6
|
+
match do |client|
|
7
|
+
client.clear
|
8
|
+
if client.received.empty?
|
9
|
+
client.wait_for_responses options
|
10
|
+
end
|
11
|
+
client.received.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
failure_message_for_should do |client|
|
15
|
+
"Expected nothing but received #{client.received}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def receive(expected=nil, options={})
|
21
|
+
Spec::Matchers::Matcher.new :receive, expected do |_expected_|
|
22
|
+
raw = nil
|
23
|
+
msg = nil
|
24
|
+
err = nil
|
25
|
+
|
26
|
+
match do |client|
|
27
|
+
if client.received.empty?
|
28
|
+
client.wait_for_responses options
|
29
|
+
end
|
30
|
+
if expected == false && !block_given?
|
31
|
+
raw = client.received.shift
|
32
|
+
msg = raw.parse if raw
|
33
|
+
client.received.empty?
|
34
|
+
else
|
35
|
+
raw = client.received.shift || raise('No Message Received')
|
36
|
+
msg = raw.parse
|
37
|
+
if block_given?
|
38
|
+
begin
|
39
|
+
msg['command'].should == expected.to_s.upcase if expected
|
40
|
+
yield msg # stick expectations in the block
|
41
|
+
true
|
42
|
+
rescue Spec::Expectations::ExpectationNotMetError => e
|
43
|
+
err = e
|
44
|
+
false
|
45
|
+
end
|
46
|
+
else
|
47
|
+
case expected
|
48
|
+
when '?'
|
49
|
+
$stderr.puts ' ?? -----> ' + msg.inspect
|
50
|
+
true
|
51
|
+
when String
|
52
|
+
msg == JSON.parse(expected)
|
53
|
+
when Symbol
|
54
|
+
msg['command'] == expected.to_s.upcase
|
55
|
+
else
|
56
|
+
msg == expected
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end # match
|
62
|
+
|
63
|
+
failure_message_for_should do |actual|
|
64
|
+
if err
|
65
|
+
"#{err.inspect} \n\n-- #{raw}"
|
66
|
+
elsif expected == false
|
67
|
+
"Expected no message but received #{raw.inspect}"
|
68
|
+
elsif msg.nil?
|
69
|
+
"Expected a message, but got nothing."
|
70
|
+
else
|
71
|
+
"Expected #{expected.inspect} but received #{msg.inspect} --> #{raw}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), './spec_helper')
|
2
|
+
|
3
|
+
describe Vorhees::Client do
|
4
|
+
Client = Vorhees::Client
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
TCPSocket.should_receive(:new).any_number_of_times
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'defaults' do
|
11
|
+
it 'should be the default options for new instances' do
|
12
|
+
c = Vorhees::Client.new
|
13
|
+
c.options.should == Vorhees::Client.defaults
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should be overridden by any supplied options' do
|
17
|
+
c = Vorhees::Client.new 'timeout' => 2, :port => 8080
|
18
|
+
c.options[:timeout].should == 2
|
19
|
+
c.options[:port].should == 8080
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'set_defaults' do
|
24
|
+
it 'should change the defaults for new instances' do
|
25
|
+
Vorhees::Client.set_defaults 'port' => 8080, :eof => "\000"
|
26
|
+
Vorhees::Client.defaults[:port].should == 8080
|
27
|
+
Vorhees::Client.defaults[:eof].should == "\000"
|
28
|
+
c = Vorhees::Client.new
|
29
|
+
c.options[:eof].should == "\000"
|
30
|
+
c.options[:port].should == 8080
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should not modify the options of extant instances' do
|
34
|
+
c = Vorhees::Client.new
|
35
|
+
lambda {
|
36
|
+
Vorhees::Client.set_defaults :eof => "\000"
|
37
|
+
}.should_not change(c, :options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#env' do
|
42
|
+
it 'should be a user-assignable hash' do
|
43
|
+
c = Vorhees::Client.new
|
44
|
+
c.env.should == {}
|
45
|
+
c.env[:connection_id] = 'foo'
|
46
|
+
c.env[:connection_id].should == 'foo'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
describe 'a new client' do
|
52
|
+
describe '#buffer' do
|
53
|
+
it 'should be an empty string' do
|
54
|
+
Client.new.buffer.should == ''
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#sent' do
|
59
|
+
it 'should be empty' do
|
60
|
+
Client.new.sent.should == []
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe '#received' do
|
65
|
+
it 'should be empty' do
|
66
|
+
Client.new.received.should == []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#socket' do
|
71
|
+
it 'should return the raw socket'
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#options' do
|
75
|
+
it 'should be the options merged with defaults' do
|
76
|
+
o = {:eof => 'XXX'}
|
77
|
+
c = Client.new(o)
|
78
|
+
c.options[:eof].should == "XXX"
|
79
|
+
c.options[:timeout].should == Client.defaults[:timeout]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
$:.unshift File.dirname(__FILE__)
|
3
|
+
|
4
|
+
require '.bundle/environment'
|
5
|
+
Bundler.setup()
|
6
|
+
|
7
|
+
require 'pp'
|
8
|
+
# require 'spec'
|
9
|
+
require 'lib/vorhees/client'
|
10
|
+
require 'lib/vorhees/matchers'
|
11
|
+
require 'eventmachine'
|
12
|
+
|
13
|
+
class MockSocket
|
14
|
+
attr_accessor :received, :sent
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@received = []
|
18
|
+
@sent = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def print data
|
22
|
+
sent << data
|
23
|
+
end
|
24
|
+
|
25
|
+
def flush
|
26
|
+
# noop
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# simple EM json server
|
31
|
+
class TestServer < EventMachine::Connection
|
32
|
+
EOF= "\n"
|
33
|
+
|
34
|
+
def post_init
|
35
|
+
@buffer = ''
|
36
|
+
end
|
37
|
+
|
38
|
+
# ensure a null byte at EOF
|
39
|
+
def send_data(data)
|
40
|
+
unless data[-1] == 0
|
41
|
+
data << EOF
|
42
|
+
end
|
43
|
+
super data
|
44
|
+
Thread.pass
|
45
|
+
end
|
46
|
+
|
47
|
+
def receive_data(data)
|
48
|
+
@buffer << data
|
49
|
+
@buffer = process_whole_messages(@buffer)
|
50
|
+
end
|
51
|
+
|
52
|
+
# process any whole messages in the buffer,
|
53
|
+
# and return the new contents of the buffer
|
54
|
+
def process_whole_messages(data)
|
55
|
+
return data if data !~ /#{EOF}/ # only process if data contains a \0 char
|
56
|
+
messages = data.split(EOF)
|
57
|
+
if data =~ /#{EOF}$/
|
58
|
+
data = ''
|
59
|
+
else
|
60
|
+
# remove the last message from the list (because it is incomplete) before processing
|
61
|
+
data = messages.pop
|
62
|
+
end
|
63
|
+
messages.each {|message| process_message(message.strip)}
|
64
|
+
return data
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_message(ln)
|
68
|
+
request = nil
|
69
|
+
begin
|
70
|
+
request = JSON.parse(ln)
|
71
|
+
rescue JSON::ParserError => e
|
72
|
+
error ["CorruptJSON", ln]
|
73
|
+
send_error 'corrupt_JSON'
|
74
|
+
raise ['CorruptJSON', ln].inspect
|
75
|
+
end
|
76
|
+
dispatch request
|
77
|
+
end
|
78
|
+
|
79
|
+
def dispatch(request)
|
80
|
+
# usually this would be a case on request['command']
|
81
|
+
# in this case just delay delivery if the request contains
|
82
|
+
# 'delay' => sec
|
83
|
+
delay = request['delay'].to_f
|
84
|
+
if delay > 0
|
85
|
+
# Ha, looks like EM::Timer doesn't allow floats in 1.8.7 ?
|
86
|
+
EM::Timer.new(delay) do
|
87
|
+
send_data request.to_json + EOF
|
88
|
+
end
|
89
|
+
else
|
90
|
+
send_data request.to_json + EOF
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/spec/usage_spec.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), './spec_helper')
|
2
|
+
|
3
|
+
TEST_SERVER_PORT = 4001
|
4
|
+
@@em = nil
|
5
|
+
|
6
|
+
def with_server options={}, &bl
|
7
|
+
Thread.abort_on_exception = true
|
8
|
+
@@em ||= Thread.fork do
|
9
|
+
EM.run do
|
10
|
+
EM.start_server('localhost', TEST_SERVER_PORT, TestServer)
|
11
|
+
end
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
if block_given?
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Vorhees::Client do
|
20
|
+
include Vorhees::Matchers
|
21
|
+
Client = Vorhees::Client
|
22
|
+
|
23
|
+
context 'mocked socket' do
|
24
|
+
before :each do
|
25
|
+
@socket = MockSocket.new
|
26
|
+
TCPSocket.should_receive(:new).and_return(@socket)
|
27
|
+
@client = Client.new
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
describe '#send_data' do
|
32
|
+
it 'should print a string directly to the socket with the EOF' do
|
33
|
+
@client.options[:eof].should == "\n"
|
34
|
+
str = '{"msg": "HELLO"}'
|
35
|
+
@client.send_data str
|
36
|
+
@socket.sent.should == [str + "\n"]
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'does not append an extra EOF' do
|
40
|
+
@client.options[:eof].should == "\n"
|
41
|
+
str = '{"msg": "HELLO"}' + "\n"
|
42
|
+
@client.send_data str
|
43
|
+
@socket.sent.should == [str]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#sends' do
|
48
|
+
context 'given a hash' do
|
49
|
+
it 'sends the hash as JSON with the EOF' do
|
50
|
+
data = {'command' => 'HELLO', 'payload' => '0xfff'}
|
51
|
+
@client.sends data
|
52
|
+
@socket.sent.length.should == 1
|
53
|
+
JSON.parse(@socket.sent.first).should == data
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'given a symbol' do
|
59
|
+
it 'sends the uppercased symbol as the default key' do
|
60
|
+
@client.options[:key].should == 'command'
|
61
|
+
@client.sends :foo
|
62
|
+
@socket.sent.should == [JSON.unparse('command' => 'FOO') + @client.eof]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'given a string' do
|
67
|
+
it 'sends a JSON pair of the default key and the string as the value' do
|
68
|
+
@client.options[:key].should == 'command'
|
69
|
+
@client.sends 'FOO'
|
70
|
+
@socket.sent.should == [JSON.unparse('command' => 'FOO') + @client.eof]
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'appends any additional values' do
|
74
|
+
@client.sends 'ERROR', :message => 'NOT_FOUND'
|
75
|
+
@client.options[:key].should == 'command'
|
76
|
+
@socket.sent.length.should == 1
|
77
|
+
JSON.parse(@socket.sent.first).should == {'command' => 'ERROR', 'message' => 'NOT_FOUND'}
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'appends an EOF' do
|
81
|
+
data = {'command' => 'HELLO', 'payload' => '0xfff'}
|
82
|
+
@client.sends data
|
83
|
+
@client.sent.first[-1,1].should == @client.eof
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'should receive matcher (running against an echo server)' do
|
90
|
+
it 'sanity check' do
|
91
|
+
with_server do
|
92
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
93
|
+
@client.sends 'HELLO'
|
94
|
+
@client.should receive('{"command":"HELLO"}')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'waits for the specified duration' do
|
99
|
+
with_server do
|
100
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
101
|
+
@client.sends 'HELLO', :delay => 0.1
|
102
|
+
@client.should receive(:hello, :timeout => 0.3)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'throws an error if it does not receive a message in the timeout' do
|
107
|
+
with_server do
|
108
|
+
# sanity test:
|
109
|
+
lambda { SystemTimer.timeout(0.1) { sleep 1 } }.should raise_error
|
110
|
+
|
111
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
112
|
+
@client.sends 'HELLO', :delay => 1
|
113
|
+
lambda {
|
114
|
+
@client.should receive(:hello, :timeout => 0.1)
|
115
|
+
}.should raise_error(RuntimeError)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'raises if the wrong command (or custom :key value) is returned' do
|
120
|
+
with_server do
|
121
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
122
|
+
@client.sends 'HELLO', :delay => 0.2
|
123
|
+
|
124
|
+
lambda {
|
125
|
+
@client.should receive(:goodbye)
|
126
|
+
}.should raise_error #(Spec::Expectations::ExpectationNotMetError, RuntimeError)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'raises if any expectations in the block fail' do
|
131
|
+
with_server do
|
132
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
133
|
+
@client.sends 'HELLO'
|
134
|
+
lambda {
|
135
|
+
@client.should receive(:hello) {|msg| msg['missing'].should_not be_nil }
|
136
|
+
}.should raise_error(Spec::Expectations::ExpectationNotMetError)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'yields the message to the block' do
|
141
|
+
with_server do
|
142
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
143
|
+
@client.sends 'HELLO', :recipient => 'world'
|
144
|
+
recipient = nil
|
145
|
+
@client.should receive(:hello) {|msg| recipient = msg['recipient'] }
|
146
|
+
recipient.should == 'world'
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'prints the message given should receive("?")' do
|
151
|
+
# ok, this was probably going overboard ...
|
152
|
+
require 'stringio'
|
153
|
+
with_server do
|
154
|
+
@client = Client.new(:host => 'localhost', :port => TEST_SERVER_PORT)
|
155
|
+
@client.sends 'HELLO'
|
156
|
+
recipient = nil
|
157
|
+
stderr, $stderr = $stderr, StringIO.new('','w+')
|
158
|
+
@client.should receive('?')
|
159
|
+
stderr, $stderr = $stderr, stderr
|
160
|
+
stderr.rewind
|
161
|
+
stderr.read.gsub(/^.*\{|\}.*$/, '').should == '"command"=>"HELLO"' + "\n"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/vorhees.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{vorhees}
|
5
|
+
s.version = "0.0.2"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["David Lee"]
|
9
|
+
s.date = %q{2010-03-15}
|
10
|
+
s.description = %q{An opinionated JSON socket server client and matchers}
|
11
|
+
s.email = %q{david at davelee.com.au}
|
12
|
+
s.extra_rdoc_files = ["README", "lib/vorhees.rb", "lib/vorhees/client.rb", "lib/vorhees/matchers.rb"]
|
13
|
+
s.files = ["Gemfile", "Manifest", "README", "Rakefile", "lib/vorhees.rb", "lib/vorhees/client.rb", "lib/vorhees/matchers.rb", "spec/client_spec.rb", "spec/spec.opts", "spec/spec_helper.rb", "spec/usage_spec.rb", "vorhees.gemspec"]
|
14
|
+
s.homepage = %q{http://github.com/davidlee/vorhees}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Vorhees", "--main", "README"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{vorhees}
|
18
|
+
s.rubygems_version = %q{1.3.6}
|
19
|
+
s.summary = %q{An opinionated JSON socket server client and matchers}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
else
|
27
|
+
end
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vorhees
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- David Lee
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-15 00:00:00 +11:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: An opinionated JSON socket server client and matchers
|
22
|
+
email: david at davelee.com.au
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- README
|
29
|
+
- lib/vorhees.rb
|
30
|
+
- lib/vorhees/client.rb
|
31
|
+
- lib/vorhees/matchers.rb
|
32
|
+
files:
|
33
|
+
- Gemfile
|
34
|
+
- Manifest
|
35
|
+
- README
|
36
|
+
- Rakefile
|
37
|
+
- lib/vorhees.rb
|
38
|
+
- lib/vorhees/client.rb
|
39
|
+
- lib/vorhees/matchers.rb
|
40
|
+
- spec/client_spec.rb
|
41
|
+
- spec/spec.opts
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
- spec/usage_spec.rb
|
44
|
+
- vorhees.gemspec
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/davidlee/vorhees
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --line-numbers
|
52
|
+
- --inline-source
|
53
|
+
- --title
|
54
|
+
- Vorhees
|
55
|
+
- --main
|
56
|
+
- README
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 1
|
72
|
+
- 2
|
73
|
+
version: "1.2"
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project: vorhees
|
77
|
+
rubygems_version: 1.3.6
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: An opinionated JSON socket server client and matchers
|
81
|
+
test_files: []
|
82
|
+
|