Skip to content

Instantly share code, notes, and snippets.

@quad
Created August 20, 2011 08:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quad/1158837 to your computer and use it in GitHub Desktop.
Save quad/1158837 to your computer and use it in GitHub Desktop.
A spec for plaque
require 'json'
require 'socket'
require 'timeout'
TIMEOUT = 2
RSpec::Matchers.define :respond_with do |regexp|
match do |io|
begin
Timeout::timeout(TIMEOUT) { @message = io.readline }
rescue Timeout::Error
false
else
@message =~ regexp
end
end
failure_message_for_should do |io|
if @message
"Expected message matching #{regexp}, instead got: #{@message}"
else
"Timed out waiting for #{regexp}"
end
end
end
RSpec::Matchers.define :receive do |hash|
match do |socket|
begin
Timeout::timeout(TIMEOUT) { @message, addr = socket.recvfrom 65536 }
rescue Timeout::Error
false
else
JSON.parse(@message) == hash
end
end
failure_message_for_should do |socket|
if @message
"Expected message matching #{hash.inspect}, instead got #{@message}"
else
"Timed out waiting for #{hash.inspect}"
end
end
end
RSpec::Matchers.define :find do |node_id|
match do |io|
start = Time.now
until Time.now > start + 5
io.puts "FIND #{node_id}"
io.should respond_with FIND_FOUND
@nodes = []
begin
Timeout::timeout(TIMEOUT) do
# TODO: Check for the trailing "."
while io.readline =~ FIND_NODE
@nodes << $1
end
end
rescue Timeout::Error
break
end
end
@nodes.include? node_id
end
failure_message_for_should do |io|
if @nodes
"Expected #{node_id.inspect} in #{@nodes.inspect}"
else
"Timed out waiting for FIND(#{node_id}) reply"
end
end
end
RSpec::Matchers.define :timeout do
match do |given_proc|
begin
Timeout::timeout(TIMEOUT) do
given_proc.call
end
rescue Timeout::Error
true
else
false
end
end
failure_message_for_should { "Didn't timeout" }
end
def plaque &block
IO.popen './plaque', 'w+', &block
end
HELLO = /^220 ([0-9A-Fa-f]{40}) ([.0-9]+) (\d+)/
PING_OK = /^230/
FIND_FOUND = /^240/
FIND_NODE = /^([0-9A-Fa-f]{40}) ([.0-9]+) (\d+)/
MESSAGE = /^350 ([0-9A-Fa-f]{40}) ([.0-9]+) (\d+)/
SEND_READY = //
WAT = /^500/
ID = 'ffffffffffffffffffffffffffffffffffffffff'
describe 'plaque' do
let(:socket) { UDPSocket.new }
let(:net) { socket.tap { |s| s.connect @ip, @port } }
subject { @plaque = plaque }
after { subject.close }
def say msg
subject.puts msg
end
def send obj, opts={}
if addr = opts[:to]
host, port = addr[3], addr[1]
socket.send obj.to_json, 0, host, port
else
net.send obj.to_json, 0
end
end
def recv
message, addr = socket.recvfrom 65536
return JSON.parse(message), addr
end
it { should respond_with HELLO }
it 'should reuse its node ID between sessions'
context "when it's running" do
before do
subject.should respond_with HELLO
@node_id, @ip, @port = $1, $2, $3.to_i
end
describe '(ping)' do
it 'should respond to a ping' do
send :t => 'abc123', :id => ID
socket.should receive 't' => 'abc123', 'id' => $1, 'r' => true
end
context 'when listening' do
before do
socket.bind '127.0.0.1', 0
net, @socket_port, name, @socket_addr = socket.addr
end
it 'should ping on command' do
say "PING #{@socket_addr} #{@socket_port}"
should respond_with PING_OK
response, addr = recv
response.should include 't'
response['id'].should == @node_id
end
it 'should not respond to a pong' do
say "PING #{@socket_addr} #{@socket_port}"
response, addr = recv
response['id'] = ID
send response, :to => addr
expect { recv }.to timeout
end
it "should generate unique transaction IDs"
end
[
['256.256.256.256', 0],
['1.2.3.4', 99999],
['', '']
].each do |addr, port|
it "should reject an invalid ping command (#{addr}:#{port})" do
say "PING #{addr} #{port}"
should respond_with WAT
end
end
end
describe '(find)' do
it 'should respond to a find node query' do
send :t => 'abc123', :target => ID
socket.should receive 't' => 'abc123', 'nodes' => []
end
context "when a second plaque is connected" do
before do
@second = plaque
@second.should respond_with HELLO
@second_node_id, @second_ip, @second_port = $1, $2, $3.to_i
# TODO: This is a hack. How can we improve?
@second_ip = '127.0.0.1' if @second_ip == '0.0.0.0'
say "PING #{@second_ip} #{@second_port}"
should respond_with PING_OK
end
after { @second.close }
it 'should respond to a find node query with results' do
send :t => 'abc123', :target => @second_node_id
socket.should receive 't' => 'abc123',
'nodes' => [{'id' => @second_node_id,
'ip' => @second_ip,
'port' => @second_port}]
end
it 'should find nodes on command' do
should find @second_node_id
end
end
it 'should reject an invalid find command' do
say 'FIND abc'
should respond_with WAT
end
end
describe '(send)' do
it 'should reject an invalid send command' do
say 'SEND abc'
should respond_with WAT
end
context 'when listening' do
before do
socket.bind '127.0.0.1', 0
net, @socket_port, name, @socket_addr = socket.addr
end
it 'should send a message' do
say "SEND #{@socket_addr} #{@socket_port}"
should respond_with SEND_READY
say 'abc123'
say '.'
say respond_with SEND_SENT
response, addr = recv
response.should include 't'
response['id'].should == @node_id
response['m'].should == 'abc123'
end
end
it 'should receive a message' do
send :t => 'abc123', :id => ID, :m => 'abc123'
should respond_with MESSAGE
$1.should == 'abc123'
should respond_with 'abc123'
should respond_with '.'
end
end
it 'should route messsages between instances' do
plaque do |second|
second.should respond_with HELLO
second_node_id, second_ip, second_port = $1, $2, $3.to_i
say "PING #{second_ip} #{second_port}"
should respond_with PING_OK
should find second_node_id
second.should find @node_id
say "SEND #{second_ip} #{second_port}"
should respond_with SEND_READY
say 'abc123'
say '.'
say respond_with SEND_SENT
second.should respond_with MESSAGE
second.should respond_with 'abc123'
second.should respond_with '.'
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment