From 5f571a50bc00192b3148036280f1b0dcccc8efd7 Mon Sep 17 00:00:00 2001 From: Marcel van Lohuizen Date: Fri, 8 May 2026 16:36:32 +0200 Subject: [PATCH] internal/cuetxtar: honor any= in cmpErr (nested @test(err, any, ...)) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmpErr is the @test(err, ...) check used inside @test(eq, {...}) bodies. Previously it ran validateErrProperties directly on val and ignored ea.any, so the any= form was effectively strict at this call site: - it appeared to work whenever cue.Value.Err() coincidentally returned an error matching the supplied code/contains, and - it failed in cases where val's cumulative error did not match but a descendant did. Add an `if ea.any { … }` branch at the top of cmpErr that dispatches to the same findDescendantError helper runErrAssertion uses, so the semantics is uniform across the top-level @test(err, any, ...) site and the nested-eq site. Lift findDescendantError to a free function since neither caller depends on inlineRunner state. Reject pos= with any= for consistency with runErrAssertion. Adds testdata/inline/eq_nested_err_any.txtar, an inline regression test for the nested-eq case. Signed-off-by: Marcel van Lohuizen Change-Id: Ib79c6c8ceb65612ac5a15f069e75d0faf5b586e2 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1236778 Reviewed-by: Daniel Martí Unity-Result: CUE porcuepine TryBot-Result: CUEcueckoo --- internal/cuetxtar/astcmp.go | 14 +++++++ internal/cuetxtar/inline_err.go | 11 +++-- .../testdata/inline/eq_nested_err_any.txtar | 40 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 internal/cuetxtar/testdata/inline/eq_nested_err_any.txtar 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") +}) -- 2.51.2