bingobot/pkg/docbuf/read_write_seek_string_test.go

166 lines
3.4 KiB
Go
Raw Permalink Normal View History

implement DocumentBuffer for persistence of runtime data In order to provide persistence of runtime state across the application the documentbuffer provides a simple cache interface that balances cached internal state events between an in memory cache and an on disk storage. From a feature development perspective the DocumentBuffer provides a simple cache interface: - Push() and Pop() individual items - Remove() and Read() bulk items - Peek() at the most recent item - Flush() items in memory to the disk as well as some control features: - Close(), which calls flush and then returns index of the last byte of useful data in the backing store. - Cached(), which returns the number of items cached in memory. Underneath the hood, documentbuffer balances the cache (memory) and store (disk) by "promoting" the most recent documents in store to cache and by "demoting" the least recent documents in cache to store. Thus, the cache is always ordered by most recent, and so is the store. Documentbuffer takes any implementation of readwriteseeker as an interface for a backing store. Theoretically this means that documentbuffer can leverage more than just a standard os.File. Possible implementations could include transactions over the network or device drivers for long term cold storage devices. In fact, documentbuffer comes with an in memory test implementation of the readwriteseeker interface called ReadWriteSeekString. This emulates the functions of Read(), Write(), and Seek() to operate on an internal string buffer. This facility is only provided for testing and mock up purposes. One note about Close(): Since the documentbuffer has no way of truncating the underlying store an edge case can present itself that necessitates Close() and specifically Close()'s return type. If the documentbuffer has Remove()ed or promote()ed more bytes of data from store than it will subsequently Flush() or demote() to disk one or more bytes of junk data may be left over from the overwriting of the previously Remove()/promote()ed data. In this case, or more specifically in all cases Close() wil return the last usable index of the underlying store. It is up to the caller to then truncate it. Regretably there is no reasonable truncate interface that applies polymorphicly to any underlying data stream. Thus the quirk around Close() remains a design challenge. This commit provides comprehensive unit tests for both DocumentBuffer and ReadWriteSeekString. Not implemented in this commit is the whole design for runtime data persistence. It is intended that internal modules for bingobot that provide functionality directly to the user leverage the event pub/sub system as their sole authoritative source for state management. As long as these modules provide the same output for the same input sequence of events consistently than all parts of the application will recover from error status or crashes by re-ingesting the on disk storage of events provided by the documentbuffer. This way the application will always come back up with minimal data loss and potentially the exact same state as before it went down. Signed-off-by: Ava Affine <ava@sunnypup.io>
2024-11-25 18:07:26 -08:00
package docbuf
import (
"io"
"testing"
)
func SetupTestingBuffer(t *testing.T) ReadWriteSeekString{
str := NewReadWriteSeekString()
n, e := str.Write([]byte("test"))
if n != 4 || e != nil {
t.Fatalf("Failed to write to buffer: %e", e)
}
return str
}
// can it read
func TestRWSSRead(t *testing.T) {
b := make([]byte, 4)
buf := SetupTestingBuffer(t)
c, err := buf.Seek(0, io.SeekStart)
if err != nil || c != 0 {
t.Fatalf("seek failed: %e", err)
}
n, err := buf.Read(b)
if n != 4 || err != nil || string(b) != "test" {
t.Fatalf("read failed: %e", err)
}
m, err := buf.Seek(-3, io.SeekEnd)
if err != nil || m != 1 {
t.Fatalf("seek failed: %e", err)
}
b = make([]byte, 4)
l, err := buf.Read(b)
if l != 3 || err != nil || string(b[:3]) != "est" {
t.Fatalf("read failed: %e", err)
}
k, err := buf.Seek(0, io.SeekStart)
if k != 0 || err != nil {
t.Fatalf("seek failed: %e", err)
}
b = make([]byte, 3)
j, err := buf.Read(b)
if j != 3 || err != nil || string(b) != "tes" {
t.Fatalf("read failed: %e", err)
}
b = make([]byte, 1)
i, err := buf.Read(b)
if i != 1 || err != nil || string(b) != "t" {
t.Fatalf("read failed: %e", err)
}
}
// can it write
func TestRWSSWrite(t *testing.T) {
buf := SetupTestingBuffer(t)
if buf.Contents() != "test" || buf.Cursor() != 4 {
t.Fatalf("write failed: %s", buf.Contents())
}
m, err := buf.Write([]byte("test2"))
if m != 5 ||
err != nil ||
buf.Contents() != "testtest2" ||
buf.Cursor() != 9 {
t.Fatalf("write failed: %e", err)
}
n, err := buf.Seek(2, io.SeekStart)
if n != 2 || err != nil {
t.Fatalf("seek failed: %e", err)
}
o, err := buf.Write([]byte("one"))
if o != 3 ||
err != nil ||
buf.Contents() != "teoneest2" ||
buf.Cursor() != 5 {
t.Fatalf("write failed: %e", err)
}
p, err := buf.Seek(0, io.SeekEnd)
if p != 9 || err != nil {
t.Fatalf("seek (%d) failed: %e", p, err)
}
q, err := buf.Write([]byte("two"))
if q != 3 ||
err != nil ||
buf.Contents() != "teoneest2two" ||
buf.Cursor() != 12 {
t.Fatalf("write failed: %e", err)
}
}
// if it seeks can it read from new position
// if it seeks can it write to new position
func TestRWSSSeek(t *testing.T) {
buf := SetupTestingBuffer(t)
if n, err := buf.Seek(0, io.SeekStart);
n != 0 ||
err != nil {
t.Fatalf("seek failed: %e", err)
}
if n, err := buf.Seek(3, io.SeekStart);
n != 3 ||
err != nil {
t.Fatalf("seek failed: %e", err)
}
if n, err := buf.Seek(-1, io.SeekStart);
n != 3 ||
err == nil ||
err.Error() != "seek index (-1) is negative" {
t.Fatalf("seek should have failed but didnt: %e", err)
}
if n, err := buf.Seek(0, io.SeekCurrent);
n != 3 ||
err != nil {
t.Fatalf("seek failed: %e", err)
}
if n, err := buf.Seek(-2, io.SeekEnd);
n != 2 ||
err != nil {
t.Fatalf("seek failed: %e", err)
}
if n, err := buf.Seek(-1, io.SeekCurrent);
n != 1 ||
err != nil {
t.Fatalf("seek failed: %e", err)
}
if n, err := buf.Seek(-2, io.SeekCurrent);
n != 1 ||
err == nil ||
err.Error() != "seek index (-1) is negative" {
t.Fatalf("seek should have failed but didnt: %e", err)
}
if n, err := buf.Seek(-1, io.SeekEnd);
n != 3 ||
err != nil {
t.Fatalf("seek failed: %e", err)
}
if n, err := buf.Seek(-5, io.SeekEnd);
n != 3 ||
err == nil ||
err.Error() != "seek index (-1) is negative" {
t.Fatalf("seek should have failed but didnt: %e", err)
}
}