diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-08 09:11:16 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-08 09:11:16 -0700 |
| commit | 3e5ae31fa6d8c33f61d070ca20c34c0dcba7666b (patch) | |
| tree | 248ec903bafebaaee3ddb4030ca70def39ac66ad /hotline/mem_file_store_test.go | |
| parent | a42a5961861f638fef3b49685fc6df2eea38fa0d (diff) | |
Add in-memory MemFileStore backend
Provide a reference non-OS FileStore that models storage as a flat
keyspace of cleaned paths, deriving directory structure from key prefixes
the way an object store does. It proves the FileStore abstraction is
genuinely backend-agnostic and serves as a hermetic test fixture needing
no temp directory. Symlink/ReadLink return errors.ErrUnsupported, matching
how an object-store backend would behave.
Tests drive a full upload -> list -> download round-trip and directory
listing entirely in memory.
Diffstat (limited to 'hotline/mem_file_store_test.go')
| -rw-r--r-- | hotline/mem_file_store_test.go | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/hotline/mem_file_store_test.go b/hotline/mem_file_store_test.go new file mode 100644 index 0000000..71fa339 --- /dev/null +++ b/hotline/mem_file_store_test.go @@ -0,0 +1,166 @@ +package hotline + +import ( + "bytes" + "errors" + "io" + "io/fs" + "log/slog" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/encoding" +) + +func TestMemFileStore_WriteReadAppend(t *testing.T) { + m := NewMemFileStore() + + // Create + write. + w, err := m.Create("/files/foo.txt") + require.NoError(t, err) + _, err = w.Write([]byte("hello")) + require.NoError(t, err) + require.NoError(t, w.Close()) + + got, err := m.ReadFile("/files/foo.txt") + require.NoError(t, err) + assert.Equal(t, []byte("hello"), got) + + // Append via OpenFile with O_APPEND (mirrors the .incomplete upload path). + aw, err := m.OpenFile("/files/foo.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + require.NoError(t, err) + _, err = aw.Write([]byte(" world")) + require.NoError(t, err) + require.NoError(t, aw.Close()) + + got, err = m.ReadFile("/files/foo.txt") + require.NoError(t, err) + assert.Equal(t, []byte("hello world"), got) + + // Open returns a reader over the committed bytes. + r, err := m.Open("/files/foo.txt") + require.NoError(t, err) + b, err := io.ReadAll(r) + require.NoError(t, err) + require.NoError(t, r.Close()) + assert.Equal(t, []byte("hello world"), b) +} + +func TestMemFileStore_StatAndParents(t *testing.T) { + m := NewMemFileStore() + + require.NoError(t, m.WriteFile("/files/sub/bar.txt", []byte("data"), 0644)) + + // Nested write auto-registers parent directories. + fi, err := m.Stat("/files/sub") + require.NoError(t, err) + assert.True(t, fi.IsDir()) + + fi, err = m.Stat("/files/sub/bar.txt") + require.NoError(t, err) + assert.False(t, fi.IsDir()) + assert.Equal(t, int64(4), fi.Size()) + + _, err = m.Stat("/files/missing") + assert.True(t, errors.Is(err, fs.ErrNotExist)) +} + +func TestMemFileStore_ReadDir(t *testing.T) { + m := NewMemFileStore() + require.NoError(t, m.WriteFile("/root/a.txt", []byte("a"), 0644)) + require.NoError(t, m.WriteFile("/root/b.txt", []byte("bb"), 0644)) + require.NoError(t, m.Mkdir("/root/sub", 0755)) + require.NoError(t, m.WriteFile("/root/sub/deep.txt", []byte("x"), 0644)) + + entries, err := m.ReadDir("/root") + require.NoError(t, err) + + var names []string + for _, e := range entries { + names = append(names, e.Name()) + } + assert.Equal(t, []string{"a.txt", "b.txt", "sub"}, names) +} + +func TestMemFileStore_RenameRemove(t *testing.T) { + m := NewMemFileStore() + require.NoError(t, m.WriteFile("/f/a.incomplete", []byte("payload"), 0644)) + + // Rename mirrors the incomplete->final upload commit. + require.NoError(t, m.Rename("/f/a.incomplete", "/f/a")) + _, err := m.Stat("/f/a.incomplete") + assert.True(t, errors.Is(err, fs.ErrNotExist)) + got, err := m.ReadFile("/f/a") + require.NoError(t, err) + assert.Equal(t, []byte("payload"), got) + + require.NoError(t, m.WriteFile("/f/sub/x", []byte("x"), 0644)) + require.NoError(t, m.RemoveAll("/f/sub")) + _, err = m.Stat("/f/sub/x") + assert.True(t, errors.Is(err, fs.ErrNotExist)) +} + +func TestMemFileStore_WalkCounts(t *testing.T) { + m := NewMemFileStore() + require.NoError(t, m.Mkdir("/w", 0755)) + require.NoError(t, m.WriteFile("/w/a.txt", []byte("1234"), 0644)) + require.NoError(t, m.WriteFile("/w/sub/b.txt", []byte("567"), 0644)) + + // CalcTotalSize and CalcItemCount are pure Walk consumers — exercising them proves Walk + // works against a non-OS backend. + size, err := CalcTotalSize(m, "/w") + require.NoError(t, err) + assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x07}, size) // 4 + 3 bytes + + count, err := CalcItemCount(m, "/w") + require.NoError(t, err) + // Walk visits /w, /w/a.txt, /w/sub, /w/sub/b.txt = 4 non-hidden entries, minus the root. + assert.Equal(t, []byte{0x00, 0x03}, count) +} + +func TestMemFileStore_SymlinkUnsupported(t *testing.T) { + m := NewMemFileStore() + err := m.Symlink("/a", "/b") + assert.True(t, errors.Is(err, errors.ErrUnsupported)) + + _, err = m.ReadLink("/b") + assert.True(t, errors.Is(err, errors.ErrUnsupported)) +} + +// TestMemFileStore_DownloadRoundTrip proves the abstraction is genuinely swappable: a file +// staged entirely in memory (no disk, no *os.File) is served through the real DownloadHandler. +func TestMemFileStore_DownloadRoundTrip(t *testing.T) { + m := NewMemFileStore() + + fileData := []byte("the quick brown fox") + require.NoError(t, m.WriteFile("/files/story.txt", fileData, 0644)) + + ft := &FileTransfer{bytesSentCounter: &WriteCounter{}} + + var out bytes.Buffer + err := DownloadHandler(&out, "/files/story.txt", ft, m, slog.Default(), true) + require.NoError(t, err) + + // The download stream is the flattened file object header followed by the data fork; the + // raw file bytes must appear in the output. + assert.True(t, bytes.Contains(out.Bytes(), fileData), "download output should contain the file data") + assert.Equal(t, int64(len(fileData)), ft.bytesSentCounter.Total) +} + +// TestMemFileStore_GetFileNameList proves directory listing works against the in-memory backend. +func TestMemFileStore_GetFileNameList(t *testing.T) { + m := NewMemFileStore() + require.NoError(t, m.WriteFile(filepath.Join("/library", "readme.txt"), []byte("12345"), 0644)) + + fields, err := GetFileNameList(m, "/library", nil, encoding.Nop.NewEncoder(), slog.Default()) + require.NoError(t, err) + require.Len(t, fields, 1) + + var fnwi FileNameWithInfo + _, err = fnwi.Write(fields[0].Data) + require.NoError(t, err) + assert.Equal(t, "readme.txt", string(fnwi.Name)) +} |