internal/pkg: deduplicate dependency errors
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 1m51s
Test / Sandbox (race detector) (push) Successful in 2m44s
Test / ShareFS (push) Successful in 3m42s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (push) Successful in 4m43s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 2m44s
All checks were successful
Test / Create distribution (push) Successful in 29s
Test / Sandbox (push) Successful in 1m51s
Test / Sandbox (race detector) (push) Successful in 2m44s
Test / ShareFS (push) Successful in 3m42s
Test / Hpkg (push) Successful in 4m19s
Test / Hakurei (push) Successful in 4m43s
Test / Hakurei (race detector) (push) Successful in 5m18s
Test / Flake checks (push) Successful in 2m44s
This significantly simplifies error reporting for caller. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unique"
|
||||||
|
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/container/stub"
|
"hakurei.app/container/stub"
|
||||||
@@ -74,7 +75,14 @@ func TestExec(t *testing.T) {
|
|||||||
return stub.UniqueError(0xcafe)
|
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(
|
{"invalid paths", pkg.NewExec(
|
||||||
"", nil, 0,
|
"", nil, 0,
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ type pendingArtifactDep struct {
|
|||||||
// Address of result error slice populated during [Cache.Cure], dereferenced
|
// Address of result error slice populated during [Cache.Cure], dereferenced
|
||||||
// after acquiring errsMu if curing fails. No additional action is taken,
|
// after acquiring errsMu if curing fails. No additional action is taken,
|
||||||
// [Cache] and its caller are responsible for further error handling.
|
// [Cache] and its caller are responsible for further error handling.
|
||||||
errs *[]error
|
errs *DependencyCureError
|
||||||
// Address of mutex synchronising access to errs.
|
// Address of mutex synchronising access to errs.
|
||||||
errsMu *sync.Mutex
|
errsMu *sync.Mutex
|
||||||
|
|
||||||
@@ -1108,6 +1108,76 @@ func (c *Cache) Cure(a Artifact) (
|
|||||||
return c.cure(a)
|
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.
|
// cure implements Cure without checking the full dependency graph.
|
||||||
func (c *Cache) cure(a Artifact) (
|
func (c *Cache) cure(a Artifact) (
|
||||||
pathname *check.Absolute,
|
pathname *check.Absolute,
|
||||||
@@ -1290,7 +1360,7 @@ func (c *Cache) cure(a Artifact) (
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(deps))
|
wg.Add(len(deps))
|
||||||
res := make([]*check.Absolute, len(deps))
|
res := make([]*check.Absolute, len(deps))
|
||||||
errs := make([]error, 0, len(deps))
|
errs := make(DependencyCureError, 0, len(deps))
|
||||||
var errsMu sync.Mutex
|
var errsMu sync.Mutex
|
||||||
for i, d := range deps {
|
for i, d := range deps {
|
||||||
pending := pendingArtifactDep{d, &res[i], &errs, &errsMu, &wg}
|
pending := pendingArtifactDep{d, &res[i], &errs, &errsMu, &wg}
|
||||||
@@ -1306,11 +1376,7 @@ func (c *Cache) cure(a Artifact) (
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
err = errors.Join(errs...)
|
err = &errs
|
||||||
if err == nil {
|
|
||||||
// unreachable
|
|
||||||
err = syscall.ENOTRECOVERABLE
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i, p := range res {
|
for i, p := range res {
|
||||||
@@ -1411,7 +1477,7 @@ func (pending *pendingArtifactDep) cure(c *Cache) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pending.errsMu.Lock()
|
pending.errsMu.Lock()
|
||||||
*pending.errs = append(*pending.errs, err)
|
*pending.errs = append(*pending.errs, &CureError{c.Ident(pending.a), err})
|
||||||
pending.errsMu.Unlock()
|
pending.errsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -534,6 +534,28 @@ func TestCache(t *testing.T) {
|
|||||||
}}}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
|
}}}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
|
||||||
0400,
|
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(
|
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) {
|
func TestNew(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user