require.NoError(t, log.Close())
}()
- require.Equal(t, written, log.messages)
+ require.Equal(t, len(written), log.Len())
+ for _, message := range written {
+ read, err := log.Message(message.ID)
+ require.NoError(t, err)
+ require.Equal(t, message, read)
+ }
message, err := log.Append("user", "after the reopen")
require.NoError(t, err)
require.Equal(t, 6, message.ID)
}
+func TestLogBuildsAMissingIndexAgain(t *testing.T) {
+ root := t.TempDir()
+ clock := newTestClock()
+ log, err := openLog(t.Context(), root, clock.Now)
+ require.NoError(t, err)
+
+ for _, text := range []string{"a", "b", "c"} {
+ _, err = log.Append("user", text)
+ require.NoError(t, err)
+
+ clock.now = clock.now.Add(24 * time.Hour)
+ }
+
+ require.NoError(t, log.Close())
+
+ // A missing index, and an index that a crash cut in a record.
+ for _, cut := range []func(string) error{os.Remove, logTruncateToOneAndAHalf} {
+ require.NoError(t, cut(filepath.Join(root, "index", "main.idx")))
+
+ log, err = openLog(t.Context(), root, clock.Now)
+ require.NoError(t, err)
+ require.Equal(t, 3, log.Len())
+
+ message, err := log.Message(2)
+ require.NoError(t, err)
+ require.Equal(t, "c", message.Text)
+ require.NoError(t, log.Close())
+ }
+}
+
func TestLogWritesOneFilePerDay(t *testing.T) {
root := t.TempDir()
clock := newTestClock()
first, err := os.ReadFile(filepath.Join(root, "main", "2026-01-01.jsonl"))
require.NoError(t, err)
- require.Equal(t, `{"i":0,"kind":"user","text":"first day","size":9,"date":"2026-01-01T12:00:00Z"}`+"\n", string(first))
+ require.JSONEq(t, `{"i":0,"kind":"user","text":"first day","size":9,"date":"2026-01-01T12:00:00Z"}`+"\n", string(first))
second, err := os.ReadFile(filepath.Join(root, "main", "2026-01-02.jsonl"))
require.NoError(t, err)
- require.Equal(t, `{"i":1,"kind":"user","text":"second day","size":10,"date":"2026-01-02T12:00:00Z"}`+"\n", string(second))
+ require.JSONEq(t, `{"i":1,"kind":"user","text":"second day","size":10,"date":"2026-01-02T12:00:00Z"}`+"\n", string(second))
}
func TestLogAllowsOneWriter(t *testing.T) {
require.ErrorContains(t, err, "expected message 0, found 1")
}
-// testClock is a clock that tests set by hand.
-type testClock struct {
- now time.Time
+func TestLogCutsAnIncompleteLastLine(t *testing.T) {
+ root := t.TempDir()
+ clock := newTestClock()
+ log, err := openLog(t.Context(), root, clock.Now)
+ require.NoError(t, err)
+
+ _, err = log.Append("user", "whole")
+ require.NoError(t, err)
+ require.NoError(t, log.Close())
+
+ path := filepath.Join(root, "main", "2026-01-01.jsonl")
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)
+ require.NoError(t, err)
+
+ _, err = file.WriteString(`{"i":1,"kind":"user","te`)
+ require.NoError(t, err)
+ require.NoError(t, file.Close())
+
+ log, err = openLog(t.Context(), root, clock.Now)
+ require.NoError(t, err)
+ require.Equal(t, 1, log.Len())
+
+ message, err := log.Append("user", "after the cut")
+ require.NoError(t, err)
+ require.Equal(t, 1, message.ID)
+ require.NoError(t, log.Close())
+
+ log, err = openLog(t.Context(), root, clock.Now)
+ require.NoError(t, err)
+
+ defer func() {
+ require.NoError(t, log.Close())
+ }()
+
+ message, err = log.Message(1)
+ require.NoError(t, err)
+ require.Equal(t, "after the cut", message.Text)
}
-func newTestClock() *testClock {
- return &testClock{
- now: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
- }
+func TestLogRejectsAnIncompleteLineBeforeTheLastDay(t *testing.T) {
+ root := t.TempDir()
+ require.NoError(t, os.MkdirAll(filepath.Join(root, "main"), 0o700))
+
+ first := `{"i":0,"kind":"user","text":"x","size":1,"date":"2026-01-01T12:00:00Z"}` + "\n" + `{"i":1,"kin`
+ second := `{"i":1,"kind":"user","text":"y","size":1,"date":"2026-01-02T12:00:00Z"}` + "\n"
+ require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-01.jsonl"), []byte(first), 0o600))
+ require.NoError(t, os.WriteFile(filepath.Join(root, "main", "2026-01-02.jsonl"), []byte(second), 0o600))
+
+ _, err := openLog(t.Context(), root, newTestClock().Now)
+ require.ErrorContains(t, err, "ends in an incomplete line")
}
-func (c *testClock) Now() time.Time {
- return c.now
+// logTruncateToOneAndAHalf cuts the index at path in its second record.
+func logTruncateToOneAndAHalf(path string) error {
+ return os.Truncate(path, positionBytes+positionBytes/2)
}