From 84b17a369e9830e6d3c5eddf95c70feda24ab77f Mon Sep 17 00:00:00 2001 From: Aiden Fine Date: Thu, 6 Aug 2026 23:05:20 -0400 Subject: [PATCH] add some more tests --- src/aof/aof_test.go | 266 ++++++++++++++++++++++++++++++++++++++ src/resp/resp_test.go | 217 +++++++++++++++++++++++++++++++ src/writer/writer_test.go | 104 +++++++++++++++ 3 files changed, 587 insertions(+) create mode 100644 src/aof/aof_test.go create mode 100644 src/resp/resp_test.go create mode 100644 src/writer/writer_test.go diff --git a/src/aof/aof_test.go b/src/aof/aof_test.go new file mode 100644 index 0000000..0be109f --- /dev/null +++ b/src/aof/aof_test.go @@ -0,0 +1,266 @@ +package aof + +import ( + "os" + "path/filepath" + "testing" + "zeno/src/resp" +) + +func tempAofPath(t *testing.T) string { + t.Helper() + return filepath.Join(t.TempDir(), "test.aof") +} + +func TestNewAof(t *testing.T) { + tc := map[string]struct { + path string + expectErr bool + }{ + "creates file at valid path": { + path: tempAofPath(t), + expectErr: false, + }, + "fails on invalid path": { + path: "/nonexistent/dir/test.aof", + expectErr: true, + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + aof, err := NewAof(test.path) + if test.expectErr { + if err == nil { + t.Fatal("expected error but got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer aof.Close() + + if aof.File == nil { + t.Fatal("expected File to be set") + } + if aof.Rd == nil { + t.Fatal("expected Rd to be set") + } + + if _, err := os.Stat(test.path); os.IsNotExist(err) { + t.Fatal("expected aof file to be created on disk") + } + }) + } +} + +func TestClose(t *testing.T) { + path := tempAofPath(t) + aof, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error creating aof: %v", err) + } + + if err := aof.Close(); err != nil { + t.Fatalf("unexpected error closing aof: %v", err) + } + + // writing after close should fail + err = aof.Write(resp.Value{Type: "bulk", Bulk: "test"}) + if err == nil { + t.Fatal("expected error writing to closed aof") + } +} + +func TestWrite(t *testing.T) { + tc := map[string]struct { + value resp.Value + expectedOutput string + }{ + "write bulk value": { + value: resp.Value{Type: "bulk", Bulk: "hello"}, + expectedOutput: "$5\r\nhello\r\n", + }, + "write array with bulk elements": { + value: resp.Value{ + Type: "array", + Array: []resp.Value{ + {Type: "bulk", Bulk: "SET"}, + {Type: "bulk", Bulk: "key"}, + {Type: "bulk", Bulk: "value"}, + }, + }, + expectedOutput: "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", + }, + "write string value": { + value: resp.Value{Type: "string", Str: "OK"}, + expectedOutput: "+OK\r\n", + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + path := tempAofPath(t) + aof, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer aof.Close() + + if err := aof.Write(test.value); err != nil { + t.Fatalf("unexpected error writing: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("unexpected error reading file: %v", err) + } + if string(data) != test.expectedOutput { + t.Fatalf("unexpected file content:\ngot: %q\nwant: %q", string(data), test.expectedOutput) + } + }) + } +} + +func TestWriteMultiple(t *testing.T) { + path := tempAofPath(t) + aof, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer aof.Close() + + values := []resp.Value{ + {Type: "bulk", Bulk: "first"}, + {Type: "bulk", Bulk: "second"}, + } + for _, v := range values { + if err := aof.Write(v); err != nil { + t.Fatalf("unexpected error writing: %v", err) + } + } + + expected := "$5\r\nfirst\r\n$6\r\nsecond\r\n" + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("unexpected error reading file: %v", err) + } + if string(data) != expected { + t.Fatalf("unexpected file content:\ngot: %q\nwant: %q", string(data), expected) + } +} + +func TestRead(t *testing.T) { + path := tempAofPath(t) + + // write a known RESP array to the file directly + content := "*2\r\n$3\r\nSET\r\n$3\r\nfoo\r\n" + if err := os.WriteFile(path, []byte(content), 0666); err != nil { + t.Fatalf("unexpected error writing test file: %v", err) + } + + aof, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer aof.Close() + + var got []resp.Value + err = aof.Read(func(value resp.Value) { + got = append(got, value) + }) + if err != nil { + t.Fatalf("unexpected error reading: %v", err) + } + + if len(got) == 0 { + t.Fatal("expected at least one value from callback") + } + + if got[0].Type != "array" { + t.Fatalf("unexpected type: got %s expected array", got[0].Type) + } + if len(got[0].Array) != 2 { + t.Fatalf("unexpected array length: got %d expected 2", len(got[0].Array)) + } + if got[0].Array[0].Bulk != "SET" { + t.Fatalf("unexpected first element: got %s expected SET", got[0].Array[0].Bulk) + } + if got[0].Array[1].Bulk != "foo" { + t.Fatalf("unexpected second element: got %s expected foo", got[0].Array[1].Bulk) + } +} + +func TestReadEmptyFile(t *testing.T) { + path := tempAofPath(t) + aof, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer aof.Close() + + called := false + err = aof.Read(func(value resp.Value) { + called = true + }) + + if called { + t.Fatal("callback should not be called for empty file") + } +} + +func TestWriteThenRead(t *testing.T) { + path := tempAofPath(t) + aof, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + written := resp.Value{ + Type: "array", + Array: []resp.Value{ + {Type: "bulk", Bulk: "SET"}, + {Type: "bulk", Bulk: "mykey"}, + {Type: "bulk", Bulk: "myvalue"}, + }, + } + if err := aof.Write(written); err != nil { + t.Fatalf("unexpected error writing: %v", err) + } + aof.Close() + + // reopen and read + aof2, err := NewAof(path) + if err != nil { + t.Fatalf("unexpected error reopening: %v", err) + } + defer aof2.Close() + + var got []resp.Value + err = aof2.Read(func(value resp.Value) { + got = append(got, value) + }) + if err != nil { + t.Fatalf("unexpected error reading: %v", err) + } + + if len(got) == 0 { + t.Fatal("expected at least one value from read") + } + if got[0].Type != "array" { + t.Fatalf("unexpected type: got %s expected array", got[0].Type) + } + if len(got[0].Array) != 3 { + t.Fatalf("unexpected array length: got %d expected 3", len(got[0].Array)) + } + if got[0].Array[0].Bulk != "SET" { + t.Fatalf("unexpected command: got %s expected SET", got[0].Array[0].Bulk) + } + if got[0].Array[1].Bulk != "mykey" { + t.Fatalf("unexpected key: got %s expected mykey", got[0].Array[1].Bulk) + } + if got[0].Array[2].Bulk != "myvalue" { + t.Fatalf("unexpected value: got %s expected myvalue", got[0].Array[2].Bulk) + } +} diff --git a/src/resp/resp_test.go b/src/resp/resp_test.go new file mode 100644 index 0000000..b266bec --- /dev/null +++ b/src/resp/resp_test.go @@ -0,0 +1,217 @@ +package resp + +import ( + "strings" + "testing" +) + +func TestReadBulk(t *testing.T) { + tc := map[string]struct { + input string + expectedType string + expectedBulk string + }{ + "simple bulk string": { + input: "$5\r\nhello\r\n", + expectedType: "bulk", + expectedBulk: "hello", + }, + "empty bulk string": { + input: "$0\r\n\r\n", + expectedType: "bulk", + expectedBulk: "", + }, + "bulk with spaces": { + input: "$11\r\nhello world\r\n", + expectedType: "bulk", + expectedBulk: "hello world", + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + r := NewResp(strings.NewReader(test.input)) + got, err := r.Read() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Type != test.expectedType { + t.Fatalf("unexpected type: got %s expected %s", got.Type, test.expectedType) + } + if got.Bulk != test.expectedBulk { + t.Fatalf("unexpected bulk: got %q expected %q", got.Bulk, test.expectedBulk) + } + }) + } +} + +func TestReadArray(t *testing.T) { + tc := map[string]struct { + input string + expectedLen int + expectedBulks []string + }{ + "two element array": { + input: "*2\r\n$3\r\nSET\r\n$3\r\nfoo\r\n", + expectedLen: 2, + expectedBulks: []string{"SET", "foo"}, + }, + "three element array": { + input: "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", + expectedLen: 3, + expectedBulks: []string{"SET", "key", "value"}, + }, + "single element array": { + input: "*1\r\n$4\r\nPING\r\n", + expectedLen: 1, + expectedBulks: []string{"PING"}, + }, + "empty array": { + input: "*0\r\n", + expectedLen: 0, + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + r := NewResp(strings.NewReader(test.input)) + got, err := r.Read() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Type != "array" { + t.Fatalf("unexpected type: got %s expected array", got.Type) + } + if len(got.Array) != test.expectedLen { + t.Fatalf("unexpected array length: got %d expected %d", len(got.Array), test.expectedLen) + } + for i, expected := range test.expectedBulks { + if got.Array[i].Bulk != expected { + t.Fatalf("unexpected element %d: got %q expected %q", i, got.Array[i].Bulk, expected) + } + } + }) + } +} + +func TestMarshalBulk(t *testing.T) { + tc := map[string]struct { + value Value + expected string + }{ + "simple bulk": { + value: Value{Type: "bulk", Bulk: "hello"}, + expected: "$5\r\nhello\r\n", + }, + "empty bulk": { + value: Value{Type: "bulk", Bulk: ""}, + expected: "$0\r\n\r\n", + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + got := string(test.value.Marshal()) + if got != test.expected { + t.Fatalf("unexpected marshal:\ngot: %q\nwant: %q", got, test.expected) + } + }) + } +} + +func TestMarshalString(t *testing.T) { + v := Value{Type: "string", Str: "OK"} + got := string(v.Marshal()) + expected := "+OK\r\n" + if got != expected { + t.Fatalf("unexpected marshal:\ngot: %q\nwant: %q", got, expected) + } +} + +func TestMarshalError(t *testing.T) { + v := Value{Type: "error", Str: "ERR unknown command"} + got := string(v.Marshal()) + expected := "-ERR unknown command\r\n" + if got != expected { + t.Fatalf("unexpected marshal:\ngot: %q\nwant: %q", got, expected) + } +} + +func TestMarshalNull(t *testing.T) { + v := Value{Type: "null"} + got := string(v.Marshal()) + expected := "$-1\r\n" + if got != expected { + t.Fatalf("unexpected marshal:\ngot: %q\nwant: %q", got, expected) + } +} + +func TestMarshalArray(t *testing.T) { + tc := map[string]struct { + value Value + expected string + }{ + "array of bulk strings": { + value: Value{ + Type: "array", + Array: []Value{ + {Type: "bulk", Bulk: "SET"}, + {Type: "bulk", Bulk: "key"}, + {Type: "bulk", Bulk: "value"}, + }, + }, + expected: "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", + }, + "empty array": { + value: Value{Type: "array", Array: []Value{}}, + expected: "*0\r\n", + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + got := string(test.value.Marshal()) + if got != test.expected { + t.Fatalf("unexpected marshal:\ngot: %q\nwant: %q", got, test.expected) + } + }) + } +} + +func TestMarshalUnknownType(t *testing.T) { + v := Value{Type: "unknown"} + got := v.Marshal() + if len(got) != 0 { + t.Fatalf("expected empty bytes for unknown type, got %q", string(got)) + } +} + +func TestRoundTrip(t *testing.T) { + original := Value{ + Type: "array", + Array: []Value{ + {Type: "bulk", Bulk: "HSET"}, + {Type: "bulk", Bulk: "users"}, + {Type: "bulk", Bulk: "user1"}, + }, + } + + marshaled := original.Marshal() + r := NewResp(strings.NewReader(string(marshaled))) + got, err := r.Read() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got.Type != original.Type { + t.Fatalf("type mismatch: got %s expected %s", got.Type, original.Type) + } + if len(got.Array) != len(original.Array) { + t.Fatalf("array length mismatch: got %d expected %d", len(got.Array), len(original.Array)) + } + for i := range original.Array { + if got.Array[i].Bulk != original.Array[i].Bulk { + t.Fatalf("element %d mismatch: got %q expected %q", i, got.Array[i].Bulk, original.Array[i].Bulk) + } + } +} diff --git a/src/writer/writer_test.go b/src/writer/writer_test.go new file mode 100644 index 0000000..2119368 --- /dev/null +++ b/src/writer/writer_test.go @@ -0,0 +1,104 @@ +package writer + +import ( + "bytes" + "errors" + "testing" + "zeno/src/resp" +) + +type errWriter struct{} + +func (e *errWriter) Write(p []byte) (int, error) { + return 0, errors.New("write failed") +} + +func TestNewWriter(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(&buf) + if w == nil { + t.Fatal("expected non-nil Writer") + } +} + +func TestWrite(t *testing.T) { + tc := map[string]struct { + value resp.Value + expected string + }{ + "bulk string": { + value: resp.Value{Type: "bulk", Bulk: "hello"}, + expected: "$5\r\nhello\r\n", + }, + "simple string": { + value: resp.Value{Type: "string", Str: "OK"}, + expected: "+OK\r\n", + }, + "error": { + value: resp.Value{Type: "error", Str: "ERR bad command"}, + expected: "-ERR bad command\r\n", + }, + "null": { + value: resp.Value{Type: "null"}, + expected: "$-1\r\n", + }, + "array of bulk strings": { + value: resp.Value{ + Type: "array", + Array: []resp.Value{ + {Type: "bulk", Bulk: "SET"}, + {Type: "bulk", Bulk: "key"}, + {Type: "bulk", Bulk: "val"}, + }, + }, + expected: "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\nval\r\n", + }, + "empty array": { + value: resp.Value{Type: "array", Array: []resp.Value{}}, + expected: "*0\r\n", + }, + } + + for tcName, test := range tc { + t.Run(tcName, func(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(&buf) + + err := w.Write(test.value) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if buf.String() != test.expected { + t.Fatalf("unexpected output:\ngot: %q\nwant: %q", buf.String(), test.expected) + } + }) + } +} + +func TestWriteError(t *testing.T) { + w := NewWriter(&errWriter{}) + err := w.Write(resp.Value{Type: "string", Str: "OK"}) + if err == nil { + t.Fatal("expected error but got nil") + } +} + +func TestWriteMultiple(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(&buf) + + values := []resp.Value{ + {Type: "string", Str: "OK"}, + {Type: "bulk", Bulk: "data"}, + } + for _, v := range values { + if err := w.Write(v); err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + + expected := "+OK\r\n$4\r\ndata\r\n" + if buf.String() != expected { + t.Fatalf("unexpected output:\ngot: %q\nwant: %q", buf.String(), expected) + } +}