owpengram-server/internal/app/files/blobfs_test.go
2026-08-04 23:09:11 +03:00

324 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package files
import (
"bytes"
"context"
"os"
"strings"
"sync"
"testing"
"time"
)
func TestLocalFSPutGetRoundTrip(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
data := []byte("hello telesrv media blob 你好")
key, err := fs.Put(ctx, data)
if err != nil {
t.Fatalf("put: %v", err)
}
if key == "" {
t.Fatal("empty object key")
}
// 内容寻址:相同内容应得到相同 key去重
key2, err := fs.Put(ctx, data)
if err != nil {
t.Fatalf("put again: %v", err)
}
if key != key2 {
t.Fatalf("expected dedup key %q == %q", key, key2)
}
got, err := fs.Get(ctx, key)
if err != nil {
t.Fatalf("get: %v", err)
}
if !bytes.Equal(got, data) {
t.Fatalf("roundtrip mismatch: got %q want %q", got, data)
}
}
func TestLocalFSDelete(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
key, err := fs.Put(ctx, []byte("delete me"))
if err != nil {
t.Fatalf("put: %v", err)
}
if _, err := fs.Get(ctx, key); err != nil {
t.Fatalf("get before delete: %v", err)
}
if err := fs.Delete(ctx, key); err != nil {
t.Fatalf("delete: %v", err)
}
if _, err := fs.Get(ctx, key); err == nil {
t.Fatal("expected get after delete to fail")
}
// Deleting an already-absent object must be idempotent, not an error --
// the retention sweep can legitimately retry after a partial failure.
if err := fs.Delete(ctx, key); err != nil {
t.Fatalf("delete already-missing object: %v", err)
}
}
func TestLocalFSPutReaderRoundTrip(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
data := strings.Repeat("streamed-", 1024)
key, size, sum, err := fs.PutReader(ctx, strings.NewReader(data))
if err != nil {
t.Fatalf("put reader: %v", err)
}
if key == "" || size != int64(len(data)) || len(sum) != 32 {
t.Fatalf("stream metadata key=%q size=%d sha=%d", key, size, len(sum))
}
got, err := fs.Get(ctx, key)
if err != nil {
t.Fatalf("get: %v", err)
}
if string(got) != data {
t.Fatalf("roundtrip mismatch")
}
}
func TestLocalFSDeleteExpiredUploadParts(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
oldPart, err := fs.PutUploadPart(ctx, 10, 100, 0, []byte("old"))
if err != nil {
t.Fatalf("put old upload part: %v", err)
}
freshPart, err := fs.PutUploadPart(ctx, 10, 100, 1, []byte("fresh"))
if err != nil {
t.Fatalf("put fresh upload part: %v", err)
}
oldPath, err := fs.uploadPartPath(oldPart.ObjectKey)
if err != nil {
t.Fatalf("old upload part path: %v", err)
}
oldTime := time.Now().Add(-48 * time.Hour)
if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil {
t.Fatalf("age old upload part: %v", err)
}
deleted, err := fs.DeleteExpiredUploadParts(ctx, time.Now().Add(-24*time.Hour), 10)
if err != nil {
t.Fatalf("delete expired upload parts: %v", err)
}
if deleted != 1 {
t.Fatalf("deleted = %d, want 1", deleted)
}
if _, err := fs.GetUploadPart(ctx, oldPart.ObjectKey); err == nil {
t.Fatalf("old upload part still exists")
}
if data, err := fs.GetUploadPart(ctx, freshPart.ObjectKey); err != nil || string(data) != "fresh" {
t.Fatalf("fresh upload part = %q err=%v", data, err)
}
}
func TestLocalFSDistinctContent(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
k1, _ := fs.Put(ctx, []byte("aaa"))
k2, _ := fs.Put(ctx, []byte("bbb"))
if k1 == k2 {
t.Fatal("distinct content must yield distinct keys")
}
}
func TestLocalFSGetRange(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
key, err := fs.Put(ctx, []byte("0123456789"))
if err != nil {
t.Fatalf("put: %v", err)
}
cases := []struct {
name string
offset, limit int64
want string
}{
{"head", 0, 4, "0123"},
{"middle", 3, 4, "3456"},
{"limit-exceeds-remaining", 7, 100, "789"},
{"zero-limit-reads-to-end", 2, 0, "23456789"},
{"offset-at-end", 10, 5, ""},
{"offset-past-end", 20, 5, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
data, total, err := fs.GetRange(ctx, key, tc.offset, tc.limit)
if err != nil {
t.Fatalf("getrange: %v", err)
}
if string(data) != tc.want {
t.Errorf("data = %q, want %q", data, tc.want)
}
if total != 10 {
t.Errorf("total = %d, want 10", total)
}
})
}
}
func TestLocalFSReusesOpenBlobFileWhileActive(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
key, err := fs.Put(ctx, []byte("0123456789"))
if err != nil {
t.Fatalf("put: %v", err)
}
first, err := fs.openBlobFile(key)
if err != nil {
t.Fatalf("open first: %v", err)
}
second, err := fs.openBlobFile(key)
if err != nil {
t.Fatalf("open second: %v", err)
}
firstReleased, secondReleased := false, false
t.Cleanup(func() {
if !firstReleased {
fs.releaseBlobFile(first)
}
if !secondReleased {
fs.releaseBlobFile(second)
}
})
if first != second {
t.Fatal("same active blob should reuse one open file handle")
}
fs.mu.Lock()
open, refs := len(fs.openBlobFiles), first.refs
fs.mu.Unlock()
if open != 1 || refs != 2 {
t.Fatalf("active files=%d refs=%d, want 1/2", open, refs)
}
fs.releaseBlobFile(first)
firstReleased = true
fs.mu.Lock()
open, refs = len(fs.openBlobFiles), second.refs
fs.mu.Unlock()
if open != 1 || refs != 1 {
t.Fatalf("after first release active files=%d refs=%d, want 1/1", open, refs)
}
var buf [4]byte
if n, err := second.file.ReadAt(buf[:], 3); err != nil || n != len(buf) || string(buf[:]) != "3456" {
t.Fatalf("shared file ReadAt n=%d err=%v bytes=%q, want 3456", n, err, buf[:])
}
fs.releaseBlobFile(second)
secondReleased = true
fs.mu.Lock()
open = len(fs.openBlobFiles)
fs.mu.Unlock()
if open != 0 {
t.Fatalf("after final release active files=%d, want 0", open)
}
}
func TestLocalFSReusesOpenBlobFileUnderConcurrentOpen(t *testing.T) {
fs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("new local fs: %v", err)
}
ctx := context.Background()
key, err := fs.Put(ctx, []byte(strings.Repeat("x", 1024)))
if err != nil {
t.Fatalf("put: %v", err)
}
const readers = 32
start := make(chan struct{})
files := make([]*sharedBlobFile, readers)
errs := make([]error, readers)
var wg sync.WaitGroup
for i := range files {
wg.Add(1)
go func(i int) {
defer wg.Done()
<-start
files[i], errs[i] = fs.openBlobFile(key)
}(i)
}
close(start)
wg.Wait()
released := false
t.Cleanup(func() {
if released {
return
}
for _, f := range files {
if f != nil {
fs.releaseBlobFile(f)
}
}
})
for i, err := range errs {
if err != nil {
t.Fatalf("open %d: %v", i, err)
}
}
first := files[0]
if first == nil {
t.Fatal("first open returned nil")
}
for i, f := range files {
if f != first {
t.Fatalf("file %d = %p, want shared %p", i, f, first)
}
}
fs.mu.Lock()
open, refs := len(fs.openBlobFiles), first.refs
fs.mu.Unlock()
if open != 1 || refs != readers {
t.Fatalf("active files=%d refs=%d, want 1/%d", open, refs, readers)
}
wg = sync.WaitGroup{}
for _, f := range files {
wg.Add(1)
go func(f *sharedBlobFile) {
defer wg.Done()
fs.releaseBlobFile(f)
}(f)
}
wg.Wait()
released = true
fs.mu.Lock()
open = len(fs.openBlobFiles)
fs.mu.Unlock()
if open != 0 {
t.Fatalf("after concurrent release active files=%d, want 0", open)
}
}