internal/pkg: correctly scrub substitute status
All checks were successful
Test / Create distribution (push) Successful in 1m6s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m44s
Test / Hakurei (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m35s
Test / Flake checks (push) Successful in 1m14s

Scrubbing for status predates substitutes. This change fixes scrub handling of substitute status entries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-06-02 15:37:55 +09:00
parent 1490b32387
commit 74c18390b4
3 changed files with 122 additions and 49 deletions

View File

@@ -6,13 +6,11 @@ import (
"io/fs"
"log"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"unique"
"hakurei.app/check"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
@@ -236,25 +234,7 @@ func TestClean(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := check.MustAbs(t.TempDir())
if err := os.Chmod(base.String(), 0700); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
if err != nil {
t.Error(err)
return nil
}
if !d.IsDir() {
return nil
}
return os.Chmod(path, 0700)
}); err != nil {
t.Fatal(err)
}
})
base := makeBase(t)
msg := message.New(log.New(os.Stderr, "clean: ", 0))
msg.SwapVerbose(testing.Verbose())
c, err := pkg.Open(t.Context(), msg, 0, 0, 0, base)

View File

@@ -1107,19 +1107,28 @@ func (c *Cache) Scrub(checks int) error {
got := p.Get().(*Checksum)
defer p.Put(got)
if _, err := os.Stat(c.base.Append(
var ok bool
for _, name := range [...]string{
dirIdentifier,
dirSubstitute,
} {
if _, err := os.Stat(c.base.Append(
name,
ent.Name(),
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
addErr(dir.Append(ent.Name()), err)
}
continue
}
ok = true
}
if !ok {
seMu.Lock()
se.DanglingStatus = append(se.DanglingStatus, *want)
seMu.Unlock()
return false
}
return true
return ok
}}
}
wg.Wait()

View File

@@ -61,6 +61,11 @@ var (
//
//go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
irArtifact map[pkg.Kind]pkg.IRReadFunc
// statusHeader is the header written to all status files in dirStatus.
//
//go:linkname statusHeader hakurei.app/internal/pkg.statusHeader
statusHeader string
)
// newRContext returns the address of a new [pkg.RContext] unsafely created for
@@ -434,6 +439,31 @@ const (
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()
@@ -443,25 +473,7 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper()
t.Parallel()
base := check.MustAbs(t.TempDir())
if err := os.Chmod(base.String(), 0700); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
if err != nil {
t.Error(err)
return nil
}
if !d.IsDir() {
return nil
}
return os.Chmod(path, 0700)
}); err != nil {
t.Fatal(err)
}
})
base := makeBase(t)
msg := message.New(log.New(os.Stderr, "cache: ", 0))
msg.SwapVerbose(testing.Verbose())
@@ -536,9 +548,10 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
// destroy empty status directory
if err := syscall.Rmdir(base.Append("status").String()); err != nil {
t.Error(expectsFrom(base.Append("status").String()))
if !errors.Is(err, syscall.ENOTEMPTY) {
t.Fatal(err)
}
}
// destroy empty fault directory
if err := os.Remove(base.Append("fault").String()); err != nil {
@@ -1406,6 +1419,77 @@ func TestCache(t *testing.T) {
"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},
}, nil},
{"substituted", substituted, base.Append(
"identifier",
pkg.Encode(c.Ident(substituted).Value()),
), expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}, 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/0Jmc-vGnNDlXTD3jxy-4DGxHW-2-2LtLZ9SXaJtDIqu4uyHfDwDbghNBQ2aYRpab": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"identifier/L-A8SK4ZX631eyealbJVH08u5pAVEf2NMk8RmLlxl7BVADkU4hNjWD6pi5H7uL1F": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")},
"identifier/VbVV2dFVosCriHnG9t5vwfW4lbHvzOkV2eYwpGpPJZOgNBc0g1wZAhbdKEweLqmW": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"identifier/aSMwvPAwdsIF9e1spuLyRNEc8aTFA4HRVasoNqGjxdm1laSMs2h2teGsoSzLp_pR": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")},
"status": {Mode: fs.ModeDir | 0700},
"status/0Jmc-vGnNDlXTD3jxy-4DGxHW-2-2LtLZ9SXaJtDIqu4uyHfDwDbghNBQ2aYRpab": {Mode: fs.ModeSymlink | 0777, Data: []byte("dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM")},
"status/VbVV2dFVosCriHnG9t5vwfW4lbHvzOkV2eYwpGpPJZOgNBc0g1wZAhbdKEweLqmW": {Mode: 0400, Data: []byte(statusHeader + "destroyed")},
"status/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: 0400, Data: []byte(statusHeader + "destroyed")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"work": {Mode: fs.ModeDir | 0700},
}},
}
checkWithCache(t, testCases)
}