Permalink
Browse files

Added specs or Zombie protocol.

  • Loading branch information...
1 parent 88fc95e commit 45aba851be15047db4ac540ba97a14e6e2671336 @assaf assaf committed Jan 26, 2011
Showing with 184 additions and 82 deletions.
  1. +2 −2 CHANGELOG.md
  2. +105 −0 spec/protocol.coffee
  3. +77 −80 src/zombie/protocol.coffee
View
@@ -34,8 +34,8 @@ Modified `lastRequest`/`lastResponse` to use the window resources, fixed
`browser.status` and `browser.redirected` to only look at the page
resource itself.
- 278 Tests
- 4.2 sec to complete
+ 282 Tests
+ 4.3 sec to complete
### Version 0.8.10 2011-01-13
View
@@ -0,0 +1,105 @@
+require "./helpers"
+{ vows: vows, assert: assert, zombie: zombie, brains: brains } = require("vows")
+NET = require("net")
+
+
+listen = (callback)=>
+ if @active
+ process.nextTick callback
+ else
+ @active = true
+ zombie.listen 8091, (error)=>
+ brains.ready =>
+ process.nextTick -> callback error
+
+class Client
+ constructor: ->
+ stream = NET.createConnection(8091, "localhost")
+ stream.setNoDelay true
+ data = ""
+ stream.on "data", (chunk)=>
+ @response = undefined
+ error = null
+ data += chunk
+ while /\r\n/.test(data)
+ data = data.replace /^\-(.*)\r\n/, (_, error)->
+ error = new Error(error)
+ return ""
+ data = data.replace /^\+(.*)\r\n/, (_, string)=>
+ @response = string
+ return ""
+ data = data.replace /^\:(\d+)\r\n/, (_, integer)=>
+ @response = parseInt(integer, 10)
+ return ""
+ length = null
+ data = data.replace /^\$(\d+)\r\n/, (_, a)->
+ length = parseInt(a, 10)
+ return ""
+ if length == -1
+ @response = null
+ else if length != null
+ @response = data.slice(0, length)
+ data = data.slice(length + 2)
+ length = null
+ data = data.replace /^\*(\d+)\r\n/, (_, a)->
+ length = parseInt(a, 10)
+ return ""
+ if length == -1
+ @response = null
+ else if length != null
+ @response = []
+ for i in [0...length]
+ size = 0
+ data = data.replace /^\$(\d+)\r\n/, (_, b)->
+ size = parseInt(b, 10)
+ return ""
+ if size >= 0
+ value = data.slice(0, size)
+ data = data.slice(size + 2)
+ @response.push value
+ else
+ @response.push null
+ if error
+ @callback error
+ else if @response != undefined
+ @callback null, this
+ this.send = (commands, callback)->
+ @callback = callback
+ if commands.join
+ commands = [commands] unless commands[0].join
+ stream.write commands.map((argv)-> "*#{argv.length}\r\n" + argv.map((arg)-> "$#{arg.length}\r\n#{arg}").join("") ).join("")
+ else
+ stream.write "*1\r\n$#{commands.length}\r\n#{commands}"
+
+
+execute = (tests)->
+ commands = tests.commands
+ delete tests.commands
+ tests.topic = (client)->
+ listen (error)=>
+ client ||= new Client
+ client.send commands, @callback
+ return tests
+
+
+vows.describe("Protocol").addBatch(
+ "echo":
+ execute
+ commands: ["ECHO", "Hello"]
+ "should echo Hello": (client)-> assert.equal client.response, "Hello"
+
+ "visit":
+ execute
+ commands: ["VISIT", "http://localhost:3003/"]
+ "should return OK": (client)-> assert.equal client.response, "OK"
+
+ "visit and wait":
+ execute
+ commands: [["VISIT", "http://localhost:3003/"], ["WAIT"]]
+ "should return OK": (client)-> assert.equal client.response, "OK"
+ "status":
+ execute
+ commands: "STATUS"
+ "should return status code": (client)-> assert.equal client.response, 200
+
+).export(module)
View
@@ -9,40 +9,17 @@ MULTI = 3
class Context
- constructor: (@stream)->
+ constructor: (@stream, @debug)->
this.reset()
- reset: ->
- @browser = new module.parent.exports.Browser(debug: debug)
-
-# Server-side of the Zombie protocol.
-# See http://redis.io/topics/protocol
-class Protocol
- constructor: (port)->
- port ||= 8091
- active = false
- commands = {}
- debug = false
- server = net.createServer (stream)->
- # For each connection (stream): no delay, send data as soon as
- # it's available.
- stream.setNoDelay true
- input = ""
- context = new Context(stream)
- stream.on "data", (chunk)->
- # Collect input and process as much as possible.
- input = process(context, input + chunk)
- stream.on "end", ->
- # Collect input and process as much as possible.
- process context, input
-
- # ## Processing
-
argc = 0 # Number of arguments
argl = 0 # Size of next argument
argv = [] # Received arguments
+ input = "" # Remaining input to process
+ last = null # The last command in the queue.
- # Process the currently available input, returns remaining input.
- process = (context, input)->
+ # Process the currently available input.
+ this.process = (chunk)->
+ input += chunk if chunk
if argc
# We're here because we're waiting for argc arguments to arrive
# before we can execute the next requet.
@@ -58,58 +35,57 @@ class Protocol
if argv.length == argc
# We have all the arguments we expect, run a command and
# reset argc/argv to await the next command.
- queue context, argv
+ queue argv
argc = 0
argv = []
# See if we have more input to process.
- return process(context, input) if input.length > 0
+ this.process() if input.length > 0
else
# We're here because we expect to read the argument length:
# $<number of bytes of argument 1> CR LF
- input = input.replace /^\$(\d+)\r\n/, (_, value)->
+ input = input.replace /^\$(\d+)\r\n/, (_, value)=>
argl = parseInt(value, 10)
- console.log "Expecting argument of size #{argl}" if debug
+ console.log "Expecting argument of size #{argl}" if @debug
return ""
if argl
- return process(context, input)
+ this.process()
else
throw new Error("Expecting $<argc>CRLF") if input.length > 0 && input[0] != "$"
else
# We're here because we epxect to read the number of arguments:
# *<number of arguments> CR LF
- input = input.replace /^\*(\d+)\r\n/, (_, value)->
+ input = input.replace /^\*(\d+)\r\n/, (_, value)=>
argc = parseInt(value, 10)
- console.log "Expecting #{argc} arguments" if debug
+ console.log "Expecting #{argc} arguments" if @debug
return ""
if argc
- return process(context, input)
+ this.process()
else
- console.log input.length
throw new Error("Expecting *<argc>CRLF") if input.length > 0 && input[0] != "*"
- return input
- # The last command in the queue.
- last = null
# Queue next command to execute (since we're pipelining, we wait for
# the previous command to complete and send its output first).
- queue = (context, argv)->
+ queue = (argv)=>
command = {}
# Invoke this command.
- command.invoke = ->
- if fn = commands[argv[0]]
- console.log "Executing #{argv.join(" ")}" if debug
- argv[0] = command.reply
- fn.apply context, argv
- else
- command.reply ERROR, "Unknown command #{argv[0]}"
+ command.invoke = =>
+ try
+ if fn = this[argv[0].toLowerCase()]
+ console.log "Executing #{argv.join(" ")}" if debug
+ argv[0] = command.reply
+ fn.apply this, argv
+ else
+ command.reply ERROR, "Unknown command #{argv[0]}"
+ catch error
+ command.reply ERROR, "Failed on #{argv[0]}: #{error.message}"
# Send a reply back to the client and if there's another command
# in the queue, invoke it next.
- command.reply = (type, value)->
- respond context.stream, type, value
+ command.reply = (type, value)=>
+ respond @stream, type, value
last = command.next if last == command
# Invoke next command in queue.
if command.next
- process.nextTick -> command.next.invoke
+ process.nextTick -> command.next.invoke()
if last
# There's another command in the queue, add us at the end.
last.next = command
@@ -145,8 +121,55 @@ class Protocol
else
stream.write "*-1\r\n"
+ # Turns debugging on. To turn debugging off, pass 0 or false as the
+ # argument (no argument required to turn it off).
+ debug: (reply, debug)->
+ this.browser.debug = (debug == "0" || debug == "off")
+
+ # For testing purposes.
+ echo: (reply, text)->
+ reply SINGLE, text
+
+ # Resets the context. Discards current browser.
+ reset: (reply)->
+ @browser = new module.parent.exports.Browser(debug: @debug)
+ reply SINGLE, "OK" if reply
+
+ # Returns the status code of the request for loading the window.
+ status: (reply)-> reply INTEGER, @browser.statusCode || 0
+
+ # Tells browser to visit <url>.
+ visit: (reply, url)->
+ @browser.visit url
+ reply SINGLE, "OK"
+
+ # Tells browser to wait for all events to be processed.
+ #
+ # Replies with error or OK.
+ wait: (reply)->
+ @browser.wait (error)->
+ if error
+ reply ERROR, error.message
+ else
+ reply SINGLE, "OK"
+
+
+# Server-side of the Zombie protocol.
+# See http://redis.io/topics/protocol
+class Protocol
+ constructor: (port)->
+ debug = false
+ server = net.createServer (stream)->
+ # For each connection (stream): no delay, send data as soon as
+ # it's available.
+ stream.setNoDelay true
+ context = new Context(stream, debug)
+ stream.on "data", (chunk)-> context.process chunk
+
# ## Controlling
+ active = false
+ port ||= 8091
# Start listening to incoming requests.
this.listen = (callback)->
listener = (err)->
@@ -166,42 +189,16 @@ class Protocol
this.__defineGetter__ "active", ->active
- # ## Commnands
-
- # For testing purposes.
- commands.ECHO = (reply, text)->
- reply SINGLE, text
-
- # Resets the context. Discards current browser.
- commands.RESET = (reply)->
- this.reset()
- reply SINGLE, "OK"
-
- # Tells browser to visit <url>.
- #
- # Replies with OK.
- commands.VISIT = (reply, url)->
- this.browser.visit url
- reply SINGLE, "OK"
- # Tells browser to wait for all events to be processed.
- #
- # Replies with error or OK.
- commands.WAIT = (reply)->
- this.browser.wait (err)->
- if err
- reply ERROR, err.message
- else
- reply SINGLE, "OK"
-
-
exports.Protocol = Protocol
# ### Zombie.listen port, callback
# ### Zombie.listen socket, callback
+# ### Zombie.listen callback
#
# Ask Zombie to listen on the specified port for requests. The default
# port is 8091, or you can specify a socket name. The callback is
# invoked once Zombie is ready to accept new connections.
exports.listen = (port, callback)->
+ [port, callback] = [8091, port] unless callback
protocol = new Protocol(port)
protocol.listen callback

0 comments on commit 45aba85

Please sign in to comment.