aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/pkg/exec_test.go10
-rw-r--r--internal/pkg/pkg.go82
-rw-r--r--internal/pkg/pkg_test.go94
3 files changed, 177 insertions, 9 deletions
diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go
index 558ab39b..b2fad602 100644
--- a/internal/pkg/exec_test.go
+++ b/internal/pkg/exec_test.go
@@ -11,6 +11,7 @@ import (
"os/exec"
"slices"
"testing"
+ "unique"
"hakurei.app/container/check"
"hakurei.app/container/stub"
@@ -74,7 +75,14 @@ func TestExec(t *testing.T) {
return stub.UniqueError(0xcafe)
},
}),
- ), nil, pkg.Checksum{}, errors.Join(stub.UniqueError(0xcafe))},
+ ), nil, pkg.Checksum{}, &pkg.DependencyCureError{
+ {
+ Ident: unique.Make(pkg.ID(pkg.MustDecode(
+ "CWEoJqnSBpWf8uryC2qnIe3O1a_FZWUWZGbiVPsQFGW7pvDHiSwoK3QCU9-uxN87",
+ ))),
+ Err: stub.UniqueError(0xcafe),
+ },
+ }},
{"invalid paths", pkg.NewExec(
"", nil, 0,
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
index e0b50cfc..65bf5e17 100644
--- a/internal/pkg/pkg.go
+++ b/internal/pkg/pkg.go
@@ -394,7 +394,7 @@ type pendingArtifactDep struct {
// Address of result error slice populated during [Cache.Cure], dereferenced
// after acquiring errsMu if curing fails. No additional action is taken,
// [Cache] and its caller are responsible for further error handling.
- errs *[]error
+ errs *DependencyCureError
// Address of mutex synchronising access to errs.
errsMu *sync.Mutex
@@ -1108,6 +1108,76 @@ func (c *Cache) Cure(a Artifact) (
return c.cure(a)
}
+// CureError wraps a non-nil error returned attempting to cure an [Artifact].
+type CureError struct {
+ Ident unique.Handle[ID]
+ Err error
+}
+
+// Unwrap returns the underlying error.
+func (e *CureError) Unwrap() error { return e.Err }
+
+// Error returns the error message from the underlying Err.
+func (e *CureError) Error() string { return e.Err.Error() }
+
+// A DependencyCureError wraps errors returned while curing dependencies.
+type DependencyCureError []*CureError
+
+// sort sorts underlying errors by their identifier.
+func (e *DependencyCureError) sort() {
+ var identBuf [2]ID
+ slices.SortFunc(*e, func(a, b *CureError) int {
+ identBuf[0], identBuf[1] = a.Ident.Value(), b.Ident.Value()
+ return slices.Compare(identBuf[0][:], identBuf[1][:])
+ })
+}
+
+// unwrap recursively expands and deduplicates underlying errors.
+func (e *DependencyCureError) unwrap() DependencyCureError {
+ errs := make(DependencyCureError, 0, len(*e))
+ for _, err := range *e {
+ if _e, ok := err.Err.(*DependencyCureError); ok {
+ errs = append(errs, _e.unwrap()...)
+ continue
+ }
+ errs = append(errs, err)
+ }
+ me := make(map[unique.Handle[ID]]*CureError, len(errs))
+ for _, err := range errs {
+ me[err.Ident] = err
+ }
+ return slices.AppendSeq(
+ make(DependencyCureError, 0, len(me)),
+ maps.Values(me),
+ )
+}
+
+// Unwrap returns a deduplicated slice of underlying errors.
+func (e *DependencyCureError) Unwrap() []error {
+ errs := e.unwrap()
+ errs.sort()
+ _errs := make([]error, len(errs))
+ for i, err := range errs {
+ _errs[i] = err
+ }
+ return _errs
+}
+
+// Error returns a user-facing multiline error message.
+func (e *DependencyCureError) Error() string {
+ errs := e.unwrap()
+ errs.sort()
+ if len(errs) == 0 {
+ return "invalid dependency cure outcome"
+ }
+ var buf strings.Builder
+ buf.WriteString("errors curing dependencies:")
+ for _, err := range errs {
+ buf.WriteString("\n\t" + Encode(err.Ident.Value()) + ": " + err.Error())
+ }
+ return buf.String()
+}
+
// cure implements Cure without checking the full dependency graph.
func (c *Cache) cure(a Artifact) (
pathname *check.Absolute,
@@ -1290,7 +1360,7 @@ func (c *Cache) cure(a Artifact) (
var wg sync.WaitGroup
wg.Add(len(deps))
res := make([]*check.Absolute, len(deps))
- errs := make([]error, 0, len(deps))
+ errs := make(DependencyCureError, 0, len(deps))
var errsMu sync.Mutex
for i, d := range deps {
pending := pendingArtifactDep{d, &res[i], &errs, &errsMu, &wg}
@@ -1306,11 +1376,7 @@ func (c *Cache) cure(a Artifact) (
wg.Wait()
if len(errs) > 0 {
- err = errors.Join(errs...)
- if err == nil {
- // unreachable
- err = syscall.ENOTRECOVERABLE
- }
+ err = &errs
return
}
for i, p := range res {
@@ -1411,7 +1477,7 @@ func (pending *pendingArtifactDep) cure(c *Cache) {
}
pending.errsMu.Lock()
- *pending.errs = append(*pending.errs, err)
+ *pending.errs = append(*pending.errs, &CureError{c.Ident(pending.a), err})
pending.errsMu.Unlock()
}
diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go
index 586385b1..6717869d 100644
--- a/internal/pkg/pkg_test.go
+++ b/internal/pkg/pkg_test.go
@@ -534,6 +534,28 @@ func TestCache(t *testing.T) {
}}}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
0400,
)},
+
+ {"noncomparable error", &stubArtifactF{
+ kind: pkg.KindExec,
+ params: []byte("artifact with dependency returning noncomparable error"),
+ deps: []pkg.Artifact{newStubFile(
+ pkg.KindHTTPGet,
+ pkg.ID{0xff, 3},
+ nil,
+ nil, struct {
+ _ []byte
+ stub.UniqueError
+ }{UniqueError: 0xbad},
+ )},
+ }, nil, pkg.Checksum{}, &pkg.DependencyCureError{
+ {
+ Ident: unique.Make(pkg.ID{0xff, 3}),
+ Err: struct {
+ _ []byte
+ stub.UniqueError
+ }{UniqueError: 0xbad},
+ },
+ }},
})
if c0, err := unsafeOpen(
@@ -1020,6 +1042,78 @@ errors during scrub:
}
}
+func TestDependencyCureError(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ err pkg.DependencyCureError
+ want string
+ unwrap []error
+ }{
+ {"simple", pkg.DependencyCureError{
+ {Ident: unique.Make(pkg.ID{0xff, 9}), Err: stub.UniqueError(0xbad09)},
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ {Ident: unique.Make(pkg.ID{0xff, 0xf}), Err: stub.UniqueError(0xbad0f)},
+ {Ident: unique.Make(pkg.ID{0xff, 1}), Err: stub.UniqueError(0xbad01)},
+ }, `errors curing dependencies:
+ _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{
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 1}), Err: stub.UniqueError(0xbad01)},
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 9}), Err: stub.UniqueError(0xbad09)},
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 0xf}), Err: stub.UniqueError(0xbad0f)},
+ }},
+
+ {"dedup", pkg.DependencyCureError{
+ {Ident: unique.Make(pkg.ID{0xff, 9}), Err: stub.UniqueError(0xbad09)},
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ {Ident: unique.Make(pkg.ID{0xff, 0xfd}), Err: &pkg.DependencyCureError{
+ {Ident: unique.Make(pkg.ID{0xff, 9}), Err: stub.UniqueError(0xbad09)},
+ {Ident: unique.Make(pkg.ID{0xff, 0xc}), Err: &pkg.DependencyCureError{
+ {Ident: unique.Make(pkg.ID{0xff, 0xf}), Err: stub.UniqueError(0xbad0f)},
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ }},
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ }},
+ {Ident: unique.Make(pkg.ID{0xff, 0xff}), Err: &pkg.DependencyCureError{
+ {Ident: unique.Make(pkg.ID{0xff, 9}), Err: stub.UniqueError(0xbad09)},
+ {Ident: unique.Make(pkg.ID{0xff, 0xc}), Err: &pkg.DependencyCureError{
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ }},
+ {Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ }},
+ {Ident: unique.Make(pkg.ID{0xff, 0xf}), Err: stub.UniqueError(0xbad0f)},
+ {Ident: unique.Make(pkg.ID{0xff, 1}), Err: stub.UniqueError(0xbad01)},
+ }, `errors curing dependencies:
+ _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{
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 0}), Err: stub.UniqueError(0xbad00)},
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 1}), Err: stub.UniqueError(0xbad01)},
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 9}), Err: stub.UniqueError(0xbad09)},
+ &pkg.CureError{Ident: unique.Make(pkg.ID{0xff, 0xf}), Err: stub.UniqueError(0xbad0f)},
+ }},
+ }
+ 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)
+ }
+ })
+ }
+}
+
func TestNew(t *testing.T) {
t.Parallel()