324 lines
7.6 KiB
Go
324 lines
7.6 KiB
Go
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)
|
||
}
|
||
}
|