Files
acp/mmap/mmap_linux_test.go
Samuel Cui 4d5dbd0b8c Release ACP v0.2.3
Simplify indexed copy facts and unify staged output completion.
Fix cleanup, rewrite persistence, native paths, and terminal reporting.
Maintain compatibility, upstream notices, and automated release checks.
2026-10-05 16:29:55 +08:00

118 lines
3.5 KiB
Go

//go:build linux
package mmap
import (
"errors"
"os"
"syscall"
"testing"
)
// recordPrefetch replaces the madvise syscall with a spy that records every advice the prefetch
// issues, so a test can pin the advice sequence and the error path without syscall injection.
func recordPrefetch(t *testing.T, fail error) *[]int {
t.Helper()
previous := madvise
advices := new([]int)
madvise = func(_ []byte, advice int) error {
*advices = append(*advices, advice)
return fail
}
t.Cleanup(func() { madvise = previous })
return advices
}
// TestPrefetchIssuesEveryAdvice pins the Linux prefetch. An madvise call takes exactly one advice:
// these are values, not flag bits, so passing `MADV_SEQUENTIAL|MADV_WILLNEED` keeps only the
// prefetch and silently drops the sequential hint, which is why the two hints are two calls.
func TestPrefetchIssuesEveryAdvice(t *testing.T) {
// Observe advice calls when opening a small mapping.
advices := recordPrefetch(t, nil)
reader, err := Open(writeFixture(t, "prefetch.bin", []byte("0123456789")))
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = reader.Close() })
// Require the documented hints independently of the implementation's advice list.
want := []int{syscall.MADV_SEQUENTIAL, syscall.MADV_WILLNEED}
if len(*advices) != len(want) {
t.Fatalf("prefetch issued advice %v, want %v", *advices, want)
}
for index, advice := range want {
if (*advices)[index] != advice {
t.Fatalf("prefetch issued advice %v, want %v", *advices, want)
}
}
}
// TestPrefetchSkipsAFileAboveTheWindow pins the documented threshold: a mapping above
// prefetchMaxSize receives no advice at all, so a large copy does not fault in its whole content.
func TestPrefetchSkipsAFileAboveTheWindow(t *testing.T) {
advices := recordPrefetch(t, nil)
path := writeFixture(t, "large.bin", nil)
if err := os.Truncate(path, prefetchMaxSize+1); err != nil {
t.Fatalf("grow the fixture: %v", err)
}
reader, err := Open(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = reader.Close() })
if len(*advices) != 0 {
t.Fatalf("a file above the prefetch window received advice %v", *advices)
}
}
// TestPrefetchFailureReleasesTheMapping pins the documented error path: a mapping whose madvise
// failed is released before the error returns, and the descriptor opened for it is closed with it,
// so a host that refuses the advice cannot leak either.
func TestPrefetchFailureReleasesTheMapping(t *testing.T) {
failure := errors.New("madvise fixture")
recordPrefetch(t, failure)
previousUnmap := munmap
unmapped := 0
munmap = func(data []byte) error {
unmapped++
return previousUnmap(data)
}
t.Cleanup(func() { munmap = previousUnmap })
path := writeFixture(t, "advice-failure.bin", []byte("0123456789"))
before := descriptorCount(t)
const attempts = 8
for index := 0; index < attempts; index++ {
reader, err := Open(path)
if reader != nil || err == nil {
t.Fatalf("Open with a failing madvise = (%v, %v), want a failure", reader, err)
}
if !errors.Is(err, failure) {
t.Fatalf("Open error = %v, want %v", err, failure)
}
}
if unmapped != attempts {
t.Fatalf("released %d mappings, want one per failed open", unmapped)
}
if after := descriptorCount(t); after > before {
t.Fatalf("descriptors leaked: %d -> %d", before, after)
}
}
// descriptorCount counts this process's open descriptors.
func descriptorCount(t *testing.T) int {
t.Helper()
entries, err := os.ReadDir("/proc/self/fd")
if err != nil {
t.Skipf("cannot count descriptors: %v", err)
}
return len(entries)
}