mirror of
https://github.com/samuelncui/acp.git
synced 2026-11-08 04:21:16 +00:00
Reshape the public API around one push engine and restore theaf05f05csurface as a compatibility shell over it. - NewStream takes caller-owned items (Source/Targets), reports every accepted item through one results callback, and Wait returns the run's terminal error. Run, BatchSource and the item callbacks are gone. - The engine owns the result path: WithResultBuffer is the depth of the result queue, WithResultBatch is how many results one delivery carries, and WithResultFlushInterval is how long a successful result may wait. A result that carries any error is delivered immediately as its own batch. - Restore theaf05f05cAPI byte-compatibly in the same package: New, Copyer, Wait/WaitErr, AccurateJob, WildcardJob, the report events and the JSON report round-trip through encoding/json alone. - One descriptor serves a whole item; the stored hash is read through the descriptor that reads the content and the computed hash is published through it before it closes. - Make every handoff escapable by a hard stop: the read buffer, a chunk send, an event send and the result buffer. - Fix the recorded sweep findings: report indent and round-trip, cmd/acp exit status, acp-rewrite final paths, mmap EOF/close/slice, disk-usage reservations, ErrTargetIO identity, the fatal-panic safety net, option validation, and the FreeBSD/Windows build paths. - Cover the contract with semantic tests, add Makefile check/race/vet/cross targets, and document the withdrawn v0.1.0/v0.2.0 migration in README.
100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
package acp
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"syscall"
|
|
"testing"
|
|
)
|
|
|
|
// TestMappingErrorIdentities pins the identity of every mapped target failure. An I/O error
|
|
// describes one target, not a device that turned read-only, so it must not abort the target.
|
|
func TestMappingErrorIdentities(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
from error
|
|
want error
|
|
wantAbort bool
|
|
}{
|
|
{name: "no space", from: syscall.ENOSPC, want: ErrTargetNoSpace, wantAbort: true},
|
|
{name: "read-only device", from: syscall.EROFS, want: ErrTargetDropToReadonly, wantAbort: true},
|
|
{name: "io error", from: syscall.EIO, want: ErrTargetIO, wantAbort: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mapped := mappingError(tt.from)
|
|
if !errors.Is(mapped, tt.want) {
|
|
t.Fatalf("mappingError(%v) = %v, want %v", tt.from, mapped, tt.want)
|
|
}
|
|
if got := checkErrorAbort(mapped); got != tt.wantAbort {
|
|
t.Fatalf("checkErrorAbort(%v) = %t, want %t", mapped, got, tt.wantAbort)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A target I/O error must never be classified as a device that dropped to read-only.
|
|
if errors.Is(mappingError(syscall.EIO), ErrTargetDropToReadonly) {
|
|
t.Fatal("an I/O error is still classified as a read-only device")
|
|
}
|
|
if err := mappingError(nil); err != nil {
|
|
t.Fatalf("mappingError(nil) = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
// TestDiskUsageRefreshKeepsInflightReservations pins the reservation accounting across a
|
|
// refresh: a new measurement replaces the capacity estimate, never the commitments already
|
|
// made against it on the same mount point.
|
|
func TestDiskUsageRefreshKeepsInflightReservations(t *testing.T) {
|
|
cache := newDiskUsageCache(t.TempDir(), math.MaxInt64)
|
|
if err := cache.check(1); err != nil {
|
|
t.Fatalf("first check: %v", err)
|
|
}
|
|
|
|
cache.lock.Lock()
|
|
// An in-flight copy has already reserved everything the device reported as free.
|
|
cache.used = cache.freeSpace + 1
|
|
cache.lock.Unlock()
|
|
|
|
// The next check refreshes, so the measurement can prove nothing landed; the reservation
|
|
// of the copy still in flight must reject the new one.
|
|
if err := cache.check(0); !errors.Is(err, ErrTargetNoSpace) {
|
|
t.Fatalf("check() = %v, want %v", err, ErrTargetNoSpace)
|
|
}
|
|
}
|
|
|
|
// TestDiskUsageAccountsLandedBytes pins the other half of the refresh: bytes that landed since
|
|
// the previous measurement have already consumed the measured capacity, so they stop holding a
|
|
// reservation of their own instead of being counted twice.
|
|
func TestDiskUsageAccountsLandedBytes(t *testing.T) {
|
|
cache := newDiskUsageCache("unused", math.MaxInt64)
|
|
|
|
// 800 bytes are reserved against a 1000-byte estimate, and 250 bytes landed since.
|
|
cache.freeSpace = 1000
|
|
cache.used = 800
|
|
if err := cache.account(750, 200); err != nil {
|
|
t.Fatalf("account() = %v, want nil", err)
|
|
}
|
|
if cache.freeSpace != 750 || cache.used != 550 {
|
|
t.Fatalf("cache = free %d used %d, want free 750 used 550", cache.freeSpace, cache.used)
|
|
}
|
|
|
|
// The request asking for space right now keeps its own reservation even when everything
|
|
// counted before it has landed.
|
|
cache.freeSpace = 1000
|
|
cache.used = 10
|
|
if err := cache.account(999, 100); err != nil {
|
|
t.Fatalf("account() = %v, want nil", err)
|
|
}
|
|
if cache.used != 100 {
|
|
t.Fatalf("used = %d, want the current request's 100", cache.used)
|
|
}
|
|
|
|
// A reservation that survives the refresh is still checked against the new capacity.
|
|
cache.freeSpace = 100
|
|
cache.used = 900
|
|
if err := cache.account(50, 200); !errors.Is(err, ErrTargetNoSpace) {
|
|
t.Fatalf("account() = %v, want %v", err, ErrTargetNoSpace)
|
|
}
|
|
}
|