diff --git a/internal/cuetxtar/astcmp.go b/internal/cuetxtar/astcmp.go index de98b462a..1c507a50c 100644 --- a/internal/cuetxtar/astcmp.go +++ b/internal/cuetxtar/astcmp.go @@ -712,6 +712,20 @@ func cmpFieldAttrs(path cue.Path, expected []*ast.Attribute, child cue.Value) er // Position specs use absolute line numbers (not deltas) since nested @test(err) // attributes have no source line to be relative to. func (c *cmpCtx) cmpErr(path cue.Path, val cue.Value, ea *errArgs) error { + if ea.any { + // @test(err, any, ...): val itself need not carry the error + // directly; the assertion holds when any descendant matches + // the supplied properties (code=, contains=, args=). pos= + // is not supported with any= because positions describe a + // specific source location, not a property of the subtree. + if ea.posSet { + return pathErr(path, "@test(err, any, pos=...): pos= is not supported with any") + } + if !findDescendantError(val, ea) { + return pathErr(path, "expected a descendant error matching code=%v contains=%q, none found", ea.codes, ea.contains) + } + return nil + } if err := checkIsErr(val); err != nil { return pathErr(path, "%v", err) } diff --git a/internal/cuetxtar/inline_err.go b/internal/cuetxtar/inline_err.go index 5d6597d75..c60b5150c 100644 --- a/internal/cuetxtar/inline_err.go +++ b/internal/cuetxtar/inline_err.go @@ -465,7 +465,7 @@ func (r *inlineRunner) runErrAssertion(t testing.TB, path cue.Path, val cue.Valu t.Errorf("path %s: @test(err, any, pos=...): pos= is not supported with any", path) return } - found := r.findDescendantError(val, ea) + found := findDescendantError(val, ea) if !found { t.Errorf("path %s: expected a descendant error with code=%v, none found", path, ea.codes) logHint(t, pa.hint) @@ -1058,8 +1058,11 @@ func checkMsgArgs(t testing.TB, path cue.Path, e cueerrors.Error, expected []str } // findDescendantError walks val looking for any descendant with an error -// matching ea (code=, contains=, args=). Returns true if found. -func (r *inlineRunner) findDescendantError(val cue.Value, ea *errArgs) bool { +// matching ea (code=, contains=, args=). Returns true if found. The check +// is independent of any inlineRunner state, so it is exposed as a free +// function shared by runErrAssertion (top-level @test(err, any, …)) and +// cmpErr (nested @test(err, any, …) inside @test(eq, {...}) bodies). +func findDescendantError(val cue.Value, ea *errArgs) bool { if checkIsErr(val) == nil && ea.validateErrProperties(val) == nil && msgArgsMatch(val.Err(), ea.msgArgs) { @@ -1071,7 +1074,7 @@ func (r *inlineRunner) findDescendantError(val cue.Value, ea *errArgs) bool { return false } for iter.Next() { - if r.findDescendantError(iter.Value(), ea) { + if findDescendantError(iter.Value(), ea) { return true } } diff --git a/internal/cuetxtar/testdata/inline/eq_nested_err_any.txtar b/internal/cuetxtar/testdata/inline/eq_nested_err_any.txtar new file mode 100644 index 000000000..c689232a7 --- /dev/null +++ b/internal/cuetxtar/testdata/inline/eq_nested_err_any.txtar @@ -0,0 +1,40 @@ +# Tests that @test(err, any, code=...) inside an @test(eq, {...}) body is +# satisfied when any descendant of the value carries a matching error, +# even when the field itself is not a direct error and its cumulative +# error code differs from what `any` is asking for. +# +# Without the cmpErr `any` branch, validateErrProperties is run directly +# against val and matches the *combined* error code, so a search for +# `code=eval` on a struct whose first error is a cycle would +# incorrectly fail. +-- in/test.cue -- +// top.y has two children with distinct error codes: `c` is a cycle, +// `e` is an eval-time conflict. The any= form must succeed at locating +// `e` as a descendant matching code=eval. +top: { + y: { + c: top.y.c + e: 1 & 2 + } +} @test(eq, { + y: _|_ @test(err, any, code=eval, contains="conflicting values") +}) +-- out/status.txt -- +update: output passes run +force: identical to update +-- out/update/out/errors.txt -- +[eval] top.y.e: conflicting values 2 and 1: + ./test.cue:7:6 + ./test.cue:7:10 +-- out/update/test.cue -- +// top.y has two children with distinct error codes: `c` is a cycle, +// `e` is an eval-time conflict. The any= form must succeed at locating +// `e` as a descendant matching code=eval. +top: { + y: { + c: top.y.c + e: 1 & 2 + } +} @test(eq, { + y: _|_ @test(err, any, code=eval, contains="conflicting values") +})