From c2d900ec74e03e9c782cfe7b7ce06ff62cda6601 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 29 Aug 2026 18:58:06 +0900 Subject: pkg: move from internal Closes #43. Signed-off-by: Ophestra --- pkg/pkg_test.go | 2375 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2375 insertions(+) create mode 100644 pkg/pkg_test.go (limited to 'pkg/pkg_test.go') diff --git a/pkg/pkg_test.go b/pkg/pkg_test.go new file mode 100644 index 00000000..c11b6ac4 --- /dev/null +++ b/pkg/pkg_test.go @@ -0,0 +1,2375 @@ +package pkg_test + +import ( + "archive/tar" + "bytes" + "context" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + "sync" + "syscall" + "testing" + "testing/fstest" + "time" + "unique" + "unsafe" + + "hakurei.app/check" + "hakurei.app/container" + "hakurei.app/fhs" + "hakurei.app/internal/info" + "hakurei.app/internal/landlock" + "hakurei.app/internal/stub" + "hakurei.app/message" + "hakurei.app/pkg" +) + +var skipLock = func() pkg.CacheAttr { + var attr pkg.CacheAttr + *(*bool)(unsafe.Pointer(reflect.ValueOf(&attr). + Elem(). + FieldByName("skipLock"). + UnsafeAddr())) = true + return attr +}() + +var ( + // extension is a string uniquely identifying a set of custom [Artifact] + // implementations registered by calling [Register]. + // + //go:linkname extension hakurei.app/pkg.extension + extension string + + // opened is false if [Open] was never called. + // + //go:linkname opened hakurei.app/pkg.opened + opened bool + + // irArtifact refers to artifact IR interpretation functions and must not be + // written to directly. + // + //go:linkname irArtifact hakurei.app/pkg.irArtifact + irArtifact map[pkg.Kind]pkg.IRReadFunc + + // statusHeader is the header written to all status files in dirStatus. + // + //go:linkname statusHeader hakurei.app/pkg.statusHeader + statusHeader string +) + +// newRContext returns the address of a new [pkg.RContext] unsafely created for +// the specified [testing.TB]. +func newRContext(tb testing.TB, c *pkg.Cache) *pkg.RContext { + var r pkg.RContext + rContextVal := reflect.ValueOf(&r).Elem().FieldByName("ctx") + reflect.NewAt( + rContextVal.Type(), + unsafe.Pointer(rContextVal.UnsafeAddr()), + ).Elem().Set(reflect.ValueOf(tb.Context())) + rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache") + reflect.NewAt( + rCacheVal.Type(), + unsafe.Pointer(rCacheVal.UnsafeAddr()), + ).Elem().Set(reflect.ValueOf(c)) + return &r +} + +// overrideIdent overrides the ID method of [Artifact]. +type overrideIdent struct { + id pkg.ID + pkg.TrivialArtifact +} + +func (a overrideIdent) ID() pkg.ID { return a.id } + +// overrideIdentFile overrides the ID method of [FileArtifact]. +type overrideIdentFile struct { + id pkg.ID + pkg.FileArtifact +} + +func (a overrideIdentFile) ID() pkg.ID { return a.id } + +// A knownIdentArtifact implements [pkg.KnownIdent] and [Artifact] +type knownIdentArtifact interface { + pkg.KnownIdent + pkg.TrivialArtifact +} + +// A knownIdentFile implements [pkg.KnownIdent] and [FileArtifact] +type knownIdentFile interface { + pkg.KnownIdent + pkg.FileArtifact +} + +// overrideChecksum overrides the Checksum method of [Artifact]. +type overrideChecksum struct { + checksum pkg.Checksum + knownIdentArtifact +} + +func (a overrideChecksum) Checksum() pkg.Checksum { return a.checksum } + +// overrideChecksumFile overrides the Checksum method of [FileArtifact]. +type overrideChecksumFile struct { + checksum pkg.Checksum + knownIdentFile +} + +func (a overrideChecksumFile) Checksum() pkg.Checksum { return a.checksum } + +// A stubArtifact implements [TrivialArtifact] with hardcoded behaviour. +type stubArtifact struct { + kind pkg.Kind + params []byte + deps []pkg.Artifact + + cure func(t *pkg.TContext) error +} + +func (a *stubArtifact) Kind() pkg.Kind { return a.kind } +func (a *stubArtifact) Params(ctx *pkg.IContext) { ctx.Write(a.params) } +func (a *stubArtifact) Inputs() []pkg.Artifact { return a.deps } +func (a *stubArtifact) Cure(t *pkg.TContext) error { return a.cure(t) } +func (*stubArtifact) IsExclusive() bool { return false } + +// A stubArtifactF implements [FloodArtifact] with hardcoded behaviour. +type stubArtifactF struct { + kind pkg.Kind + params []byte + deps []pkg.Artifact + excl bool + + cure func(f *pkg.FContext) error +} + +func (a *stubArtifactF) Kind() pkg.Kind { return a.kind } +func (a *stubArtifactF) Params(ctx *pkg.IContext) { ctx.Write(a.params) } +func (a *stubArtifactF) Inputs() []pkg.Artifact { return a.deps } +func (a *stubArtifactF) Cure(f *pkg.FContext) error { return a.cure(f) } +func (a *stubArtifactF) IsExclusive() bool { return a.excl } + +// A stubFile implements [FileArtifact] with hardcoded behaviour. +type stubFile struct { + data []byte + err error + + stubArtifact +} + +func (*stubFile) IsExecutable() bool { return false } + +func (a *stubFile) Cure(*pkg.RContext) (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(a.data)), a.err +} + +// newStubFile returns an implementation of [pkg.File] with hardcoded behaviour. +func newStubFile( + kind pkg.Kind, + id pkg.ID, + sum *pkg.Checksum, + data []byte, + err error, +) pkg.FileArtifact { + f := overrideIdentFile{id, &stubFile{data, err, stubArtifact{ + kind, + nil, + nil, + func(*pkg.TContext) error { + panic("unreachable") + }, + }}} + if sum == nil { + return f + } else { + return overrideChecksumFile{*sum, f} + } +} + +// stubExtern implements [External] with hardcoded prepared outcomes. +type stubExtern struct { + artifact map[unique.Handle[pkg.ID]]pkg.Checksum + checksum map[unique.Handle[pkg.Checksum]]fstest.MapFS + status map[unique.Handle[pkg.ID]]string +} + +func (e stubExtern) Artifact(_ context.Context, id unique.Handle[pkg.ID]) (*pkg.Checksum, error) { + if checksum, ok := e.artifact[id]; ok { + return &checksum, nil + } + return nil, nil +} + +func (e stubExtern) Checksum(checksum unique.Handle[pkg.Checksum]) pkg.Artifact { + var buf bytes.Buffer + if err := pkg.Write(e.checksum[checksum], ".", &buf); err != nil { + panic(err) + } + return pkg.NewArchive(pkg.NewFile("", buf.Bytes())) +} + +func (e stubExtern) Status(_ *pkg.RContext, id unique.Handle[pkg.ID]) (io.ReadCloser, error) { + if status, ok := e.status[id]; ok { + return io.NopCloser(strings.NewReader(status)), nil + } + return nil, nil +} + +// destroyArtifact removes all traces of an [Artifact] from the on-disk cache. +// Do not use this in a test case without a very good reason to do so. +func destroyArtifact( + t *testing.T, + base *check.Absolute, + c *pkg.Cache, + a pkg.Artifact, +) { + if pathname, checksum, err := c.Cure(a); err != nil { + t.Fatalf("Cure: error = %v", err) + } else if err = os.Remove(pathname.String()); err != nil { + t.Fatal(err) + } else { + p := base.Append( + "checksum", + pkg.Encode(checksum.Value()), + ) + if err = filepath.WalkDir(p.String(), func( + path string, + d fs.DirEntry, + err error, + ) error { + if err != nil { + return err + } + if d.IsDir() { + return os.Chmod(path, 0700) + } + return nil + }); err != nil && !errors.Is(err, os.ErrNotExist) { + t.Fatal(err) + } + if err = os.RemoveAll(p.String()); err != nil { + t.Fatal(err) + } + } +} + +// newDestroyArtifactFunc returns a function that calls destroyArtifact. +func newDestroyArtifactFunc(a pkg.Artifact) func( + t *testing.T, + base *check.Absolute, + c *pkg.Cache, +) { + return func( + t *testing.T, + base *check.Absolute, + c *pkg.Cache, + ) { + destroyArtifact(t, base, c, a) + } +} + +// destroyStatus counts non-substitution status entries and destroys them. +func destroyStatus(t *testing.T, base *check.Absolute, c, s int) { + dents, err := os.ReadDir(base.Append("status").String()) + if err != nil { + t.Fatal(err) + } + + var gotC, gotS int + for _, dent := range dents { + if err = os.Remove(base.Append( + "status", + dent.Name(), + ).String()); err != nil { + t.Fatal(err) + } + + if dent.Type().IsRegular() { + gotC++ + continue + } + if dent.Type()&fs.ModeSymlink == fs.ModeSymlink { + gotS++ + continue + } + t.Errorf("%s: %s", dent.Name(), dent.Type()) + } + + if gotC != c { + t.Errorf("status: c = %d, want %d", gotC, c) + } + if gotS != s { + t.Errorf("status: s = %d, want %d", gotS, s) + } +} + +func TestIdent(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + a pkg.Artifact + want unique.Handle[pkg.ID] + }{ + {"decompress", &stubArtifact{ + pkg.KindDecompress, + []byte{pkg.Gzip, 0, 0, 0, 0, 0, 0, 0}, + []pkg.Artifact{ + overrideIdent{pkg.ID{}, new(stubArtifact)}, + }, + nil, + }, unique.Make[pkg.ID](pkg.MustDecode( + "fkmge-H56Ph2LzNNbEUjTypes8LfOxrF_CEXg07kq6ho_nbTNvzQde9-bjB7gyB_", + ))}, + } + + msg := message.New(log.New(os.Stderr, "ident: ", 0)) + msg.SwapVerbose(true) + var cache *pkg.Cache + if a, err := check.NewAbs(t.TempDir()); err != nil { + t.Fatal(err) + } else if cache, err = pkg.Open(t.Context(), msg, a, nil); err != nil { + t.Fatal(err) + } + t.Cleanup(cache.Close) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := cache.Ident(tc.a); got != tc.want { + t.Errorf("Ident: %s, want %s", + pkg.Encode(got.Value()), + pkg.Encode(tc.want.Value()), + ) + } + }) + } +} + +// An expectsKnown describes an expected file or directory. +type expectsKnown interface { + // hash returns the checksum of the represented data. + hash() (checksum pkg.Checksum) +} + +// An expectsChecksum is a prepared checksum value. +type expectsChecksum pkg.Checksum + +// hash returns e. +func (e expectsChecksum) hash() pkg.Checksum { return e } + +// An expectsFile is the contents of a file expected by the test suite. +type expectsFile []byte + +// hash computes the checksum of e. +func (e expectsFile) hash() (checksum pkg.Checksum) { + h := sha512.New384() + h.Write(e) + h.Sum(checksum[:0]) + return +} + +// An expectsFS describes the state of a filesystem expected by the test suite. +type expectsFS fstest.MapFS + +// hash computes the checksum of e. +func (e expectsFS) hash() (checksum pkg.Checksum) { + if err := pkg.SumFS(&checksum, fstest.MapFS(e), "."); err != nil { + panic(err) + } + return +} + +// expectsFrom generates expectsFS for a filesystem directory. +func expectsFrom(pathname string) string { + var buf strings.Builder + buf.WriteString("expectsFS{\n") + if err := filepath.WalkDir(pathname, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + var rel string + if rel, err = filepath.Rel(pathname, path); err != nil { + return err + } + buf.WriteString("\t" + strconv.Quote(rel) + ": {Mode: ") + + var fi fs.FileInfo + if fi, err = d.Info(); err != nil { + return err + } + mode := fi.Mode() + + switch { + case mode.IsDir(): + buf.WriteString("fs.ModeDir | 0" + + strconv.FormatInt(int64(mode&^fs.ModeDir), 8)) + + case mode&fs.ModeSymlink != 0: + buf.WriteString("fs.ModeSymlink | 0" + + strconv.FormatInt(int64(mode&^fs.ModeSymlink), 8) + + ", Data: []byte(") + var linkname string + if linkname, err = os.Readlink(path); err != nil { + return err + } + buf.WriteString(strconv.Quote(linkname)) + buf.WriteByte(')') + + case mode.IsRegular(): + buf.WriteString("0" + strconv.FormatInt(int64(mode), 8)) + var p []byte + if p, err = os.ReadFile(path); err != nil { + return err + } + + if len(p) > 0 { + buf.WriteString(", Data: []byte(") + buf.WriteString(strconv.Quote(unsafe.String(unsafe.SliceData(p), len(p)))) + buf.WriteByte(')') + } + } + buf.WriteString("},\n") + return nil + }); err != nil { + panic(err) + } + buf.WriteString("}") + return buf.String() +} + +// cacheTestCase is a test case passed to checkWithCache where a new instance +// of [pkg.Cache] is prepared for the test case, and is validated and removed +// on test completion. +type cacheTestCase struct { + name string + flags int + early func(t *testing.T, base *check.Absolute) + f func(t *testing.T, base *check.Absolute, c *pkg.Cache) + want expectsFS +} + +const ( + // checkDestroySubstitutes arranges for substitutes to be destroyed before + // measurement during checkWithCache. + checkDestroySubstitutes = 1 << (iota + 32) +) + +// makeBase returns a [pkg.Cache] base directory created for tb. +func makeBase(tb testing.TB) (base *check.Absolute) { + tb.Helper() + + base = check.MustAbs(tb.TempDir()) + if err := os.Chmod(base.String(), 0700); err != nil { + tb.Fatal(err) + } + tb.Cleanup(func() { + if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error { + if err != nil { + tb.Error(err) + return nil + } + if !d.IsDir() { + return nil + } + return os.Chmod(path, 0700) + }); err != nil { + tb.Fatal(err) + } + }) + return +} + +// checkWithCache runs a slice of cacheTestCase. +func checkWithCache(t *testing.T, testCases []cacheTestCase) { + t.Helper() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Helper() + t.Parallel() + + base := makeBase(t) + msg := message.New(log.New(os.Stderr, "cache: ", 0)) + msg.SwapVerbose(testing.Verbose()) + + flags := tc.flags | pkg.CSuppressInit + + if info.CanDegrade { + if _, err := landlock.GetABI(); err != nil { + if !errors.Is(err, syscall.ENOSYS) { + t.Fatalf("LandlockGetABI: error = %v", err) + } + flags |= pkg.CHostAbstract + t.Log("Landlock LSM is unavailable, setting CHostAbstract") + } + } + + var scrubFunc func() error // scrub after hashing + if c, err := pkg.Open(t.Context(), msg, base, &pkg.CacheAttr{ + Cures: 1 << 4, + Flags: flags, + }); err != nil { + t.Fatalf("Open: error = %v", err) + } else { + t.Cleanup(c.Close) + if tc.early != nil { + tc.early(t, base) + } + tc.f(t, base, c) + scrubFunc = func() error { + err = c.Scrub(1 << 7) + idents, checksums, cleanErr := c.Clean(false, false) + if len(idents) > 0 { + t.Errorf("destroyed %d idents", len(idents)) + } + if len(checksums) > 0 { + t.Errorf("destroyed %d checksums", len(checksums)) + } + return errors.Join(err, cleanErr) + } + } + + var restoreTemp bool + if _, err := os.Lstat(base.Append("temp").String()); err != nil { + if !errors.Is(err, os.ErrNotExist) { + t.Fatal(err) + } + } else { + restoreTemp = true + } + + // destroy lock and variant file to avoid changing cache checksums + for _, s := range []string{ + "lock", + "variant", + } { + pathname := base.Append(s) + if p, err := os.ReadFile(pathname.String()); err != nil { + t.Fatal(err) + } else if len(p) != 0 { + t.Fatalf("file %q: %q", s, string(p)) + } + if err := os.Remove(pathname.String()); err != nil { + t.Fatal(err) + } + } + + // destroy non-deterministic substitutes + if tc.flags&checkDestroySubstitutes != 0 { + substitute := base.Append("substitute") + if err := os.RemoveAll(substitute.String()); err != nil { + t.Fatal(err) + } else if err = os.Mkdir(substitute.String(), 0700); err != nil { + t.Fatal(err) + } + } + + // destroy empty status directory + if err := syscall.Rmdir(base.Append("status").String()); err != nil { + if !errors.Is(err, syscall.ENOTEMPTY) { + t.Fatal(err) + } + } + + // destroy empty fault directory + if err := os.Remove(base.Append("fault").String()); err != nil { + t.Fatal(err) + } + + want := tc.want.hash() + + var checksum pkg.Checksum + if err := pkg.SumDir(&checksum, base); err != nil { + t.Fatalf("SumDir: error = %v", err) + } else if checksum != want { + t.Fatal(expectsFrom(base.String())) + } + + if err := scrubFunc(); err != nil { + t.Fatal("cache contains inconsistencies\n\n" + err.Error()) + } + + if restoreTemp { + if err := os.Mkdir( + base.Append("temp").String(), + 0700, + ); err != nil { + t.Fatal(err) + } + } + + // validate again to make sure scrub did not condemn anything + if err := pkg.SumDir(&checksum, base); err != nil { + t.Fatalf("SumDir: error = %v", err) + } else if checksum != want { + t.Fatalf("(scrubbed) %s", expectsFrom(base.String())) + } + }) + } +} + +// A cureStep contains an [Artifact] to be cured, and the expected outcome. +type cureStep struct { + name string + + a pkg.Artifact + + pathname *check.Absolute + output expectsKnown + whence int + err error +} + +// ignorePathname is passed to cureMany to skip the pathname check. +var ignorePathname = check.MustAbs("/\x00") + +// cureMany cures many artifacts against a [Cache] and checks their outcomes. +func cureMany(t *testing.T, c *pkg.Cache, steps []cureStep) { + t.Helper() + + makeChecksumH := func(checksum pkg.Checksum) unique.Handle[pkg.Checksum] { + if checksum == (pkg.Checksum{}) { + return unique.Handle[pkg.Checksum]{} + } + return unique.Make(checksum) + } + + for _, step := range steps { + t.Log("cure step:", step.name) + if pathname, checksum, whence, err := c.CureWhence(step.a); !reflect.DeepEqual(err, step.err) { + faults, _err := c.ReadFaults(step.a) + if _err != nil { + t.Errorf("ReadFaults: error = %v", _err) + } + + var p []byte + for _, fault := range faults { + p, _err = os.ReadFile(fault.String()) + if _err != nil { + t.Error(_err) + continue + } + t.Log(string(p)) + t.Logf("faulting cure terminated %s ago", time.Since(faults[0].Time())) + } + t.Fatalf("Cure: error = %v, want %v", err, step.err) + } else if step.whence != whence { + t.Fatalf("Cure: whence = %s, want %s", pkg.WhenceString(whence), pkg.WhenceString(step.whence)) + } else if step.pathname != ignorePathname && !pathname.Is(step.pathname) { + t.Fatalf("Cure: pathname = %q, want %q", pathname, step.pathname) + } else if step.output == nil || checksum != makeChecksumH(step.output.hash()) { + if pathname != nil { + if name, _err := filepath.EvalSymlinks(pathname.String()); _err != nil { + t.Fatal(_err) + } else { + t.Fatal(expectsFrom(name)) + } + } else if checksum != (unique.Handle[pkg.Checksum]{}) { + t.Fatalf("Cure: unexpected checksum %s", pkg.Encode(checksum.Value())) + } + } else { + v := any(err) + if err == nil { + v = pathname + } + var checksumVal pkg.Checksum + if checksum != (unique.Handle[pkg.Checksum]{}) { + checksumVal = checksum.Value() + } + t.Log(pkg.Encode(checksumVal)+":", v) + } + } +} + +// newWantScrubError returns the address to a new [ScrubError] for base. +func newWantScrubError(base *check.Absolute) *pkg.ScrubError { + return &pkg.ScrubError{ + ChecksumMismatches: []pkg.ChecksumMismatchError{ + {Got: pkg.MustDecode( + "vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", + ), Want: pkg.Checksum{0xff, 0}}, + }, + DanglingIdentifiers: []pkg.ID{ + {0xfe, 0}, + {0xfe, 0xfe}, + {0xfe, 0xff}, + }, + Errs: map[unique.Handle[string]][]error{ + base.Append("checksum", "__8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").Handle(): { + pkg.InvalidFileModeError(fs.ModeSymlink), + }, + + base.Append("checksum", "invalid").Handle(): { + base64.CorruptInputError(4), + }, + + base.Append("nonexistent").Handle(): { + base64.CorruptInputError(8), + }, + + base.Append("identifier", pkg.Encode(pkg.ID{0xfe, 0xff})).Handle(): { + &os.PathError{ + Op: "readlink", + Path: base.Append( + "identifier", + pkg.Encode(pkg.ID{0xfe, 0xff}), + ).String(), + Err: syscall.EINVAL, + }, + }, + base.Append("identifier", "invalid").Handle(): { + base64.CorruptInputError(4), + }, + }, + } +} + +func TestCache(t *testing.T) { + t.Parallel() + + testdata := expectsFile("" + + "\x00\x00\x00\x00" + + "\xad\x0b\x00" + + "\x04" + + "\xfe\xfe\x00\x00" + + "\xfe\xca\x00\x00") + + testCases := []cacheTestCase{ + {"file", pkg.CValidateKnown | pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + identifier := (pkg.ID)(bytes.Repeat([]byte{ + 0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f, + }, 8)) + wantPathname := base.Append( + "identifier", + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + ) + identifier0 := (pkg.ID)(bytes.Repeat([]byte{ + 0x71, 0xa7, 0xde, 0x6d, 0xa6, 0xde, + }, 8)) + wantPathname0 := base.Append( + "identifier", + "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe", + ) + failingFile := newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 3}, + nil, + nil, struct { + _ []byte + stub.UniqueError + }{UniqueError: 0xbad}, + ) + + cureMany(t, c, []cureStep{ + {"initial file", newStubFile( + pkg.KindHTTPGet, + identifier, + new(testdata.hash()), + testdata, nil, + ), wantPathname, testdata, pkg.WNew, nil}, + + {"identical content", newStubFile( + pkg.KindHTTPGet, + identifier0, + new(testdata.hash()), + testdata, nil, + ), wantPathname0, testdata, pkg.WCache, nil}, + + {"existing entry", newStubFile( + pkg.KindHTTPGet, + identifier, + new(testdata.hash()), + testdata, nil, + ), wantPathname, testdata, pkg.WCache, nil}, + + {"checksum mismatch", newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 0}, + new(pkg.Checksum), + testdata, nil, + ), nil, nil, pkg.WNew, &pkg.ChecksumMismatchError{ + Got: testdata.hash(), + }}, + + {"store without validation", newStubFile( + pkg.KindHTTPGet, + pkg.MustDecode("vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX"), + nil, + []byte{0}, nil, + ), base.Append( + "identifier", + "vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", + ), expectsChecksum{ + 0xbe, 0xc0, 0x21, 0xb4, 0xf3, 0x68, + 0xe3, 0x06, 0x91, 0x34, 0xe0, 0x12, + 0xc2, 0xb4, 0x30, 0x70, 0x83, 0xd3, + 0xa9, 0xbd, 0xd2, 0x06, 0xe2, 0x4e, + 0x5f, 0x0d, 0x86, 0xe1, 0x3d, 0x66, + 0x36, 0x65, 0x59, 0x33, 0xec, 0x2b, + 0x41, 0x34, 0x65, 0x96, 0x68, 0x17, + 0xa9, 0xc2, 0x08, 0xa1, 0x17, 0x17, + }, pkg.WNew, nil}, + + {"incomplete implementation", struct{ pkg.Artifact }{&stubArtifact{ + kind: pkg.KindExec, + params: []byte("artifact overridden to be incomplete"), + }}, nil, nil, pkg.WNew, pkg.InvalidArtifactError(pkg.MustDecode( + "poVrv3zNINbHd-WUJihwgWQwJpbHURFHfpY7Zux-ztto_bQhhS5PdpbNMjdkXv-V", + ))}, + + {"error passthrough", newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 1}, + nil, + nil, stub.UniqueError(0xcafe), + ), nil, nil, pkg.WNew, stub.UniqueError(0xcafe)}, + + {"error caching", newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 1}, + nil, + nil, nil, + ), nil, nil, pkg.WCache, stub.UniqueError(0xcafe)}, + + {"cache hit bad type", overrideChecksum{testdata.hash(), overrideIdent{pkg.ID{0xff, 2}, &stubArtifact{ + kind: pkg.KindTar, + }}}, nil, nil, pkg.WCache, pkg.InvalidFileModeError( + 0400, + )}, + + {"noncomparable error", &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("artifact with dependency returning noncomparable error"), + deps: []pkg.Artifact{failingFile}, + + cure: func(f *pkg.FContext) error { + panic("attempting to cure impossible artifact") + }, + }, nil, nil, pkg.WNew, pkg.InputError{ + failingFile: struct { + _ []byte + stub.UniqueError + }{UniqueError: 0xbad}, + }}, + }) + + if c0, err := pkg.Open( + t.Context(), + message.New(nil), + base, &skipLock, + ); err != nil { + t.Fatalf("open: error = %v", err) + } else { + t.Cleanup(c.Close) // check doubled cancel + cureMany(t, c0, []cureStep{ + {"cache hit ident", overrideIdent{ + id: identifier, + }, wantPathname, testdata, pkg.WCache, nil}, + + {"cache miss checksum match", newStubFile( + pkg.KindHTTPGet, + testdata.hash(), + nil, + testdata, + nil, + ), base.Append( + "identifier", + pkg.Encode(testdata.hash()), + ), testdata, pkg.WNew, nil}, + }) + + // cure after close + c.Close() + if _, _, err = c.Cure(&stubArtifactF{ + kind: pkg.KindExec, + params: []byte("unreachable artifact cured after cancel"), + deps: []pkg.Artifact{pkg.NewFile("", []byte("unreachable dependency"))}, + }); !reflect.DeepEqual(err, context.Canceled) { + t.Fatalf("(closed) Cure: error = %v", err) + } + } + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0}}, + "checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: 0400, Data: []byte{0, 0, 0, 0, 0xad, 0xb, 0, 4, 0xfe, 0xfe, 0, 0, 0xfe, 0xca, 0, 0}}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")}, + "identifier/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")}, + "identifier/cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")}, + "identifier/deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq")}, + + "substitute": {Mode: fs.ModeDir | 0700}, + + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"directory", pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + id := pkg.MustDecode( + "HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", + ) + makeSample := func(t *pkg.TContext) error { + work := t.GetWorkDir() + if err := os.Mkdir(work.String(), 0700); err != nil { + return err + } + + if err := os.WriteFile( + work.Append("check").String(), + []byte{0, 0}, + 0400, + ); err != nil { + return err + } + + if err := os.MkdirAll(work.Append( + "lib", + "pkgconfig", + ).String(), 0700); err != nil { + return err + } + + return os.Symlink( + "/proc/nonexistent/libedac.so", + work.Append( + "lib", + "libedac.so", + ).String(), + ) + } + want := expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + + "check": {Mode: 0400, Data: []byte{0, 0}}, + + "lib": {Mode: fs.ModeDir | 0700}, + "lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, + + "lib/pkgconfig": {Mode: fs.ModeDir | 0700}, + } + wantPathname := base.Append( + "identifier", + pkg.Encode(id), + ) + + id0 := pkg.MustDecode( + "Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa", + ) + wantPathname0 := base.Append( + "identifier", + pkg.Encode(id0), + ) + + makeGarbage := func(work *check.Absolute, wantErr error) error { + if err := os.Mkdir(work.String(), 0700); err != nil { + return err + } + + mode := fs.FileMode(0) + if wantErr == nil { + mode = 0500 + } + + if err := os.MkdirAll(work.Append( + "lib", + "pkgconfig", + ).String(), 0700); err != nil { + return err + } + + if err := os.WriteFile(work.Append( + "lib", + "check", + ).String(), nil, 0400&mode); err != nil { + return err + } + + if err := os.Chmod(work.Append( + "lib", + "pkgconfig", + ).String(), 0500&mode); err != nil { + return err + } + if err := os.Chmod(work.Append( + "lib", + ).String(), 0500&mode); err != nil { + return err + } + + return wantErr + } + + cureMany(t, c, []cureStep{ + {"initial directory", overrideChecksum{want.hash(), overrideIdent{id, &stubArtifact{ + kind: pkg.KindTar, + cure: makeSample, + }}}, wantPathname, want, pkg.WNew, nil}, + + {"identical identifier", overrideChecksum{want.hash(), overrideIdent{id, &stubArtifact{ + kind: pkg.KindTar, + }}}, wantPathname, want, pkg.WCache, nil}, + + {"identical checksum", overrideIdent{id0, &stubArtifact{ + kind: pkg.KindTar, + cure: makeSample, + }}, wantPathname0, want, pkg.WNew, nil}, + + {"cure fault", overrideIdent{pkg.ID{0xff, 0}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return makeGarbage(t.GetWorkDir(), stub.UniqueError(0xcafe)) + }, + }}, nil, nil, pkg.WNew, stub.UniqueError(0xcafe)}, + + {"checksum mismatch", overrideChecksum{pkg.Checksum{}, overrideIdent{pkg.ID{0xff, 1}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return makeGarbage(t.GetWorkDir(), nil) + }, + }}}, nil, nil, pkg.WNew, &pkg.ChecksumMismatchError{ + Got: pkg.MustDecode( + "CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT", + ), + }}, + + {"cache hit bad type", newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 2}, + new(want.hash()), + testdata, nil, + ), nil, nil, pkg.WCache, pkg.InvalidFileModeError( + fs.ModeDir | 0500, + )}, + + {"openFile directory", overrideIdent{pkg.ID{0xff, 3}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + r, err := t.Open(overrideChecksumFile{checksum: want.hash()}) + if err != nil { + panic(err) + } + _, err = io.ReadAll(r) + return err + }, + }}, nil, nil, pkg.WNew, &os.PathError{ + Op: "read", + Path: base.Append( + "checksum", + pkg.Encode(want.hash()), + ).String(), + Err: syscall.EISDIR, + }}, + + {"no output", overrideIdent{pkg.ID{0xff, 4}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return nil + }, + }}, nil, nil, pkg.WNew, pkg.NoOutputError{}}, + + {"file output", overrideIdent{pkg.ID{0xff, 5}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return os.WriteFile(t.GetWorkDir().String(), []byte{0}, 0400) + }, + }}, nil, nil, pkg.WNew, errors.New("non-file artifact produced regular file")}, + + {"symlink output", overrideIdent{pkg.ID{0xff, 6}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return os.Symlink( + t.GetWorkDir().String(), + t.GetWorkDir().String(), + ) + }, + }}, nil, nil, pkg.WNew, pkg.InvalidFileModeError( + fs.ModeSymlink | 0777, + )}, + + {"alternative", &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("substitutable artifact"), + deps: []pkg.Artifact{newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 8}, + nil, + []byte("substitutable dependency"), + nil, + )}, + + cure: func(f *pkg.FContext) error { + return makeSample(&f.TContext) + }, + }, base.Append( + "identifier", + "gIH5eti07nBAhVti3ZTn8EIGOBCZWWvNpxg5CmCZlDzIC4mBdomPmQLeEpf1IQg9", + ), want, pkg.WNew, nil}, + + {"substitutable", &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("substitutable artifact"), + deps: []pkg.Artifact{newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 10}, + nil, + []byte("substitutable dependency"), + nil, + )}, + + cure: func(f *pkg.FContext) error { + panic("substitution missed") + }, + }, base.Append( + "identifier", + "VGjpp7NVw3OYLpgOQumfM0KcJgY3suz6torykUczcg4bGT24ySkPn5hy-4dNqqkR", + ), want, pkg.WSubstitute, nil}, + }) + + if c0, err := pkg.Open( + t.Context(), + message.New(nil), + base, &skipLock, + ); err != nil { + t.Fatalf("open: error = %v", err) + } else { + t.Cleanup(c.Close) + cureMany(t, c0, []cureStep{ + {"substitutable", &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("substitutable artifact"), + deps: []pkg.Artifact{newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 0xff, 0xfd, 0xfd}, + nil, + []byte("substitutable dependency"), + nil, + )}, + + cure: func(f *pkg.FContext) error { + panic("substitution missed") + }, + }, base.Append( + "identifier", + "zeZ4JEZ0atAKl6Am67cBID6-KR18nZGgeYfU4qTuYaWhQC4eAn0dyTXdIBH9Bos8", + ), want, pkg.WSubstitute, nil}, + }) + } + + if dents, err := os.ReadDir(base.Append("status").String()); err != nil { + t.Fatal(err) + } else if len(dents) > 0 { + t.Errorf("ReadDir: %v", dents) + } + + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/7lfQ4QwSpV8nw7IDh0JiQ_jqUPrPv3_Vfie034RxsSy-cy4vO8DVvxgpx2LW08oO": {Mode: 0400, Data: []byte("substitutable dependency")}, + "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b": {Mode: fs.ModeDir | 0500}, + "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/check": {Mode: 0400, Data: []byte{0, 0}}, + "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib": {Mode: fs.ModeDir | 0700}, + "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib/pkgconfig": {Mode: fs.ModeDir | 0700}, + "checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b/lib/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")}, + "identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")}, + "identifier/zeZ4JEZ0atAKl6Am67cBID6-KR18nZGgeYfU4qTuYaWhQC4eAn0dyTXdIBH9Bos8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")}, + "identifier/___9_QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/7lfQ4QwSpV8nw7IDh0JiQ_jqUPrPv3_Vfie034RxsSy-cy4vO8DVvxgpx2LW08oO")}, + "identifier/_wgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/7lfQ4QwSpV8nw7IDh0JiQ_jqUPrPv3_Vfie034RxsSy-cy4vO8DVvxgpx2LW08oO")}, + "identifier/_woAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/7lfQ4QwSpV8nw7IDh0JiQ_jqUPrPv3_Vfie034RxsSy-cy4vO8DVvxgpx2LW08oO")}, + "identifier/VGjpp7NVw3OYLpgOQumfM0KcJgY3suz6torykUczcg4bGT24ySkPn5hy-4dNqqkR": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")}, + "identifier/gIH5eti07nBAhVti3ZTn8EIGOBCZWWvNpxg5CmCZlDzIC4mBdomPmQLeEpf1IQg9": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")}, + + "substitute": {Mode: fs.ModeDir | 0700}, + "substitute/nWPg893oqn_YNN8kgfZrs_LVLwP90e0mKxqU8UwoazQh-yBPwhoviD7of6YALx-M": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b")}, + + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"pending", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + wantErr := stub.UniqueError(0xcafe) + n, ready := make(chan struct{}), make(chan struct{}) + go func() { + if _, _, err := c.Cure(overrideIdent{pkg.ID{0xff}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + close(ready) + <-n + return wantErr + }, + }}); !reflect.DeepEqual(err, wantErr) { + panic(fmt.Sprintf("Cure: error = %v, want %v", err, wantErr)) + } + }() + + <-ready + wCureDone := make(chan struct{}) + go func() { + if _, _, err := c.Cure(overrideIdent{pkg.ID{0xff}, &stubArtifact{ + kind: pkg.KindTar, + }}); !reflect.DeepEqual(err, wantErr) { + panic(fmt.Sprintf("Cure: error = %v, want %v", err, wantErr)) + } + close(wCureDone) + }() + + // check cache activity while a cure is blocking + cureMany(t, c, []cureStep{ + {"error passthrough", newStubFile( + pkg.KindHTTPGet, + pkg.ID{0xff, 1}, + nil, + nil, stub.UniqueError(0xbad), + ), nil, nil, pkg.WNew, stub.UniqueError(0xbad)}, + + {"file output", overrideIdent{pkg.ID{0xff, 2}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return os.WriteFile( + t.GetWorkDir().String(), + []byte{0}, + 0400, + ) + }, + }}, nil, nil, pkg.WNew, errors.New( + "non-file artifact produced regular file", + )}, + }) + + wantErrScrub := &pkg.ScrubError{ + Errs: map[unique.Handle[string]][]error{ + base.Handle(): {errors.New("scrub began with pending artifacts")}, + }, + } + if err := c.Scrub(1 << 6); !reflect.DeepEqual(err, wantErrScrub) { + t.Fatalf("Scrub: error = %#v, want %#v", err, wantErrScrub) + } + + notify := c.Done(unique.Make(pkg.ID{0xff})) + go close(n) + if notify != nil { + <-notify + } + for c.Done(unique.Make(pkg.ID{0xff})) != nil { + } + <-wCureDone + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + "checksum": {Mode: fs.ModeDir | 0700}, + "identifier": {Mode: fs.ModeDir | 0700}, + "substitute": {Mode: fs.ModeDir | 0700}, + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"cancel abort block", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + var wg sync.WaitGroup + defer wg.Wait() + + var started sync.WaitGroup + defer started.Wait() + + blockCures := func(d byte, e stub.UniqueError, n int) { + started.Add(n) + for i := range n { + wg.Go(func() { + if _, _, err := c.Cure(overrideIdent{pkg.ID{d, byte(i)}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + started.Done() + <-t.Unwrap().Done() + return e + stub.UniqueError(i) + }, + }}); !reflect.DeepEqual(err, e+stub.UniqueError(i)) { + panic(err) + } + }) + } + started.Wait() + } + + blockCures(0xfd, 0xbad, 16) + c.Abort() + wg.Wait() + + blockCures(0xfd, 0xcafe, 16) + c.Abort() + wg.Wait() + + blockCures(0xff, 0xbad, 1) + if !c.Cancel(unique.Make(pkg.ID{0xff})) { + t.Fatal("missed cancellation") + } + wg.Wait() + + blockCures(0xff, 0xcafe, 1) + if !c.Cancel(unique.Make(pkg.ID{0xff})) { + t.Fatal("missed cancellation") + } + wg.Wait() + + for c.Cancel(unique.Make(pkg.ID{0xff})) { + } + + c.Close() + c.Abort() + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + "checksum": {Mode: fs.ModeDir | 0700}, + "identifier": {Mode: fs.ModeDir | 0700}, + "substitute": {Mode: fs.ModeDir | 0700}, + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"no assume checksum", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + makeGarbage := func(work *check.Absolute, wantErr error) error { + if err := os.Mkdir(work.String(), 0700); err != nil { + return err + } + + if err := os.WriteFile(work.Append( + "check", + ).String(), nil, 0400); err != nil { + return err + } + + return wantErr + } + + want := expectsChecksum(pkg.MustDecode( + "Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M", + )) + + cureMany(t, c, []cureStep{ + {"create", overrideChecksum{want.hash(), overrideIdent{pkg.ID{0xff, 0}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return makeGarbage(t.GetWorkDir(), nil) + }, + }}}, base.Append( + "identifier", + pkg.Encode(pkg.ID{0xff, 0}), + ), want, pkg.WNew, nil}, + + {"reject", overrideChecksum{want.hash(), overrideIdent{pkg.ID{0xfe, 1}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return makeGarbage(t.GetWorkDir(), stub.UniqueError(0xbad)) + }, + }}}, nil, nil, pkg.WNew, stub.UniqueError(0xbad)}, + + {"match", overrideChecksum{want.hash(), overrideIdent{pkg.ID{0xff, 1}, &stubArtifact{ + kind: pkg.KindTar, + cure: func(t *pkg.TContext) error { + return makeGarbage(t.GetWorkDir(), nil) + }, + }}}, base.Append( + "identifier", + pkg.Encode(pkg.ID{0xff, 1}), + ), want, pkg.WNew, nil}, + }) + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M": {Mode: fs.ModeDir | 0500}, + "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check": {Mode: 0400, Data: []byte{}}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")}, + "identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")}, + + "substitute": {Mode: fs.ModeDir | 0700}, + + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"scrub", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + cureMany(t, c, []cureStep{ + {"bad measured file", newStubFile( + pkg.KindHTTPGet, + pkg.Checksum{0xfe, 0}, + &pkg.Checksum{0xff, 0}, + []byte{0}, nil, + ), base.Append( + "identifier", + pkg.Encode(pkg.Checksum{0xfe, 0}), + ), expectsChecksum{0xff, 0}, pkg.WNew, nil}, + }) + + for _, p := range [][]string{ + {"identifier", "invalid"}, + {"identifier", pkg.Encode(pkg.ID{0xfe, 0xff})}, + {"checksum", "invalid"}, + } { + if err := os.WriteFile( + base.Append(p...).String(), + nil, + 0400, + ); err != nil { + t.Fatal(err) + } + } + + for _, p := range [][]string{ + {"../nonexistent", "checksum", pkg.Encode(pkg.Checksum{0xff, 0xff})}, + {"../nonexistent", "identifier", pkg.Encode(pkg.Checksum{0xfe, 0xfe})}, + } { + if err := os.Symlink( + p[0], + base.Append(p[1:]...).String(), + ); err != nil { + t.Fatal(err) + } + } + + wantErr := newWantScrubError(base) + if err := c.Scrub(1 << 6); !reflect.DeepEqual(err, wantErr) { + t.Fatalf("Scrub: error =\n%s\nwant\n%s", err, wantErr) + } + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + "checksum": {Mode: fs.ModeDir | 0700}, + "identifier": {Mode: fs.ModeDir | 0700}, + "substitute": {Mode: fs.ModeDir | 0700}, + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"status substitute clean", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + destroyed := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("destroyed"), + deps: []pkg.Artifact{ + pkg.NewFile("destroyed-input", []byte("destroyed")), + }, + cure: func(f *pkg.FContext) error { + if w, err := f.GetStatusWriter(); err != nil { + return err + } else if _, err = w.Write([]byte("destroyed")); err != nil { + return err + } + + p := f.GetWorkDir() + if err := os.MkdirAll(p.String(), 0755); err != nil { + return err + } + return os.WriteFile(p.Append("result").String(), nil, 0444) + }, + } + substituted := new(*destroyed) + substituted.deps = []pkg.Artifact{ + pkg.NewFile("destroyed-input-0", []byte("destroyed")), + } + substituted.cure = func(*pkg.FContext) error { + panic("substitutable cure reached") + } + + cureMany(t, c, []cureStep{ + {"destroyed", destroyed, base.Append( + "identifier", + pkg.Encode(c.Ident(destroyed).Value()), + ), expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + "result": {Mode: 0444}, + }, pkg.WNew, nil}, + + {"substituted", substituted, base.Append( + "identifier", + pkg.Encode(c.Ident(substituted).Value()), + ), expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + "result": {Mode: 0444}, + }, pkg.WSubstitute, nil}, + }) + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE": {Mode: fs.ModeDir | 0500}, + "checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444}, + "checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV": {Mode: 0400, Data: []byte("destroyed")}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/CLyyxJWn1yUGFs_0KnJ4ZSU2hH4-elvGFRB36x14LrNrKBJPxvORhL9O4Rc7oeYU": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")}, + "identifier/arv8a6z4i0x6pUb5Z2T-3iE4ETvyZj-uReHGKsS7uiBgMgKNEOzTGG3LIZb8vp0L": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")}, + "identifier/heRgWhNJmhm898V68kw0ta76t4rP1o29tYW_TrdyBWXGfo37K3QWA7Z-JkLXAwcn": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, + "identifier/q-VoVBI3IjcWTqwIokpi3y05CP113CHgHslc-XWnhOvDULwzVc3q9Y5xOCm_YyLZ": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, + + "status": {Mode: fs.ModeDir | 0700}, + "status/q-VoVBI3IjcWTqwIokpi3y05CP113CHgHslc-XWnhOvDULwzVc3q9Y5xOCm_YyLZ": {Mode: fs.ModeSymlink | 0777, Data: []byte("dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x")}, + "status/heRgWhNJmhm898V68kw0ta76t4rP1o29tYW_TrdyBWXGfo37K3QWA7Z-JkLXAwcn": {Mode: 0400, Data: []byte(statusHeader + "destroyed")}, + "status/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: 0400, Data: []byte(statusHeader + "destroyed")}, + + "substitute": {Mode: fs.ModeDir | 0700}, + "substitute/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, + + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"extern", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + a := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("extern"), + } + wantIdent := c.Ident(a) + wantOutput := expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + "result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")}, + } + var wantChecksum pkg.Checksum + if err := pkg.SumFS( + &wantChecksum, + fstest.MapFS(wantOutput), + ".", + ); err != nil { + t.Fatal(err) + } + wantChecksumH := unique.Make(wantChecksum) + + _a := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("extern substitute"), + deps: []pkg.Artifact{pkg.NewFile("", nil)}, + } + _wantIdent := c.Ident(_a) + _wantOutput := expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + } + var _wantChecksum pkg.Checksum + if err := pkg.SumFS( + &_wantChecksum, + fstest.MapFS(_wantOutput), + ".", + ); err != nil { + t.Fatal(err) + } + _wantChecksumH := unique.Make(_wantChecksum) + + kca := pkg.NewExec( + "", "", + new(pkg.Checksum), 0, false, false, + fhs.AbsRoot, nil, fhs.AbsRoot, nil, + ) + kcIdent := c.Ident(kca) + + c.SetExternal(stubExtern{ + artifact: map[unique.Handle[pkg.ID]]pkg.Checksum{ + wantIdent: wantChecksum, + _wantIdent: _wantChecksum, + kcIdent: wantChecksum, + }, + checksum: map[unique.Handle[pkg.Checksum]]fstest.MapFS{ + wantChecksumH: fstest.MapFS(wantOutput), + _wantChecksumH: fstest.MapFS(_wantOutput), + }, + status: map[unique.Handle[pkg.ID]]string{ + wantIdent: "\x00", + kcIdent: "unreachable", + }, + }) + + cureMany(t, c, []cureStep{ + {"extern", a, base.Append( + "identifier", + pkg.Encode(wantIdent.Value()), + ), wantOutput, pkg.WExternal, nil}, + + {"substitute", _a, base.Append( + "identifier", + pkg.Encode(_wantIdent.Value()), + ), _wantOutput, pkg.WExternal, nil}, + + {"mismatch", kca, nil, nil, pkg.WExternal, &pkg.ChecksumMismatchError{ + Got: wantChecksum, + }}, + }) + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500}, + "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400}, + "checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl": {Mode: fs.ModeDir | 0500}, + "checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl/result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/k8yyhlBV3JpYzzywAnrRWkfp2TmgIDNC6FqMFwrrR2x0C3unBLveDNKgigOjG9MM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "identifier/Xys8hPpJsKirZmTTuT7SwQ6661TCp_ZRu8iUjPPD7pC5E3ll7xw4zhNlZ2PVcz_-": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "identifier/8sInO_dhPW9kweit7jVuH_vtMkRxlZT7XE4_yEKPZmpGkgVxkmxz0OpXgbMsH6ET": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "identifier/ofaXbB5gIza_7W4Rzo5XDe_8FwlCPKWn3TBcmGgFAkkTWMl5GXGmOzX89Jd2Mk2A": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, + "identifier/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + + "status": {Mode: fs.ModeDir | 0700}, + "status/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: 0400, Data: []byte("\x00")}, + + "substitute": {Mode: fs.ModeDir | 0700}, + "substitute/socGRnhdsJlJ4Z01ZwHFZdLzFH0rWvoaJrylcUKtvhBdGsE7VoYl-vdUuvfK8B4I": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + + "work": {Mode: fs.ModeDir | 0700}, + }}, + + {"extern shallow", pkg.CExternShallow, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { + a := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("extern"), + } + wantIdent := c.Ident(a) + wantOutput := expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + "result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")}, + } + var wantChecksum pkg.Checksum + if err := pkg.SumFS( + &wantChecksum, + fstest.MapFS(wantOutput), + ".", + ); err != nil { + t.Fatal(err) + } + wantChecksumH := unique.Make(wantChecksum) + + _a := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("extern substitute"), + deps: []pkg.Artifact{pkg.NewFile("", nil)}, + } + _wantIdent := c.Ident(_a) + _wantOutput := expectsFS{ + ".": {Mode: fs.ModeDir | 0500}, + } + var _wantChecksum pkg.Checksum + if err := pkg.SumFS( + &_wantChecksum, + fstest.MapFS(_wantOutput), + ".", + ); err != nil { + t.Fatal(err) + } + _wantChecksumH := unique.Make(_wantChecksum) + + kca := pkg.NewExec( + "", "", + new(pkg.Checksum), 0, false, false, + fhs.AbsRoot, nil, fhs.AbsRoot, nil, + ) + kcIdent := c.Ident(kca) + + fia := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("flood input"), + deps: []pkg.Artifact{_a}, + } + fiaIdent := c.Ident(fia) + + flia := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("flood input local"), + deps: []pkg.Artifact{_a}, + } + fliaIdent := c.Ident(flia) + + h := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("hanging input"), + } + + fhia := &stubArtifactF{ + kind: pkg.KindExec, + params: []byte("flood input local"), + deps: []pkg.Artifact{h}, + } + fhiaIdent := c.Ident(fhia) + + c.SetExternal(stubExtern{ + artifact: map[unique.Handle[pkg.ID]]pkg.Checksum{ + wantIdent: wantChecksum, + _wantIdent: _wantChecksum, + kcIdent: wantChecksum, + fiaIdent: wantChecksum, + fliaIdent: wantChecksum, + fhiaIdent: wantChecksum, + }, + checksum: map[unique.Handle[pkg.Checksum]]fstest.MapFS{ + wantChecksumH: fstest.MapFS(wantOutput), + _wantChecksumH: fstest.MapFS(_wantOutput), + }, + status: map[unique.Handle[pkg.ID]]string{ + wantIdent: "\x00", + kcIdent: "unreachable", + }, + }) + + cureMany(t, c, []cureStep{ + {"extern", a, base.Append( + "identifier", + pkg.Encode(wantIdent.Value()), + ), wantOutput, pkg.WExternal, nil}, + + {"flood shallow extern", fia, base.Append( + "identifier", + pkg.Encode(fiaIdent.Value()), + ), wantOutput, pkg.WExternal, nil}, + + {"substitute", _a, base.Append( + "identifier", + pkg.Encode(_wantIdent.Value()), + ), _wantOutput, pkg.WExternal, nil}, + + {"flood shallow local", flia, base.Append( + "identifier", + pkg.Encode(fliaIdent.Value()), + ), wantOutput, pkg.WExternal, nil}, + + {"flood shallow hanging", fhia, nil, nil, + pkg.WNew, pkg.HangingInputError(c.Ident(h))}, + + {"mismatch", kca, nil, nil, pkg.WExternal, &pkg.ChecksumMismatchError{ + Got: wantChecksum, + }}, + }) + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500}, + "checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl": {Mode: fs.ModeDir | 0500}, + "checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl/result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")}, + + "identifier": {Mode: fs.ModeDir | 0700}, + "identifier/3fYJjSiWhklt7TK9MPVLGk5_8AIU8ostTcFPXULJf_qeAVG6pmC-Cqqu4ZhEXRxn": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "identifier/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "identifier/8sInO_dhPW9kweit7jVuH_vtMkRxlZT7XE4_yEKPZmpGkgVxkmxz0OpXgbMsH6ET": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "identifier/Xys8hPpJsKirZmTTuT7SwQ6661TCp_ZRu8iUjPPD7pC5E3ll7xw4zhNlZ2PVcz_-": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "identifier/k8yyhlBV3JpYzzywAnrRWkfp2TmgIDNC6FqMFwrrR2x0C3unBLveDNKgigOjG9MM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "identifier/mXlNz_v77nW7jstn_rJn4RL7dCQlkakM0y-HjO6tkDubtzaj-wCmW1hIr2Gk64_O": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + + "status": {Mode: fs.ModeDir | 0700}, + "status/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: 0400, Data: []byte("\x00")}, + + "substitute": {Mode: fs.ModeDir | 0700}, + "substitute/socGRnhdsJlJ4Z01ZwHFZdLzFH0rWvoaJrylcUKtvhBdGsE7VoYl-vdUuvfK8B4I": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "substitute/MccYezQ4pshkeBV4w_aAEIwvZdKHL3OAgAkR3ouZ43EF94Ur1HBHaN7njPfJAj50": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "substitute/VV5W0_DiA1iylaqbKFThp1ZpV_y1ccVS1XyqDnDaXoHQ-EEyPI4XFrQHcHOF2hWG": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + + "work": {Mode: fs.ModeDir | 0700}, + }}, + } + checkWithCache(t, testCases) +} + +func TestErrors(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + err error + want string + }{ + {"InvalidLookupError", pkg.InvalidLookupError{ + 0xff, 0xf0, + }, "attempting to look up non-input artifact __AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}, + + {"InvalidArtifactError", pkg.InvalidArtifactError{ + 0xff, 0xfd, + }, "artifact __0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA cannot be cured"}, + + {"ChecksumMismatchError", &pkg.ChecksumMismatchError{ + Want: (pkg.Checksum)(bytes.Repeat([]byte{ + 0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f, + }, 8)), + }, "got AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + " instead of deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, + + {"ResponseStatusError", pkg.ResponseStatusError( + http.StatusNotAcceptable, + ), "the requested URL returned non-OK status: Not Acceptable"}, + + {"DisallowedTypeflagError", pkg.DisallowedTypeflagError( + tar.TypeChar, + ), "disallowed typeflag '3'"}, + + {"InvalidFileModeError", pkg.InvalidFileModeError( + fs.ModeSymlink | 0777, + ), "artifact did not produce a regular file or directory"}, + + {"NoOutputError", pkg.NoOutputError{ + // empty struct + }, "artifact cured successfully but did not produce any output"}, + + {"IRKindError", &pkg.IRKindError{ + Got: pkg.IRKindEnd, + Want: pkg.IRKindIdent, + Ancillary: 0xcafebabe, + }, "got terminator IR value (0xcafebabe) instead of ident"}, + {"IRKindError invalid", &pkg.IRKindError{ + Got: 0xbeef, + Want: pkg.IRKindIdent, + Ancillary: 0xcafe, + }, "got invalid kind 48879 IR value (0xcafe) instead of ident"}, + + {"UnsupportedVariantError", pkg.UnsupportedVariantError( + "rosa", + ), `unsupported variant "rosa"`}, + + {"UnsupportedArchError zero", pkg.UnsupportedArchError(""), + "invalid architecture name"}, + {"UnsupportedArchError", pkg.UnsupportedArchError("riscv64"), + "unsupported architecture riscv64"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := tc.err.Error(); got != tc.want { + t.Errorf("Error: %q, want %q", got, tc.want) + } + }) + } +} + +func TestScrubError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + err pkg.ScrubError + want string + unwrap []error + }{ + {"sample", *newWantScrubError( + fhs.AbsVarLib.Append("cure"), + ), `checksum mismatches: +got vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX instead of _wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + +dangling identifiers: +_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +_v4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +_v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + +errors during scrub: + /var/lib/cure/checksum/__8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: + artifact did not produce a regular file or directory + /var/lib/cure/checksum/invalid: + illegal base64 data at input byte 4 + /var/lib/cure/identifier/_v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: + readlink /var/lib/cure/identifier/_v8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: invalid argument + /var/lib/cure/identifier/invalid: + illegal base64 data at input byte 4 + /var/lib/cure/nonexistent: + illegal base64 data at input byte 8 +`, []error{ + &pkg.ChecksumMismatchError{Got: pkg.MustDecode( + "vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", + ), Want: pkg.Checksum{0xff, 0}}, + + pkg.InvalidFileModeError(fs.ModeSymlink), + base64.CorruptInputError(4), + + &os.PathError{ + Op: "readlink", + Path: fhs.AbsVarLib.Append("cure").Append( + "identifier", + pkg.Encode(pkg.ID{0xfe, 0xff}), + ).String(), + Err: syscall.EINVAL, + }, + + base64.CorruptInputError(4), + base64.CorruptInputError(8), + }}, + + {"full", pkg.ScrubError{ + ChecksumMismatches: []pkg.ChecksumMismatchError{ + {Want: pkg.MustDecode("CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN")}, + }, + DanglingIdentifiers: []pkg.ID{ + (pkg.ID)(bytes.Repeat([]byte{0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f}, 8)), + (pkg.ID)(bytes.Repeat([]byte{0x71, 0xa7, 0xde, 0x6d, 0xa6, 0xde}, 8)), + }, + Errs: map[unique.Handle[string]][]error{ + unique.Make("/proc/nonexistent"): { + stub.UniqueError(0xcafe), + stub.UniqueError(0xbad), + stub.UniqueError(0xff), + }, + }, + }, `checksum mismatches: +got AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA instead of CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN + +dangling identifiers: +deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef +cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe + +errors during scrub: + /proc/nonexistent: + unique error 51966 injected by the test suite + unique error 2989 injected by the test suite + unique error 255 injected by the test suite +`, []error{ + &pkg.ChecksumMismatchError{Want: pkg.MustDecode("CH3AiUrCCcVOjOYLaMKKK1Da78989JtfHeIsxMzWOQFiN4mrCLDYpoDxLWqJWCUN")}, + stub.UniqueError(0xcafe), + stub.UniqueError(0xbad), + stub.UniqueError(0xff), + }}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := tc.err.Error(); got != tc.want { + t.Errorf("Error:\n\n%s\n\nwant\n\n%s", got, tc.want) + } + + if unwrap := tc.err.Unwrap(); !reflect.DeepEqual(unwrap, tc.unwrap) { + t.Errorf("Unwrap: %#v, want %#v", unwrap, tc.unwrap) + } + }) + } +} + +func TestInputError(t *testing.T) { + t.Parallel() + + makeIdent := func(ident ...byte) pkg.Artifact { + var a overrideIdent + copy(a.id[:], ident) + // does not compare equal + a.TrivialArtifact = new(stubArtifact) + return a + } + + testCases := []struct { + name string + err pkg.InputError + want string + unwrap []error + }{ + {"simple", pkg.InputError{ + makeIdent(0xff, 9): stub.UniqueError(0xbad09), + makeIdent(0xff, 0): stub.UniqueError(0xbad00), + makeIdent(0xff, 0xf): stub.UniqueError(0xbad0f), + makeIdent(0xff, 1): stub.UniqueError(0xbad01), + }, `errors curing inputs: + _wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765184 injected by the test suite + _wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765185 injected by the test suite + _wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765193 injected by the test suite + _w8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765199 injected by the test suite`, []error{ + stub.UniqueError(0xbad00), + stub.UniqueError(0xbad01), + stub.UniqueError(0xbad09), + stub.UniqueError(0xbad0f), + }}, + + {"dedup", pkg.InputError{ + makeIdent(0xff, 9): stub.UniqueError(0xbad09), + makeIdent(0xff, 9): stub.UniqueError(0xbad09), + makeIdent(0xff, 9): stub.UniqueError(0xbad09), + makeIdent(0xff, 0): stub.UniqueError(0xbad00), + makeIdent(0xff, 0): stub.UniqueError(0xbad00), + makeIdent(0xff, 1): stub.UniqueError(0xbad01), + }, `errors curing inputs: + _wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765184 injected by the test suite + _wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765185 injected by the test suite + _wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: unique error 765193 injected by the test suite`, []error{ + stub.UniqueError(0xbad00), + stub.UniqueError(0xbad01), + stub.UniqueError(0xbad09), + }}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if got := tc.err.Error(); got != tc.want { + t.Errorf("Error:\n%s\nwant\n%s", got, tc.want) + } + + if unwrap := tc.err.Unwrap(); !reflect.DeepEqual(unwrap, tc.unwrap) { + t.Errorf("Unwrap: %#v, want %#v", unwrap, tc.unwrap) + } + }) + } +} + +// earlyFailureF is a [FloodArtifact] with a large dependency graph resulting in +// a large [DependencyCureError]. +type earlyFailureF int + +func (earlyFailureF) Kind() pkg.Kind { return pkg.KindExec } +func (earlyFailureF) Params(*pkg.IContext) {} +func (earlyFailureF) IsExclusive() bool { return false } + +func (a earlyFailureF) Inputs() []pkg.Artifact { + deps := make([]pkg.Artifact, a) + for i := range deps { + deps[i] = a - 1 + } + return deps +} + +func (a earlyFailureF) Cure(*pkg.FContext) error { + if a != 0 { + panic("unexpected cure on " + strconv.Itoa(int(a))) + } + return stub.UniqueError(0xcafe) +} + +func BenchmarkEarlyDCE(b *testing.B) { + msg := message.New(log.New(os.Stderr, "dce: ", 0)) + msg.SwapVerbose(testing.Verbose()) + + for b.Loop() { + b.StopTimer() + c, err := pkg.Open(b.Context(), msg, check.MustAbs(b.TempDir()), nil) + if err != nil { + b.Fatal(err) + } + b.StartTimer() + _, _, err = c.Cure(earlyFailureF(8)) + b.StopTimer() + if !errors.Is(err, stub.UniqueError(0xcafe)) { + b.Fatalf("Cure: error = %v", err) + } + c.Close() + b.StartTimer() + } +} + +func TestDependencyCureErrorEarly(t *testing.T) { + t.Parallel() + + checkWithCache(t, []cacheTestCase{ + {"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) { + _, _, err := c.Cure(earlyFailureF(8)) + if !errors.Is(err, stub.UniqueError(0xcafe)) { + t.Fatalf("Cure: error = %v", err) + } + }, expectsFS{ + ".": {Mode: fs.ModeDir | 0700}, + "checksum": {Mode: fs.ModeDir | 0700}, + "identifier": {Mode: fs.ModeDir | 0700}, + "substitute": {Mode: fs.ModeDir | 0700}, + "work": {Mode: fs.ModeDir | 0700}, + }}, + }) +} + +func TestOpen(t *testing.T) { + t.Parallel() + + t.Run("nonexistent", func(t *testing.T) { + t.Parallel() + + wantErr := &os.PathError{ + Op: "mkdir", + Path: container.Nonexistent, + Err: syscall.ENOENT, + } + if _, err := pkg.Open( + t.Context(), + message.New(nil), + check.MustAbs(container.Nonexistent), + nil, + ); !reflect.DeepEqual(err, wantErr) { + t.Errorf("Open: error = %#v, want %#v", err, wantErr) + } + }) + + t.Run("permission", func(t *testing.T) { + t.Parallel() + + tempDir := check.MustAbs(t.TempDir()) + if err := os.Chmod(tempDir.String(), 0); err != nil { + t.Fatal(err) + } else { + t.Cleanup(func() { + if err = os.Chmod(tempDir.String(), 0700); err != nil { + t.Fatal(err) + } + }) + } + + wantErr := &os.PathError{ + Op: "mkdir", + Path: tempDir.Append("cache").String(), + Err: syscall.EACCES, + } + if _, err := pkg.Open( + t.Context(), + message.New(nil), + tempDir.Append("cache"), + nil, + ); !reflect.DeepEqual(err, wantErr) { + t.Errorf("Open: error = %#v, want %#v", err, wantErr) + } + }) + + t.Run("dirty", func(t *testing.T) { + t.Parallel() + + tempDir := check.MustAbs(t.TempDir()) + if err := os.MkdirAll(tempDir.Append( + "cache", + "work", + "dirty", + ).String(), 0755); err != nil { + t.Fatal(err) + } + + wantErr := errors.New("work is not empty, scrub likely required") + if _, err := pkg.Open( + t.Context(), + message.New(nil), + tempDir.Append("cache"), + nil, + ); !reflect.DeepEqual(err, wantErr) { + t.Errorf("Open: error = %#v, want %#v", err, wantErr) + } + }) + + t.Run("scratch", func(t *testing.T) { + t.Parallel() + + tempDir := check.MustAbs(t.TempDir()) + if err := os.MkdirAll(tempDir.Append( + "cache", + "scratch", + ).String(), 0755); err != nil { + t.Fatal(err) + } + + wantErr := errors.New("scratch is present, scrub likely required") + if _, err := pkg.Open( + t.Context(), + message.New(nil), + tempDir.Append("cache"), nil, + ); !reflect.DeepEqual(err, wantErr) { + t.Errorf("Open: error = %#v, want %#v", err, wantErr) + } + }) +} + +func TestExtensionRegister(t *testing.T) { + extensionOld := extension + openedOld := opened + t.Cleanup(func() { extension = extensionOld; opened = openedOld }) + extension = "" + opened = false + + t.Run("set", func(t *testing.T) { + t.Cleanup(func() { extension = "" }) + + const want = "rosa" + pkg.SetExtension(want) + if got := pkg.Extension(); got != want { + t.Fatalf("Extension: %q, want %q", got, want) + } + }) + + t.Run("twice", func(t *testing.T) { + t.Cleanup(func() { extension = "" }) + + defer func() { + const wantPanic = "attempting to set extension twice" + if r := recover(); r != wantPanic { + t.Errorf("panic: %#v, want %q", r, wantPanic) + } + }() + pkg.SetExtension("rosa") + pkg.SetExtension("rosa") + }) + + t.Run("invalid", func(t *testing.T) { + defer func() { + var wantPanic = pkg.ErrInvalidExtension + if r := recover(); r != wantPanic { + t.Errorf("panic: %#v, want %#v", r, wantPanic) + } + }() + pkg.SetExtension(" ") + }) + + t.Run("opened", func(t *testing.T) { + t.Cleanup(func() { opened = false }) + + if _, err := pkg.Open( + t.Context(), + message.New(log.Default()), + check.MustAbs(container.Nonexistent), + nil, + ); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Open: error = %v", err) + } + + t.Run("variant", func(t *testing.T) { + defer func() { + const wantPanic = "attempting to set extension after open" + if r := recover(); r != wantPanic { + t.Errorf("panic: %#v, want %q", r, wantPanic) + } + }() + pkg.SetExtension("rosa") + }) + + t.Run("register", func(t *testing.T) { + defer func() { + const wantPanic = "attempting to register after open" + if r := recover(); r != wantPanic { + t.Errorf("panic: %#v, want %q", r, wantPanic) + } + }() + pkg.Register(pkg.KindCustomOffset, nil) + }) + }) + + t.Run("incomplete", func(t *testing.T) { + t.Cleanup(func() { delete(irArtifact, pkg.KindCustomOffset) }) + + defer func() { + const wantPanic = "attempting to open cache with incomplete variant setup" + if r := recover(); r != wantPanic { + t.Errorf("panic: %#v, want %q", r, wantPanic) + } + }() + pkg.Register(pkg.KindCustomOffset, nil) + + t.Cleanup(func() { opened = false }) + _, _ = pkg.Open(nil, nil, nil, nil) + panic("unreachable") + }) + + t.Run("create", func(t *testing.T) { + t.Cleanup(func() { extension = "" }) + const want = "rosa" + pkg.SetExtension(want) + + base := check.MustAbs(t.TempDir()) + t.Cleanup(func() { opened = false }) + if c, err := pkg.Open(t.Context(), nil, base, nil); err != nil { + t.Fatal(err) + } else { + c.Close() + } + + if got, err := os.ReadFile(base.Append("variant").String()); err != nil { + t.Fatal(err) + } else if string(got) != want { + t.Fatalf("variant: %q", string(got)) + } + }) + + t.Run("access", func(t *testing.T) { + base := check.MustAbs(t.TempDir()) + t.Cleanup(func() { opened = false }) + + if err := os.WriteFile(base.Append("variant").String(), nil, 0); err != nil { + t.Fatal(err) + } + + wantErr := &os.PathError{ + Op: "open", + Path: base.Append("variant").String(), + Err: syscall.EACCES, + } + if _, err := pkg.Open( + t.Context(), nil, + base, nil, + ); !reflect.DeepEqual(err, wantErr) { + t.Fatalf("Open: error = %v, want %v", err, wantErr) + } + }) + + t.Run("promote", func(t *testing.T) { + t.Cleanup(func() { extension = "" }) + const want = "rosa" + pkg.SetExtension(want) + + base := check.MustAbs(t.TempDir()) + t.Cleanup(func() { opened = false }) + + variantPath := base.Append("variant") + if err := os.WriteFile(variantPath.String(), nil, 0600); err != nil { + t.Fatal(err) + } + + if _, err := pkg.Open( + t.Context(), nil, + base, + nil, + ); !reflect.DeepEqual(err, pkg.ErrWouldPromote) { + t.Fatalf("Open: error = %v", err) + } + + if p, err := os.ReadFile(variantPath.String()); err != nil { + t.Fatal(err) + } else if len(p) != 0 { + t.Fatalf("variant: %q", string(p)) + } + + if c, err := pkg.Open( + t.Context(), nil, + base, &pkg.CacheAttr{Flags: pkg.CPromoteVariant}, + ); err != nil { + t.Fatalf("Open: error = %v", err) + } else { + c.Close() + } + + if p, err := os.ReadFile(variantPath.String()); err != nil { + t.Fatal(err) + } else if string(p) != want { + t.Fatalf("variant: %q, want %q", string(p), want) + } + }) + + t.Run("open invalid", func(t *testing.T) { + base := check.MustAbs(t.TempDir()) + t.Cleanup(func() { opened = false }) + + variantPath := base.Append("variant") + if err := os.WriteFile(variantPath.String(), make([]byte, 129), 0400); err != nil { + t.Fatal(err) + } + + if _, err := pkg.Open( + t.Context(), nil, + base, nil, + ); !reflect.DeepEqual(err, pkg.ErrInvalidExtension) { + t.Fatalf("Open: error = %v", err) + } + }) + + t.Run("unsupported", func(t *testing.T) { + base := check.MustAbs(t.TempDir()) + t.Cleanup(func() { opened = false }) + + variantPath := base.Append("variant") + if err := os.WriteFile(variantPath.String(), []byte("rosa"), 0400); err != nil { + t.Fatal(err) + } + + if _, err := pkg.Open( + t.Context(), nil, + base, nil, + ); !reflect.DeepEqual(err, pkg.UnsupportedVariantError("rosa")) { + t.Fatalf("Open: error = %v", err) + } + }) +} -- cgit v1.3.1