chore: add tests for examples

This commit is contained in:
Ian Davis
2021-05-06 13:39:20 +01:00
parent 76e1ce667e
commit 0343b56ad5
12 changed files with 618 additions and 164 deletions

View File

@@ -40,6 +40,7 @@ import (
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/crypto"
"github.com/libp2p/go-libp2p-core/host"
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/libp2p/go-libp2p-core/peerstore"
@@ -58,6 +59,7 @@ func handleStream(s network.Stream) {
// stream 's' will stay open until you close it (or the other side closes it).
}
func readData(rw *bufio.ReadWriter) {
for {
str, _ := rw.ReadString('\n')
@@ -80,18 +82,20 @@ func writeData(rw *bufio.ReadWriter) {
for {
fmt.Print("> ")
sendData, err := stdReader.ReadString('\n')
if err != nil {
panic(err)
log.Println(err)
return
}
rw.WriteString(fmt.Sprintf("%s\n", sendData))
rw.Flush()
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sourcePort := flag.Int("sp", 0, "Source port number")
dest := flag.String("d", "", "Destination multiaddr string")
help := flag.Bool("help", false, "Display help")
@@ -119,36 +123,62 @@ func main() {
r = rand.Reader
}
// Creates a new RSA key pair for this host.
prvKey, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, r)
h, err := makeHost(ctx, *sourcePort, r)
if err != nil {
panic(err)
}
// 0.0.0.0 will listen on any interface device.
sourceMultiAddr, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", *sourcePort))
// libp2p.New constructs a new libp2p Host.
// Other options can be added here.
host, err := libp2p.New(
context.Background(),
libp2p.ListenAddrs(sourceMultiAddr),
libp2p.Identity(prvKey),
)
if err != nil {
panic(err)
log.Println(err)
return
}
if *dest == "" {
startPeer(ctx, h, handleStream)
} else {
rw, err := startPeerAndConnect(ctx, h, *dest)
if err != nil {
log.Println(err)
return
}
// Create a thread to read and write data.
go writeData(rw)
go readData(rw)
}
// Wait until canceled
select {
case <-ctx.Done():
}
}
func makeHost(ctx context.Context, port int, randomness io.Reader) (host.Host, error) {
// Creates a new RSA key pair for this host.
prvKey, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, randomness)
if err != nil {
log.Println(err)
return nil, err
}
// 0.0.0.0 will listen on any interface device.
sourceMultiAddr, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))
// libp2p.New constructs a new libp2p Host.
// Other options can be added here.
return libp2p.New(
ctx,
libp2p.ListenAddrs(sourceMultiAddr),
libp2p.Identity(prvKey),
)
}
func startPeer(ctx context.Context, h host.Host, streamHandler network.StreamHandler) {
// Set a function as stream handler.
// This function is called when a peer connects, and starts a stream with this protocol.
// Only applies on the receiving side.
host.SetStreamHandler("/chat/1.0.0", handleStream)
h.SetStreamHandler("/chat/1.0.0", streamHandler)
// Let's get the actual TCP port from our listen multiaddr, in case we're using 0 (default; random available port).
var port string
for _, la := range host.Network().ListenAddresses() {
for _, la := range h.Network().ListenAddresses() {
if p, err := la.ValueForProtocol(multiaddr.P_TCP); err == nil {
port = p
break
@@ -156,53 +186,52 @@ func main() {
}
if port == "" {
panic("was not able to find actual local port")
log.Println("was not able to find actual local port")
return
}
fmt.Printf("Run './chat -d /ip4/127.0.0.1/tcp/%v/p2p/%s' on another console.\n", port, host.ID().Pretty())
fmt.Println("You can replace 127.0.0.1 with public IP as well.")
fmt.Printf("\nWaiting for incoming connection\n\n")
log.Printf("Run './chat -d /ip4/127.0.0.1/tcp/%v/p2p/%s' on another console.\n", port, h.ID().Pretty())
log.Println("You can replace 127.0.0.1 with public IP as well.")
log.Println("Waiting for incoming connection")
log.Println()
}
// Hang forever
<-make(chan struct{})
} else {
fmt.Println("This node's multiaddresses:")
for _, la := range host.Addrs() {
fmt.Printf(" - %v\n", la)
func startPeerAndConnect(ctx context.Context, h host.Host, destination string) (*bufio.ReadWriter, error) {
log.Println("This node's multiaddresses:")
for _, la := range h.Addrs() {
log.Printf(" - %v\n", la)
}
fmt.Println()
log.Println()
// Turn the destination into a multiaddr.
maddr, err := multiaddr.NewMultiaddr(*dest)
maddr, err := multiaddr.NewMultiaddr(destination)
if err != nil {
log.Fatalln(err)
log.Println(err)
return nil, err
}
// Extract the peer ID from the multiaddr.
info, err := peer.AddrInfoFromP2pAddr(maddr)
if err != nil {
log.Fatalln(err)
log.Println(err)
return nil, err
}
// Add the destination's peer multiaddress in the peerstore.
// This will be used during connection and stream creation by libp2p.
host.Peerstore().AddAddrs(info.ID, info.Addrs, peerstore.PermanentAddrTTL)
h.Peerstore().AddAddrs(info.ID, info.Addrs, peerstore.PermanentAddrTTL)
// Start a stream with the destination.
// Multiaddress of the destination peer is fetched from the peerstore using 'peerId'.
s, err := host.NewStream(context.Background(), info.ID, "/chat/1.0.0")
s, err := h.NewStream(context.Background(), info.ID, "/chat/1.0.0")
if err != nil {
panic(err)
log.Println(err)
return nil, err
}
log.Println("Established connection to destination")
// Create a buffered stream so that read and writes are non blocking.
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
// Create a thread to read and write data.
go writeData(rw)
go readData(rw)
// Hang forever.
select {}
}
return rw, nil
}

View File

@@ -0,0 +1,72 @@
package main
import (
"context"
"crypto/rand"
"fmt"
"log"
"testing"
"github.com/libp2p/go-libp2p-core/network"
"github.com/libp2p/go-libp2p/examples/testutils"
)
func TestMain(t *testing.T) {
var h testutils.LogHarness
h.Expect("Waiting for incoming connection")
h.Expect("Established connection to destination")
h.Expect("Got a new stream!")
h.Run(t, func() {
// Create a context that will stop the hosts when the tests end
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
port1, err := testutils.FindFreePort(t, "", 5)
if err != nil {
log.Println(err)
return
}
port2, err := testutils.FindFreePort(t, "", 5)
if err != nil {
log.Println(err)
return
}
h1, err := makeHost(ctx, port1, rand.Reader)
if err != nil {
log.Println(err)
return
}
go startPeer(ctx, h1, func(network.Stream) {
log.Println("Got a new stream!")
cancel() // end the test
})
dest := fmt.Sprintf("/ip4/127.0.0.1/tcp/%v/p2p/%s", port1, h1.ID().Pretty())
h2, err := makeHost(ctx, port2, rand.Reader)
if err != nil {
log.Println(err)
return
}
go func() {
rw, err := startPeerAndConnect(ctx, h2, dest)
if err != nil {
log.Println(err)
return
}
rw.WriteString("test message")
rw.Flush()
}()
select {
case <-ctx.Done():
}
})
}

View File

@@ -22,13 +22,42 @@ import (
ma "github.com/multiformats/go-multiaddr"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// LibP2P code uses golog to log messages. They log with different
// string IDs (i.e. "swarm"). We can control the verbosity level for
// all loggers with:
golog.SetAllLoggers(golog.LevelInfo) // Change to INFO for extra info
// Parse options from the command line
listenF := flag.Int("l", 0, "wait for incoming connections")
targetF := flag.String("d", "", "target peer to dial")
insecureF := flag.Bool("insecure", false, "use an unencrypted connection")
seedF := flag.Int64("seed", 0, "set random seed for id generation")
flag.Parse()
if *listenF == 0 {
log.Fatal("Please provide a port to bind on with -l")
}
// Make a host that listens on the given multiaddress
ha, err := makeBasicHost(*listenF, *insecureF, *seedF)
if err != nil {
log.Fatal(err)
}
if *targetF == "" {
runListener(ctx, ha, *listenF, *insecureF)
} else {
runSender(ctx, ha, *targetF)
}
}
// makeBasicHost creates a LibP2P host with a random peer ID listening on the
// given multiaddress. It won't encrypt the connection if insecure is true.
func makeBasicHost(listenPort int, insecure bool, randseed int64) (host.Host, error) {
// If the seed is zero, use real cryptographic randomness. Otherwise, use a
// deterministic randomness source to make generated keys stay the same
// across multiple runs
var r io.Reader
if randseed == 0 {
r = rand.Reader
@@ -53,55 +82,27 @@ func makeBasicHost(listenPort int, insecure bool, randseed int64) (host.Host, er
opts = append(opts, libp2p.NoSecurity)
}
basicHost, err := libp2p.New(context.Background(), opts...)
if err != nil {
return nil, err
}
return libp2p.New(context.Background(), opts...)
}
func getHostAddress(ha host.Host) string {
// Build host multiaddress
hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", basicHost.ID().Pretty()))
hostAddr, _ := ma.NewMultiaddr(fmt.Sprintf("/ipfs/%s", ha.ID().Pretty()))
// Now we can build a full multiaddress to reach this host
// by encapsulating both addresses:
addr := basicHost.Addrs()[0]
fullAddr := addr.Encapsulate(hostAddr)
log.Printf("I am %s\n", fullAddr)
if insecure {
log.Printf("Now run \"./echo -l %d -d %s -insecure\" on a different terminal\n", listenPort+1, fullAddr)
} else {
log.Printf("Now run \"./echo -l %d -d %s\" on a different terminal\n", listenPort+1, fullAddr)
}
return basicHost, nil
addr := ha.Addrs()[0]
return addr.Encapsulate(hostAddr).String()
}
func main() {
// LibP2P code uses golog to log messages. They log with different
// string IDs (i.e. "swarm"). We can control the verbosity level for
// all loggers with:
golog.SetAllLoggers(golog.LevelInfo) // Change to INFO for extra info
// Parse options from the command line
listenF := flag.Int("l", 0, "wait for incoming connections")
target := flag.String("d", "", "target peer to dial")
insecure := flag.Bool("insecure", false, "use an unencrypted connection")
seed := flag.Int64("seed", 0, "set random seed for id generation")
flag.Parse()
if *listenF == 0 {
log.Fatal("Please provide a port to bind on with -l")
}
// Make a host that listens on the given multiaddress
ha, err := makeBasicHost(*listenF, *insecure, *seed)
if err != nil {
log.Fatal(err)
}
func runListener(ctx context.Context, ha host.Host, listenPort int, insecure bool) {
fullAddr := getHostAddress(ha)
log.Printf("I am %s\n", fullAddr)
// Set a stream handler on host A. /echo/1.0.0 is
// a user-defined protocol name.
ha.SetStreamHandler("/echo/1.0.0", func(s network.Stream) {
log.Println("Got a new stream!")
log.Println("listener received new stream")
if err := doEcho(s); err != nil {
log.Println(err)
s.Reset()
@@ -110,27 +111,54 @@ func main() {
}
})
if *target == "" {
log.Println("listening for connections")
select {} // hang forever
if insecure {
log.Printf("Now run \"./echo -l %d -d %s -insecure\" on a different terminal\n", listenPort+1, fullAddr)
} else {
log.Printf("Now run \"./echo -l %d -d %s\" on a different terminal\n", listenPort+1, fullAddr)
}
/**** This is where the listener code ends ****/
// Wait until canceled
select {
case <-ctx.Done():
}
}
func runSender(ctx context.Context, ha host.Host, targetPeer string) {
fullAddr := getHostAddress(ha)
log.Printf("I am %s\n", fullAddr)
// Set a stream handler on host A. /echo/1.0.0 is
// a user-defined protocol name.
ha.SetStreamHandler("/echo/1.0.0", func(s network.Stream) {
log.Println("sender received new stream")
if err := doEcho(s); err != nil {
log.Println(err)
s.Reset()
} else {
s.Close()
}
})
// The following code extracts target's the peer ID from the
// given multiaddress
ipfsaddr, err := ma.NewMultiaddr(*target)
ipfsaddr, err := ma.NewMultiaddr(targetPeer)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
pid, err := ipfsaddr.ValueForProtocol(ma.P_IPFS)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
peerid, err := peer.IDB58Decode(pid)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
// Decapsulate the /ipfs/<peerID> part from the target
@@ -142,23 +170,27 @@ func main() {
// so LibP2P knows how to contact it
ha.Peerstore().AddAddr(peerid, targetAddr, peerstore.PermanentAddrTTL)
log.Println("opening stream")
log.Println("sender opening stream")
// make a new stream from host B to host A
// it should be handled on host A by the handler we set above because
// we use the same /echo/1.0.0 protocol
s, err := ha.NewStream(context.Background(), peerid, "/echo/1.0.0")
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
log.Println("sender saying hello")
_, err = s.Write([]byte("Hello, world!\n"))
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
out, err := ioutil.ReadAll(s)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
log.Printf("read reply: %q\n", out)
@@ -172,7 +204,7 @@ func doEcho(s network.Stream) error {
return err
}
log.Printf("read: %s\n", str)
log.Printf("read: %s", str)
_, err = s.Write([]byte(str))
return err
}

View File

@@ -0,0 +1,58 @@
package main
import (
"context"
"log"
"testing"
"github.com/libp2p/go-libp2p/examples/testutils"
)
func TestMain(t *testing.T) {
var h testutils.LogHarness
h.Expect("listening for connections")
h.Expect("sender opening stream")
h.Expect("sender saying hello")
h.Expect("listener received new stream")
h.Expect("read: Hello, world!")
h.Expect(`read reply: "Hello, world!\n"`)
h.Run(t, func() {
// Create a context that will stop the hosts when the tests end
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Get a tcp port for the listener
lport, err := testutils.FindFreePort(t, "", 5)
if err != nil {
log.Println(err)
return
}
// Get a tcp port for the sender
sport, err := testutils.FindFreePort(t, "", 5)
if err != nil {
log.Println(err)
return
}
// Make listener
lh, err := makeBasicHost(lport, true, 1)
if err != nil {
log.Println(err)
return
}
go runListener(ctx, lh, lport, true)
// Make sender
listenAddr := getHostAddress(lh)
sh, err := makeBasicHost(sport, true, 2)
if err != nil {
log.Println(err)
return
}
runSender(ctx, sh, listenAddr)
})
}

View File

@@ -2,7 +2,7 @@ package main
import (
"context"
"fmt"
"log"
"time"
"github.com/libp2p/go-libp2p"
@@ -17,6 +17,10 @@ import (
)
func main() {
run()
}
func run() {
// The context governs the lifetime of the libp2p node.
// Cancelling it will stop the the host.
ctx, cancel := context.WithCancel(context.Background())
@@ -28,7 +32,7 @@ func main() {
panic(err)
}
fmt.Printf("Hello World, my hosts ID is %s\n", h.ID())
log.Printf("Hello World, my hosts ID is %s\n", h.ID())
// Now, normally you do not just want a simple host, you want
// that is fully configured to best support your p2p application.
@@ -105,5 +109,5 @@ func main() {
h2.Connect(ctx, *pi)
}
*/
fmt.Printf("Hello World, my second hosts ID is %s\n", h2.ID())
log.Printf("Hello World, my second hosts ID is %s\n", h2.ID())
}

View File

@@ -0,0 +1,14 @@
package main
import (
"testing"
"github.com/libp2p/go-libp2p/examples/testutils"
)
func TestMain(t *testing.T) {
var h testutils.LogHarness
h.ExpectPrefix("Hello World, my hosts ID is ")
h.ExpectPrefix("Hello World, my second hosts ID is ")
h.Run(t, run)
}

View File

@@ -13,6 +13,23 @@ import (
ma "github.com/multiformats/go-multiaddr"
)
func main() {
// Choose random ports between 10000-10100
rand.Seed(666)
port1 := rand.Intn(100) + 10000
port2 := port1 + 1
done := make(chan bool, 1)
// Make 2 hosts
h1 := makeRandomNode(port1, done)
h2 := makeRandomNode(port2, done)
log.Printf("This is a conversation between %s and %s\n", h1.ID(), h2.ID())
run(h1, h2, done)
}
// helper method - create a lib-p2p host to listen on a port
func makeRandomNode(port int, done chan bool) *Node {
// Ignoring most errors for brevity
@@ -28,22 +45,11 @@ func makeRandomNode(port int, done chan bool) *Node {
return NewNode(host, done)
}
func main() {
// Choose random ports between 10000-10100
rand.Seed(666)
port1 := rand.Intn(100) + 10000
port2 := port1 + 1
done := make(chan bool, 1)
// Make 2 hosts
h1 := makeRandomNode(port1, done)
h2 := makeRandomNode(port2, done)
func run(h1, h2 *Node, done <-chan bool) {
// connect peers
h1.Peerstore().AddAddrs(h2.ID(), h2.Addrs(), peerstore.PermanentAddrTTL)
h2.Peerstore().AddAddrs(h1.ID(), h1.Addrs(), peerstore.PermanentAddrTTL)
log.Printf("This is a conversation between %s and %s\n", h1.ID(), h2.ID())
// send messages using the protocols
h1.Ping(h2.Host)
h2.Ping(h1.Host)

View File

@@ -0,0 +1,55 @@
package main
import (
"fmt"
"log"
"testing"
"github.com/libp2p/go-libp2p/examples/testutils"
)
func TestMain(t *testing.T) {
port1, err := testutils.FindFreePort(t, "", 5)
if err != nil {
log.Println(err)
return
}
port2, err := testutils.FindFreePort(t, "", 5)
if err != nil {
log.Println(err)
return
}
done := make(chan bool, 1)
h1 := makeRandomNode(port1, done)
h2 := makeRandomNode(port2, done)
var h testutils.LogHarness
// Sequence of log messages when h1 pings h2
pingh1h2 := h.NewSequence("ping h1->h2")
pingh1h2.ExpectPrefix(fmt.Sprintf("%s: Sending ping to: %s", h1.ID(), h2.ID()))
pingh1h2.ExpectPrefix(fmt.Sprintf("%s: Received ping request from %s", h2.ID(), h1.ID()))
pingh1h2.ExpectPrefix(fmt.Sprintf("%s: Received ping response from %s", h1.ID(), h2.ID()))
// Sequence of log messages when h2 pings h1
pingh2h1 := h.NewSequence("ping h2->h1")
pingh2h1.ExpectPrefix(fmt.Sprintf("%s: Sending ping to: %s", h2.ID(), h1.ID()))
pingh2h1.ExpectPrefix(fmt.Sprintf("%s: Received ping request from %s", h1.ID(), h2.ID()))
pingh2h1.ExpectPrefix(fmt.Sprintf("%s: Received ping response from %s", h2.ID(), h1.ID()))
// Sequence of log messages when h1 sends echo to h2
echoh1h2 := h.NewSequence("echo h1->h2")
echoh1h2.ExpectPrefix(fmt.Sprintf("%s: Sending echo to: %s", h1.ID(), h2.ID()))
echoh1h2.ExpectPrefix(fmt.Sprintf("%s: Echo response to %s", h2.ID(), h1.ID()))
// Sequence of log messages when h1 sends echo to h2
echoh2h1 := h.NewSequence("echo h2->h1")
echoh2h1.ExpectPrefix(fmt.Sprintf("%s: Sending echo to: %s", h2.ID(), h1.ID()))
echoh2h1.ExpectPrefix(fmt.Sprintf("%s: Echo response to %s", h1.ID(), h2.ID()))
h.Run(t, func() {
run(h1, h2, done)
})
}

View File

@@ -2,7 +2,7 @@ package main
import (
"context"
"fmt"
"log"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p-core/network"
@@ -14,27 +14,34 @@ import (
)
func main() {
run()
}
func run() {
// Create three libp2p hosts, enable relay client capabilities on all
// of them.
// Tell the host to monitor for relays.
h1, err := libp2p.New(context.Background(), libp2p.EnableRelay(circuit.OptDiscovery))
// Tell the host use relays
h1, err := libp2p.New(context.Background(), libp2p.EnableRelay())
if err != nil {
panic(err)
log.Printf("Failed to create h1: %v", err)
return
}
// Tell the host to relay connections for other peers (The ability to *use*
// a relay vs the ability to *be* a relay)
h2, err := libp2p.New(context.Background(), libp2p.EnableRelay(circuit.OptHop))
if err != nil {
panic(err)
log.Printf("Failed to create h2: %v", err)
return
}
// Zero out the listen addresses for the host, so it can only communicate
// via p2p-circuit for our example
h3, err := libp2p.New(context.Background(), libp2p.ListenAddrs(), libp2p.EnableRelay())
if err != nil {
panic(err)
log.Printf("Failed to create h3: %v", err)
return
}
h2info := peer.AddrInfo{
@@ -44,30 +51,33 @@ func main() {
// Connect both h1 and h3 to h2, but not to each other
if err := h1.Connect(context.Background(), h2info); err != nil {
panic(err)
log.Printf("Failed to connect h1 and h2: %v", err)
return
}
if err := h3.Connect(context.Background(), h2info); err != nil {
panic(err)
log.Printf("Failed to connect h3 and h2: %v", err)
return
}
// Now, to test things, let's set up a protocol handler on h3
h3.SetStreamHandler("/cats", func(s network.Stream) {
fmt.Println("Meow! It worked!")
log.Println("Meow! It worked!")
s.Close()
})
_, err = h1.NewStream(context.Background(), h3.ID(), "/cats")
if err == nil {
fmt.Println("Didnt actually expect to get a stream here. What happened?")
log.Println("Didnt actually expect to get a stream here. What happened?")
return
}
fmt.Println("Okay, no connection from h1 to h3: ", err)
fmt.Println("Just as we suspected")
log.Printf("Okay, no connection from h1 to h3: %v", err)
log.Println("Just as we suspected")
// Creates a relay address
relayaddr, err := ma.NewMultiaddr("/p2p-circuit/ipfs/" + h3.ID().Pretty())
// Creates a relay address to h3 using h2 as the relay
relayaddr, err := ma.NewMultiaddr("/p2p/" + h2.ID().Pretty() + "/p2p-circuit/ipfs/" + h3.ID().Pretty())
if err != nil {
panic(err)
log.Println(err)
return
}
// Since we just tried and failed to dial, the dialer system will, by default
@@ -81,13 +91,14 @@ func main() {
Addrs: []ma.Multiaddr{relayaddr},
}
if err := h1.Connect(context.Background(), h3relayInfo); err != nil {
panic(err)
log.Printf("Failed to connect h1 and h3: %v", err)
return
}
// Woohoo! we're connected!
s, err := h1.NewStream(context.Background(), h3.ID(), "/cats")
if err != nil {
fmt.Println("huh, this should have worked: ", err)
log.Println("huh, this should have worked: ", err)
return
}

View File

@@ -0,0 +1,14 @@
package main
import (
"testing"
"github.com/libp2p/go-libp2p/examples/testutils"
)
func TestMain(t *testing.T) {
var h testutils.LogHarness
h.ExpectPrefix("Okay, no connection from h1 to h3")
h.ExpectPrefix("Meow! It worked!")
h.Run(t, run)
}

View File

@@ -0,0 +1,122 @@
package testutils
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"strings"
"testing"
)
// A LogHarness runs sets of assertions against the log output of a function. Assertions are grouped
// into sequences of messages that are expected to be found in the log output. Calling one of the Expect
// methods on the harness adds an expectation to the default sequence of messages. Additional sequences
// can be created by calling NewSequence.
type LogHarness struct {
buf bytes.Buffer
sequences []*Sequence
}
type Expectation interface {
IsMatch(line string) bool
String() string
}
// Run executes the function f and captures any output written using Go's standard log. Each sequence
// of expected messages is then
func (h *LogHarness) Run(t *testing.T, f func()) {
// Capture raw log output
fl := log.Flags()
log.SetFlags(0)
log.SetOutput(&h.buf)
f()
log.SetFlags(fl)
log.SetOutput(os.Stderr)
for _, seq := range h.sequences {
seq.Assert(t, bufio.NewScanner(bytes.NewReader(h.buf.Bytes())))
}
}
// Expect adds an expectation to the default sequence that the log contains a line equal to s
func (h *LogHarness) Expect(s string) {
if len(h.sequences) == 0 {
h.sequences = append(h.sequences, &Sequence{name: ""})
}
h.sequences[0].Expect(s)
}
// ExpectPrefix adds an to the default sequence expectation that the log contains a line starting with s
func (h *LogHarness) ExpectPrefix(s string) {
if len(h.sequences) == 0 {
h.sequences = append(h.sequences, &Sequence{name: ""})
}
h.sequences[0].ExpectPrefix(s)
}
// NewSequence creates a new sequence of expected log messages
func (h *LogHarness) NewSequence(name string) *Sequence {
seq := &Sequence{name: name}
h.sequences = append(h.sequences, seq)
return seq
}
type prefix string
func (p prefix) IsMatch(line string) bool {
return strings.HasPrefix(line, string(p))
}
func (p prefix) String() string {
return fmt.Sprintf("prefix %q", string(p))
}
type text string
func (t text) IsMatch(line string) bool {
return line == string(t)
}
func (t text) String() string {
return fmt.Sprintf("text %q", string(t))
}
type Sequence struct {
name string
exp []Expectation
}
func (seq *Sequence) Assert(t *testing.T, s *bufio.Scanner) {
var tag string
if seq.name != "" {
tag = fmt.Sprintf("[%s] ", seq.name)
}
// Match raw log lines against expectations
exploop:
for _, e := range seq.exp {
for s.Scan() {
if e.IsMatch(s.Text()) {
t.Logf("%ssaw: %s", tag, s.Text())
continue exploop
}
}
if s.Err() == nil {
t.Errorf("%sdid not see expected %s", tag, e.String())
return
}
}
return
}
// Expect adds an expectation that the log contains a line equal to s
func (seq *Sequence) Expect(s string) {
seq.exp = append(seq.exp, text(s))
}
// ExpectPrefix adds an expectation that the log contains a line starting with s
func (seq *Sequence) ExpectPrefix(s string) {
seq.exp = append(seq.exp, prefix(s))
}

37
examples/testutils/net.go Normal file
View File

@@ -0,0 +1,37 @@
package testutils
import (
"fmt"
"net"
"testing"
)
// FindFreePort attempts to find an unused tcp port
func FindFreePort(t *testing.T, host string, maxAttempts int) (int, error) {
t.Helper()
if host == "" {
host = "localhost"
}
for i := 0; i < maxAttempts; i++ {
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(host, "0"))
if err != nil {
t.Logf("unable to resolve tcp addr: %v", err)
continue
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
l.Close()
t.Logf("unable to listen on addr %q: %v", addr, err)
continue
}
port := l.Addr().(*net.TCPAddr).Port
l.Close()
return port, nil
}
return 0, fmt.Errorf("no free port found")
}