Add some higher level tests and benchmarks
This commit is contained in:
		
							parent
							
								
									5ea0bbfc87
								
							
						
					
					
						commit
						0c0a814a4e
					
				|  | @ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue