Files
acp/target_test.go
T
Samuel CUI 159d8ea4ea Decouple bounded copy pipeline and preserve ordered completion
Overlap source reads, hashing, ordered linear writes, and asynchronous cleanup with bounded buffers. Preserve conflicting output order, API behavior, and no-space diagnostics. Add an LTFS drive-buffer model and reproducible performance checks.
2026-10-09 00:33:14 +08:00

294 lines
9.9 KiB
Go

package acp
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
"testing"
"time"
)
func TestLinearTargetCapacityEstimateBoundaries(t *testing.T) {
observationErr := errors.New("capacity observation failed")
for _, tt := range []struct {
name string
content string
available int64
queryErr error
wantCalls int
}{
{name: "equal capacity", content: "data", available: 4, wantCalls: 1},
{name: "empty file", queryErr: observationErr},
{name: "observation failure", content: "data", queryErr: observationErr, wantCalls: 1},
} {
t.Run(tt.name, func(t *testing.T) {
// Observe capacity before creating even the target directory, without allocating large fixtures.
root := t.TempDir()
source := writeSourceFile(t, root, "source", []byte(tt.content))
parent := filepath.Join(root, "destination")
target := filepath.Join(parent, "target")
item := newFixtureItem(source, target)
copyer, err := NewStream(context.Background(), newStreamFixture(item).onResults,
SetToDevice(LinearDevice(true)))
if err != nil {
t.Fatal(err)
}
copyer.getDevice = func(string) (string, error) { return root, nil }
calls := 0
copyer.availableSpace = func(device string) (int64, error) {
calls++
if device != root {
t.Errorf("capacity device = %q, want %q", device, root)
}
if _, err := os.Stat(parent); !errors.Is(err, os.ErrNotExist) {
t.Errorf("target directory exists before capacity observation: %v", err)
}
return tt.available, tt.queryErr
}
// Use real source metadata so permitted writes complete through the ordinary target lifecycle.
if err := copyer.Submit(item); err != nil {
t.Error(err)
}
if err := copyer.Close(); err != nil {
t.Fatal(err)
}
if err := copyer.Wait(); err != nil {
t.Fatal(err)
}
// Equal capacity and empty files complete; observation errors retain their original identity.
result, err := item.terminal(t)
if err != nil || len(result.Targets) != 1 || result.Targets[0].Path != target {
t.Fatalf("unexpected result: %+v / %v", result, err)
}
if calls != tt.wantCalls || copyer.linearTargetStopped() {
t.Fatalf("capacity calls = %d, want %d; stopped = %t", calls, tt.wantCalls, copyer.linearTargetStopped())
}
if tt.queryErr != nil && tt.wantCalls > 0 {
if !errors.Is(result.Targets[0].Err, observationErr) || errors.Is(result.Targets[0].Err, ErrTargetNoSpace) {
t.Fatalf("observation error = %v", result.Targets[0].Err)
}
if _, err := os.Stat(parent); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("observation failure created output directory: %v", err)
}
return
}
if result.Targets[0].Err != nil {
t.Fatal(result.Targets[0].Err)
}
if content, err := os.ReadFile(target); err != nil || string(content) != tt.content {
t.Fatalf("target content = %q / %v, want %q", content, err, tt.content)
}
})
}
}
func TestCopyRejectsSourceAliasesBeforeModification(t *testing.T) {
for _, mode := range []ReadMode{ReadBuffered, ReadMapped} {
for _, alias := range []string{"same path", "hardlink", "symlink"} {
t.Run(fmt.Sprintf("%v/%s", mode, alias), func(t *testing.T) {
// One alias is unsafe, but another target must still receive the complete source.
root := t.TempDir()
content := bytes.Repeat([]byte("unaltered source"), 1000)
source := writeSourceFile(t, root, "source", content)
fixed := time.Unix(1700000000, 0)
if err := os.Chtimes(source, fixed, fixed); err != nil {
t.Fatal(err)
}
target := source
if alias != "same path" {
target = filepath.Join(root, "alias")
link := os.Link
if alias == "symlink" {
link = os.Symlink
}
if err := link(source, target); err != nil {
t.Skipf("alias unsupported: %v", err)
}
}
good := filepath.Join(root, "good")
item := newFixtureItem(source, target, good)
if err := runFixture(context.Background(), newStreamFixture(item), []Item{item}, Overwrite(true),
WithHashPolicy(HashRead), SetFromDevice(WithReadMode(mode))); err != nil {
t.Fatal(err)
}
// Content and metadata at the source survive; exactly the independent target succeeds.
result, err := item.terminal(t)
if err != nil || len(result.Targets) != 2 || result.Targets[0].Err == nil || result.Targets[1].Err != nil {
t.Fatalf("result=%+v / %v", result, err)
}
for _, path := range []string{source, good} {
data, err := os.ReadFile(path)
if err != nil || !bytes.Equal(data, content) {
t.Fatalf("content changed at %q: %v", path, err)
}
}
info, err := os.Stat(source)
if err != nil || !info.ModTime().Equal(fixed) {
t.Fatalf("source metadata changed: %v / %v", info, err)
}
})
}
}
}
func TestMultipleTargetsDoNotQuerySpaceOrWait(t *testing.T) {
// Ordinary targets use actual allocation errors, with no preflight estimate or reservation.
root := t.TempDir()
source := writeSourceFile(t, root, "source", bytes.Repeat([]byte("x"), 4096))
first, second := filepath.Join(root, "first"), filepath.Join(root, "second")
item := newFixtureItem(source, first, second)
fixture := newStreamFixture(item)
copyer, err := NewStream(context.Background(), fixture.onResults, WithHashPolicy(HashRead))
if err != nil {
t.Fatal(err)
}
copyer.availableSpace = func(string) (int64, error) { panic("unexpected space estimate for random targets") }
done := make(chan error, 1)
go func() {
if err := copyer.Submit(item); err != nil {
done <- err
return
}
_ = copyer.Close()
done <- copyer.Wait()
}()
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(10 * time.Second):
t.Fatal("target preparation waited for the same item's writers")
}
// Both independent real allocations succeeded.
result, err := item.terminal(t)
if err != nil {
t.Fatal(err)
}
for _, target := range result.Targets {
if target.Err != nil {
t.Fatal(target.Err)
}
}
}
func TestPreallocationFailureCleansOnlyOwnedOutput(t *testing.T) {
// Inject a real-classified allocation error while preserving independent targets on the same mount.
root := t.TempDir()
source := writeSourceFile(t, root, "source", []byte("new content"))
existing := writeSourceFile(t, root, "existing", []byte("old content"))
owned, good := filepath.Join(root, "owned"), filepath.Join(root, "good")
previous := allocateTarget
calls := 0
allocateTarget = func(file *os.File, size int64) error {
calls++
if calls == 3 {
return previous(file, size)
}
return syscall.EIO
}
t.Cleanup(func() { allocateTarget = previous })
item := newFixtureItem(source, existing, owned, good)
if err := runFixture(context.Background(), newStreamFixture(item), []Item{item}, Overwrite(true)); err != nil {
t.Fatal(err)
}
result, err := item.terminal(t)
if err != nil {
t.Fatal(err)
}
for _, target := range result.Targets[:2] {
if !errors.Is(target.Err, syscall.EIO) || !errors.Is(target.Err, ErrTargetIO) {
t.Fatalf("wrong identity: %v", target.Err)
}
}
if result.Targets[2].Err != nil {
t.Fatal(result.Targets[2].Err)
}
if _, err := os.Stat(existing); err != nil {
t.Fatalf("preexisting file removed: %v", err)
}
if _, err := os.Stat(owned); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("exclusive output survived: %v", err)
}
}
func TestSourceDescriptorClosesWhenCacheOperationPanics(t *testing.T) {
for _, stage := range []string{"lookup", "publication"} {
t.Run(stage, func(t *testing.T) {
// Inject a panic after the item has acquired its real source descriptor.
root := t.TempDir()
source := writeSourceFile(t, root, "source", []byte("cache panic fixture"))
oldRead, oldWrite := readManagedXattr, writeManagedPathXattr
sentinel := errors.New("cache operation panic")
var opened *os.File
policy := HashCachedOnly
if stage == "lookup" {
readManagedXattr = func(file *os.File) ([]byte, error) { opened = file; panic(sentinel) }
} else {
policy = HashReadRefresh
readManagedXattr = func(file *os.File) ([]byte, error) { opened = file; return nil, errSignatureXattrUnsupported }
writeManagedPathXattr = func(string, []byte) error { panic(sentinel) }
}
t.Cleanup(func() { readManagedXattr, writeManagedPathXattr = oldRead, oldWrite })
item := newFixtureItem(source)
err := runFixture(context.Background(), newStreamFixture(item), []Item{item}, WithHashPolicy(policy))
if !errors.Is(err, sentinel) {
t.Fatalf("panic identity=%v", err)
}
// A fatal cache failure can abandon a result, but must not abandon the descriptor it owns.
if opened == nil {
t.Fatal("cache operation was not reached")
}
if _, err := opened.Stat(); !errors.Is(err, os.ErrClosed) {
t.Fatalf("source descriptor leaked: %v", err)
}
})
}
}
type panicSourceReader struct{ *os.File }
func (r *panicSourceReader) Read([]byte) (int, error) { panic("source reader panic") }
func TestSourceReadPanicReleasesBuffersAndTargets(t *testing.T) {
// Instrument pooled references and the real source descriptor before injecting a read panic.
trackChunkPool(t)
root := t.TempDir()
source := writeSourceFile(t, root, "source", bytes.Repeat([]byte("x"), 4096))
target := filepath.Join(root, "target")
old := openSourceContent
var opened *os.File
openSourceContent = func(path string, mode ReadMode, info os.FileInfo) (itemSource, error) {
src, err := old(path, mode, info)
if err == nil {
opened = src.file
src.reader = &panicSourceReader{src.file}
}
return src, err
}
t.Cleanup(func() { openSourceContent = old })
item := newFixtureItem(source, target)
err := runFixture(context.Background(), newStreamFixture(item), []Item{item}, WithHashPolicy(HashRead))
if err == nil {
t.Fatal("read panic succeeded")
}
if opened == nil {
t.Fatal("source did not open")
}
if _, err := opened.Stat(); !errors.Is(err, os.ErrClosed) {
t.Fatalf("source leaked: %v", err)
}
if _, err := os.Stat(target); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("failed output survived: %v", err)
}
}