Add some higher level tests and benchmarks

This commit is contained in:
Laurence Withers 2024-02-18 14:00:29 +00:00
parent 5ea0bbfc87
commit 0c0a814a4e
1 changed files with 166 additions and 0 deletions

166
conn_test.go Normal file
View File

@ -0,0 +1,166 @@
package journal
import (
"bytes"
"encoding/binary"
"encoding/hex"
"io"
"net"
"path/filepath"
"strconv"
"testing"
)
type testingCommon interface {
TempDir() string
Fatalf(string, ...any)
}
// testConnector spawns a Conn with a local Unix datagram socket that checks
// incoming datagrams for well-formedness but otherwise discards them.
func testConnector(t testingCommon) *Conn {
sockPath := filepath.Join(t.TempDir(), "test-socket")
sock, err := net.ListenUnixgram("unixgram", &net.UnixAddr{
Name: sockPath,
Net: "unixgram",
})
if err != nil {
t.Fatalf("testConnector: ListenUnix: %v", err)
}
go func() {
buf := make([]byte, 1<<16 /*enough for our tests */)
for {
n, err := sock.Read(buf)
switch err {
case nil:
case io.EOF:
return
default:
t.Fatalf("testConnector: Read: %v", err)
}
ok, pos, desc := checkWellFormedProto(buf[:n])
if !ok {
t.Fatalf("received malformed data at pos 0x%x: %s\n%s", pos, desc, hex.Dump(buf[:n]))
}
}
}()
conn, err := Connect(sockPath)
if err != nil {
t.Fatalf("testConnector: DialUnix: %v", err)
}
conn.ErrHandler = func(err error) {
t.Fatalf("testConnector: connection error: %v", err)
}
return conn
}
func checkWellFormedProto(buf []byte) (ok bool, pos int, desc string) {
for pos < len(buf) {
// grab attribute name up to '=' or '\n'
off := bytes.IndexAny(buf[pos:], "=\n")
if off == -1 {
return false, pos, "unterminated key"
}
key := string(buf[pos : pos+off])
if err := AttrKeyValid(key); err != nil {
return false, pos, err.Error()
}
pos += off
// for KEY=VALUE, the value is terminated by a newline
if buf[pos] == '=' {
pos++
off = bytes.IndexByte(buf[pos:], '\n')
if off == -1 {
return false, pos, "unterminated value"
}
pos += off // consume value
pos++ // consume trailing newline
continue
}
// otherwise, expect an 8-bit little-endian length
pos++ // consume newline after key
if pos+8 > len(buf) {
return false, pos, "value length too short"
}
vlen := binary.LittleEndian.Uint64(buf[pos:])
pos += 8
if vlen > uint64(len(buf)) /* protect against overflow */ ||
uint64(pos)+vlen+1 > uint64(len(buf)) {
return false, pos, "value length too long"
}
pos += int(vlen)
if buf[pos] != '\n' {
return false, pos, "value not terminated by newline"
}
pos++
}
return true, pos, ""
}
// TestConcurrentEntries is best run with the race detector, and tries to pick
// up any faults that might occur when concurrent goroutines write into the same
// Conn.
func TestConcurrentEntries(t *testing.T) {
c := testConnector(t)
// attributes which will be common to all EntryErr calls
attr := make([]Attr, 0, 10 /* enough capacity to avoid realloc on append; might trigger data races */)
attr = append(attr, Attr{
Key: AttrKey{
key: "HELLO",
},
Value: []byte("world"),
})
// spawn goroutines
start := make(chan struct{})
for i := 0; i < 16; i++ {
go func() {
<-start
for j := 0; j < 100; j++ {
err := c.EntryErr(PriDebug, "message "+strconv.Itoa(i)+"."+strconv.Itoa(j), attr)
if err != nil {
t.Fatalf("message %d.%d error: %v", i, j, err)
}
}
}()
}
// try to get all the goroutines to start at roughly the same time
close(start)
}
// BenchmarkEntry is a benchmark for the common Entry function.
func BenchmarkEntry(b *testing.B) {
c := testConnector(b)
// add some common attributes
c.Common = make([]Attr, 0, 10)
c.Common = append(c.Common, Attr{
Key: AttrKey{
key: "COMMON_ATTR",
},
Value: []byte("abc123\n"),
})
// attributes which will be common to all EntryErr calls
attr := make([]Attr, 0, 10)
attr = append(attr, Attr{
Key: AttrKey{
key: "HELLO",
},
Value: []byte("world"),
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := c.EntryErr(PriDebug, "hello world!", attr)
if err != nil {
b.Fatalf("message %d: error %v", i, err)
}
}
}