diff --git a/internal/pkg/clean_test.go b/internal/pkg/clean_test.go index 1b93185c..9b71a777 100644 --- a/internal/pkg/clean_test.go +++ b/internal/pkg/clean_test.go @@ -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) diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go index 262ff667..d98b3753 100644 --- a/internal/pkg/pkg.go +++ b/internal/pkg/pkg.go @@ -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, - ent.Name(), - ).String()); err != nil { - if !errors.Is(err, os.ErrNotExist) { - addErr(dir.Append(ent.Name()), err) + 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() diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go index a4fb8361..35f858a5 100644 --- a/internal/pkg/pkg_test.go +++ b/internal/pkg/pkg_test.go @@ -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,8 +548,9 @@ 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())) - t.Fatal(err) + if !errors.Is(err, syscall.ENOTEMPTY) { + t.Fatal(err) + } } // destroy empty fault directory @@ -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) }