unison/unison-src/new-runtime-transcripts/net.md
Stew O'Connor 7a2a8fb28b refactoring new-runtime-transcripts
This adds a _base transcript which will be run as a prelude before the
other transcripts (not sure how yet). The goal is to cut down on the
boilerplate which has been creeping into the tests.

We also rename a few more of the builtin IO functions which return
Either Failure a
2021-02-23 23:56:04 -08:00

4.6 KiB

serverSocket = compose2 reraise serverSocket.impl
socketPort = compose reraise socketPort.impl
listen = compose reraise listen.impl
closeSocket = compose reraise closeSocket.impl
clientSocket = compose2 reraise clientSocket.impl
socketSend = compose2 reraise socketSend.impl
socketReceive = compose2 reraise socketReceive.impl
socketAccept = compose reraise socketAccept.impl
.> add

Tests for network related builtins

Creating server sockets

This section tests functions in the IO builtin related to binding to TCP server socket, as to be able to accept incoming TCP connections.

    builtin.io2.IO.serverSocket : Optional Text -> Text ->{io2.IO} Either Failure io2.Socket

This function takes two parameters, The first is the Hostname. If None is provided, We will attempt to bind to 0.0.0.0 (All ipv4 addresses). We currently only support IPV4 (we should fix this!) The second is the name of the port to bind to. This can be a decimal representation of a port number between 1-65535. This can be a named port like "ssh" (for port 22) or "kermit" (for port 1649), This mapping of names to port numbers is maintained by the nsswitch service, typically stored in /etc/services and queried with the getent tool:

# map number to name
$ getent services 22
ssh                   22/tcp

# map name to number
$ getent services finger
finger                79/tcp

# get a list of all known names
$ getent services | head
tcpmux                1/tcp
echo                  7/tcp
echo                  7/udp
discard               9/tcp sink null
discard               9/udp sink null
systat                11/tcp users
daytime               13/tcp
daytime               13/udp
netstat               15/tcp
qotd                  17/tcp quote

Below shows different examples of how we might specify the server coordinates.

testExplicitHost : '{io2.IO} [Result]
testExplicitHost _ = 
  test = 'let
    sock = serverSocket (Some "127.0.0.1") "1028"
    emit (Ok "successfully created socket")
    port = socketPort sock
    putBytes (stdHandle StdOut) (toUtf8 (toText port))
    expectU "should have bound to port 1028" 1028 port 

  runTest test

testDefaultHost : '{io2.IO} [Result]
testDefaultHost _ = 
  test = 'let
    sock = serverSocket None "1028"
    emit (Ok "successfully created socket")
    port = socketPort sock
    putBytes (stdHandle StdOut) (toUtf8 (toText port))
    expectU "should have bound to port 1028" 1028 port 

  runTest test

testDefaultPort : '{io2.IO} [Result]
testDefaultPort _ = 
  test = 'let
    sock = serverSocket None "0"
    emit (Ok "successfully created socket")
    port = socketPort sock
    putBytes (stdHandle StdOut) (toUtf8 (toText port))
    
    check "port should be > 1024" (1024 < port)
    check "port should be < 65536" (65536 > port)

  runTest test
.> add
.> io.test testDefaultPort

This example demonstrates connecting a TCP client socket to a TCP server socket. A thread is started for both client and server. The server socket asks for any availalbe port (by passing "0" as the port number). The server thread then queries for the actual assigned port number, and puts that into an MVar which the client thread can read. The client thread then reads a string from the server and reports it back to the main thread via a different MVar.


serverThread: MVar Nat -> Text -> '{io2.IO}()
serverThread portVar toSend = 'let
  go : '{io2.IO, Exception}()
  go = 'let
    sock = serverSocket (Some "127.0.0.1") "0"
    port = socketPort sock
    put portVar port
    listen sock
    sock' = socketAccept sock
    socketSend sock' (toUtf8 toSend)
    closeSocket sock'

  match (toEither go) with 
    Left (Failure _ t _) -> watch t ()
    _ -> ()

clientThread : MVar Nat -> MVar Text -> '{io2.IO}()
clientThread portVar resultVar = 'let
  go = 'let
    port = take portVar
    sock = clientSocket "127.0.0.1" (Nat.toText port)
    msg = fromUtf8 (socketReceive sock 100)
    put resultVar msg

  match (toEither go) with
    Left (Failure _ t _) -> watch t ()
    _ -> ()

testTcpConnect : '{io2.IO}[Result]
testTcpConnect = 'let 
  test = 'let
    portVar = !MVar.newEmpty
    resultVar = !MVar.newEmpty
    
    toSend = "12345"

    forkComp (serverThread portVar toSend)
    forkComp (clientThread portVar resultVar)
    
    received = take resultVar

    expectU "should have reaped what we've sown" toSend received
    
  runTest test
    

.> add
.> io.test testTcpConnect