vorhees 0.0.2
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 +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
|
+
|