# Wrappers around socket primitives that are easier to test.
#
# To test client operations, use `assume-resources` with a filename that
# begins with a hostname. (Filenames starting with '/' are assumed to be
# local.)
#
# To test server operations, just run a real client against localhost.
scenario example-server-test [
local-scope
# test server without a fake on a random (real) port
# that way repeatedly running the test will give ports time to timeout and
# close before reusing them
make-random-nondeterministic
port:num <- random-in-range 0/real-random-numbers, 8000, 8100
run [
socket:num <- $open-server-socket port
$print [server socket: ], socket, 10/newline
assert socket, [
F - example-server-test: $open-server-socket failed]
$print [starting up server routine], 10/newline
handler-routine:number <- start-running serve-one-request socket, example-handler
]
$print [starting to read from port ], port, 10/newline
source:&:source:char <- start-reading-from-network 0/real-resources, [localhost], [/], port
response:text <- drain source
10:@:char/raw <- copy *response
memory-should-contain [
10:array:character <- [abc]
]
]
# helper just for this scenario
def example-handler query:text -> response:text [
local-scope
load-ingredients
reply [abc]
]
#? scenario example-client-test [
#? local-scope
#? assume-resources [
#? [example.com/] -> [abc]
#? ]
#? run [
#? source:&:source:char <- start-reading-from-network resources, [example.com], [/]
#? ]
#? contents:text <- drain source
#? 10:@:char/raw <- copy *contents
#? memory-should-contain [
#? 10:address:character <- [abc]
#? ]
#? ]
type request-handler = (recipe text -> text)
def serve-one-request socket:num, request-handler:request-handler [
local-scope
load-ingredients
session:num <- $accept socket
$print [server session socket: ], session, 10/newline
assert session, [
F - example-server-test: $accept failed]
contents:&:source:char, sink:&:sink:char <- new-channel 30
sink <- start-running receive-from-socket session, sink
query:text <- drain contents
response:text <- call request-handler, query
write-to-socket session, response
$close-socket session
]
def start-reading-from-network resources:&:resources, host:text, path:text -> contents:&:source:char [
local-scope
load-ingredients
$print [running start-reading-from-network], 10/newline
{
port:num, port-found?:boolean <- next-ingredient
break-if port-found?
port <- copy 80/http-port
}
{
break-if resources
# real network
socket:num <- $open-client-socket host, port
$print [client socket: ], socket, 10/newline
assert socket, [contents]
req:text <- interpolate [GET _ HTTP/1.1], path
request-socket socket, req
contents:&:source:char, sink:&:sink:char <- new-channel 10000
start-running receive-from-socket socket, sink
return
}
# fake network
#? i:num <- copy 0
#? data:&:@:resource <- get *fs, data:offset
#? len:num <- length *data
#? {
#? done?:bool <- greater-or-equal i, len
#? break-if done?
#? tmp:resource <- index *data, i
#? i <- add i, 1
#? curr-filename:text <- get tmp, name:offset
#? found?:bool <- equal filename, curr-filename
#? loop-unless found?
#? contents:&:source:char, sink:&:sink:char <- new-channel 30
#? curr-contents:text <- get tmp, contents:offset
#? start-running transmit-from-text curr-contents, sink
#? return
#? }
return 0/not-found
]
def request-socket socket:num, s:text -> socket:num [
local-scope
load-ingredients
write-to-socket socket, s
$write-to-socket socket, 13/cr
$write-to-socket socket, 10/lf
# empty line to delimit request
$write-to-socket socket, 13/cr
$write-to-socket socket, 10/lf
]
#? def start-writing-socket network:&:local-network, port:num -> sink:&:sink:char, routine-id:num [
#? local-scope
#? load-ingredients
#? source:&:source:char, sink:&:sink:char <- new-channel 30
#? {
#? break-if network
#? socket:num <- $open-server-socket port
#? session:num <- $accept socket
#? # TODO Create channel implementation of write-to-socket.
#? return sink, 0/routine-id
#? }
#? # fake network
#? routine-id <- start-running transmit-to-fake-socket network, port, source
#? ]
#? def transmit-to-fake-socket network:&:local-network, port:num, source:&:source:char -> network:&:local-network, source:&:source:char [
#? local-scope
#? load-ingredients
#? # compute new port connection contents
#? buf:&:buffer <- new-buffer 30
#? {
#? c:char, done?:bool, source <- read source
#? break-unless c
#? buf <- append buf, c
#? break-if done?
#? loop
#? }
#? contents:text <- buffer-to-array buf
#? new-port-connection:&:port-connection <- new-port-connection port, contents
#? # Got the contents of the channel, time to write to fake port.
#? i:num <- copy 0
#? port-connections:&:@:port-connection <- get *network, data:offset
#? len:num <- length *port-connections
#? {
#? done?:bool <- greater-or-equal i, len
#? break-if done?
#? current:port-connection <- index *port-connections, i
#? current-port:num <- get current, port:offset
#? ports-match?:bool <- equal current-port, port
#? i <- add i, 1
#? loop-unless ports-match?
#? # Found an existing connection on this port, overwrite.
#? put-index *port-connections, i, *new-port-connection
#? reply
#? }
#? # Couldn't find an existing connection on this port, initialize a new one.
#? new-len:num <- add len, 1
#? new-port-connections:&:@:port-connection <- new port-connection:type, new-len
#? put *network, data:offset, new-port-connections
#? i:num <- copy 0
#? {
#? done?:bool <- greater-or-equal i, len
#? break-if done?
#? tmp:port-connection <- index *port-connections, i
#? put-index *new-port-connections, i, tmp
#? }
#? put-index *new-port-connections, len, *new-port-connection
#? ]
def receive-from-socket socket:num, sink:&:sink:char -> sink:&:sink:char [
local-scope
load-ingredients
{
$print [read-from-socket ], socket, 10/newline
req:text, eof?:bool <- $read-from-socket socket, 4096/bytes
loop-unless req
bytes-read:num <- length *req
i:num <- copy 0
{
done?:bool <- greater-or-equal i, bytes-read
break-if done?
c:char <- index *req, i # todo: unicode
sink <- write sink, c
i <- add i, 1
loop
}
loop-unless eof?
}
sink <- close sink
]
def write-to-socket socket:num, s:text [
local-scope
load-ingredients
len:num <- length *s
i:num <- copy 0
{
done?:bool <- greater-or-equal i, len
break-if done?
c:char <- index *s, i
$write-to-socket socket, c
i <- add i, 1
loop
}
]