Add testsink package, and unit tests for Conn / entries
This commit is contained in:
parent
8f364a74df
commit
ecd852eede
3 changed files with 305 additions and 160 deletions
126
testsink/message.go
Normal file
126
testsink/message.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Package testsink provides a partial implementation of the systemd-journald
|
||||
Unix socket. Datagrams received on its socket are decoded and stored for unit
|
||||
tests to examine.
|
||||
*/
|
||||
package testsink
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Message returns the Nth message received. It waits for the message to arrive.
|
||||
func (sink *Sink) Message(N int) (Message, error) {
|
||||
sink.lock.Lock()
|
||||
defer sink.lock.Unlock()
|
||||
|
||||
for len(sink.msgs) <= N {
|
||||
sink.mcond.Wait()
|
||||
if sink.err != nil {
|
||||
return Message{}, sink.err
|
||||
}
|
||||
}
|
||||
return sink.msgs[N], nil
|
||||
}
|
||||
|
||||
// Message is recorded for each datagram received.
|
||||
type Message struct {
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
type DecodedAttr struct {
|
||||
Key, Val string
|
||||
}
|
||||
|
||||
func (m *Message) Decode() (msg string, attr []DecodedAttr, err error) {
|
||||
raw := m.Raw
|
||||
var errs []error
|
||||
|
||||
DecodeLoop:
|
||||
for len(raw) > 0 {
|
||||
n, key := decodeAttrKey(raw)
|
||||
raw = raw[n:]
|
||||
switch {
|
||||
case len(raw) == 0:
|
||||
errs = append(errs, fmt.Errorf("unterminated attribute name %q", key))
|
||||
break DecodeLoop
|
||||
case key == "":
|
||||
errs = append(errs, errors.New("empty attribute name"))
|
||||
}
|
||||
|
||||
var val string
|
||||
switch raw[0] {
|
||||
case '=':
|
||||
n, val = decodeAttrValText(raw[1:])
|
||||
case '\n':
|
||||
var err error
|
||||
n, val, err = decodeAttrValLen(raw[1:])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
raw = raw[1+n:]
|
||||
if len(raw) == 0 {
|
||||
errs = append(errs, fmt.Errorf("unterminated value for attribute %q", key))
|
||||
break DecodeLoop
|
||||
}
|
||||
|
||||
if raw[0] != '\n' {
|
||||
errs = append(errs, errors.New("incorrectly terminated attribute value"))
|
||||
}
|
||||
raw = raw[1:]
|
||||
|
||||
switch key {
|
||||
case "MESSAGE":
|
||||
msg = val
|
||||
default:
|
||||
attr = append(attr, DecodedAttr{Key: key, Val: val})
|
||||
}
|
||||
}
|
||||
|
||||
return msg, attr, errors.Join(errs...)
|
||||
}
|
||||
|
||||
func decodeAttrKey(raw []byte) (n int, key string) {
|
||||
for i := range raw {
|
||||
switch raw[i] {
|
||||
case '\n', '=':
|
||||
return i, string(raw[:i])
|
||||
}
|
||||
}
|
||||
return len(raw), string(raw)
|
||||
}
|
||||
|
||||
func decodeAttrValText(raw []byte) (n int, val string) {
|
||||
term := bytes.IndexByte(raw, '\n')
|
||||
if term == -1 {
|
||||
term = len(raw)
|
||||
}
|
||||
return term, string(raw[:term])
|
||||
}
|
||||
|
||||
func decodeAttrValLen(raw []byte) (n int, val string, err error) {
|
||||
if len(raw) < 8 {
|
||||
return len(raw), "", errors.New("not enough bytes for binary attribute value length")
|
||||
}
|
||||
amt := binary.LittleEndian.Uint64(raw)
|
||||
raw = raw[8:]
|
||||
if uint64(len(raw)) < amt {
|
||||
return 8 + len(raw), string(raw), errors.New("not enough bytes for binary attribute value")
|
||||
}
|
||||
return int(8 + amt), string(raw[:amt]), nil
|
||||
}
|
||||
|
||||
// GetAttr returns the value of the attribute whose key name matches, and a
|
||||
// boolean to indicate if it found a match.
|
||||
func GetAttr(attr []DecodedAttr, key string) (value string, ok bool) {
|
||||
for i := range attr {
|
||||
if attr[i].Key == key {
|
||||
return attr[i].Val, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
82
testsink/testsink.go
Normal file
82
testsink/testsink.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Package testsink provides a partial implementation of the systemd-journald
|
||||
Unix socket. Datagrams received on its socket are decoded and stored for unit
|
||||
tests to examine.
|
||||
*/
|
||||
package testsink
|
||||
|
||||
import (
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Sink provides a Unix socket and captures messages sent to it using the
|
||||
// systemd-journald wire protocol.
|
||||
type Sink struct {
|
||||
sock *net.UnixConn
|
||||
stop chan struct{}
|
||||
|
||||
lock sync.Mutex
|
||||
mcond *sync.Cond
|
||||
msgs []Message
|
||||
err error
|
||||
}
|
||||
|
||||
// New returns a new Sink that is listening on the given path.
|
||||
func New(sockpath string) (*Sink, error) {
|
||||
sock, err := net.ListenUnixgram("unixgram", &net.UnixAddr{
|
||||
Name: sockpath,
|
||||
Net: "unixgram",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sink := &Sink{
|
||||
sock: sock,
|
||||
stop: make(chan struct{}, 1),
|
||||
}
|
||||
sink.mcond = sync.NewCond(&sink.lock)
|
||||
go sink.stopper()
|
||||
go sink.recv()
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
// Stop listening and close the socket.
|
||||
func (sink *Sink) Stop() {
|
||||
// non-blocking write onto channel; we only need to read from it once in
|
||||
// order to stop the receiver, but using this rather than close ensures
|
||||
// Stop() can be called multiple times without negative side effects
|
||||
select {
|
||||
case sink.stop <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (sink *Sink) stopper() {
|
||||
<-sink.stop
|
||||
sink.sock.Close()
|
||||
}
|
||||
|
||||
func (sink *Sink) recv() {
|
||||
buf := make([]byte, 131072)
|
||||
for {
|
||||
n, err := sink.sock.Read(buf)
|
||||
|
||||
sink.lock.Lock()
|
||||
if n > 0 {
|
||||
sink.msgs = append(sink.msgs, Message{
|
||||
Raw: slices.Clone(buf[:n]),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
sink.err = err
|
||||
}
|
||||
sink.lock.Unlock()
|
||||
|
||||
sink.mcond.Broadcast()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue