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" "io/fs"
"log" "log"
"os" "os"
"path/filepath"
"slices" "slices"
"strings" "strings"
"testing" "testing"
"unique" "unique"
"hakurei.app/check"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -236,25 +234,7 @@ func TestClean(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
base := check.MustAbs(t.TempDir()) base := makeBase(t)
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)
}
})
msg := message.New(log.New(os.Stderr, "clean: ", 0)) msg := message.New(log.New(os.Stderr, "clean: ", 0))
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
c, err := pkg.Open(t.Context(), msg, 0, 0, 0, base) 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) got := p.Get().(*Checksum)
defer p.Put(got) defer p.Put(got)
if _, err := os.Stat(c.base.Append( var ok bool
for _, name := range [...]string{
dirIdentifier, dirIdentifier,
ent.Name(), dirSubstitute,
).String()); err != nil { } {
if !errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(c.base.Append(
addErr(dir.Append(ent.Name()), err) 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() seMu.Lock()
se.DanglingStatus = append(se.DanglingStatus, *want) se.DanglingStatus = append(se.DanglingStatus, *want)
seMu.Unlock() seMu.Unlock()
return false
} }
return true return ok
}} }}
} }
wg.Wait() wg.Wait()

View File

@@ -61,6 +61,11 @@ var (
// //
//go:linkname irArtifact hakurei.app/internal/pkg.irArtifact //go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
irArtifact map[pkg.Kind]pkg.IRReadFunc 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 // newRContext returns the address of a new [pkg.RContext] unsafely created for
@@ -434,6 +439,31 @@ const (
checkDestroySubstitutes = 1 << (iota + 32) 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. // checkWithCache runs a slice of cacheTestCase.
func checkWithCache(t *testing.T, testCases []cacheTestCase) { func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper() t.Helper()
@@ -443,25 +473,7 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper() t.Helper()
t.Parallel() t.Parallel()
base := check.MustAbs(t.TempDir()) base := makeBase(t)
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)
}
})
msg := message.New(log.New(os.Stderr, "cache: ", 0)) msg := message.New(log.New(os.Stderr, "cache: ", 0))
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
@@ -536,8 +548,9 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
// destroy empty status directory // destroy empty status directory
if err := syscall.Rmdir(base.Append("status").String()); err != nil { 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) t.Fatal(err)
}
} }
// destroy empty fault directory // destroy empty fault directory
@@ -1406,6 +1419,77 @@ func TestCache(t *testing.T) {
"substitute": {Mode: fs.ModeDir | 0700}, "substitute": {Mode: fs.ModeDir | 0700},
"work": {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) checkWithCache(t, testCases)
} }