diff --git a/go.mod b/go.mod index 69b1300d..6e2b1f89 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/golangci/golangci-lint v1.64.8 github.com/matryer/moq v0.7.1 github.com/onsi/ginkgo/v2 v2.32.2 - github.com/onsi/gomega v1.43.0 + github.com/onsi/gomega v1.44.0 google.golang.org/grpc v1.83.2 k8s.io/api v0.32.2 k8s.io/apimachinery v0.32.2 diff --git a/go.sum b/go.sum index 2cd868df..dd473606 100644 --- a/go.sum +++ b/go.sum @@ -329,8 +329,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.32.2 h1:2o6vyFvR6snrJWgRVztC+OwuqqPEMI1UzYl2s2iU7Cg= github.com/onsi/ginkgo/v2 v2.32.2/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.43.0 h1:VlG/1FxqNxhSO+lq/OHBNaaqwiBK/mO8JbVkX9Y+FeU= -github.com/onsi/gomega v1.43.0/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= +github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= diff --git a/vendor/github.com/onsi/gomega/.gitignore b/vendor/github.com/onsi/gomega/.gitignore index 425d0a50..b3003b4e 100644 --- a/vendor/github.com/onsi/gomega/.gitignore +++ b/vendor/github.com/onsi/gomega/.gitignore @@ -4,4 +4,5 @@ .idea gomega.iml TODO -.vscode \ No newline at end of file +.vscode +/.release/ diff --git a/vendor/github.com/onsi/gomega/CHANGELOG.md b/vendor/github.com/onsi/gomega/CHANGELOG.md index 8f1d34e3..07f33258 100644 --- a/vendor/github.com/onsi/gomega/CHANGELOG.md +++ b/vendor/github.com/onsi/gomega/CHANGELOG.md @@ -1,3 +1,31 @@ +## Unreleased + +### Features + +### Fixes + +### Maintenance + +## 1.44.0 + +### Fixes +- `BeNumerically` compares signed and unsigned integers by value: `-1` no longer equals `uint64(math.MaxUint64)` and `uint(5)` is now greater than `-3` (#925) [26e3c6b] +- `BeNumerically("~")` no longer overflows when computing the distance between extreme integers (#928) [1955764] +- `BeNumerically("==", x, threshold)` now honors the threshold for floats, as it already did for integers (#927) [10e2aca] +- `HaveKeyWithValue` succeeds if any key accepted by the key matcher has a matching value, rather than depending on map iteration order (#929) [ffc577a] +- `HaveKey` and `HaveKeyWithValue` treat key and value matcher errors like `ContainElement` does: a match wins, and an error is only reported when nothing matches (#926) [dc91598] +- `MatchJSON` no longer treats numbers too large for a float64 as equal to one another (#930) [9d619a5] +- `MatchJSON` compares integers beyond ±2^53 exactly, so neighboring large integers (e.g. IDs) no longer match; all other numbers are still compared as float64s (#931) [8ef1aa7, 630fe12] +- `HaveExactElements` reports missing or extra elements that start at index 0, and reports the first extra element's index rather than the last (#934) [af1b777] +- `MatchYAML` compares every document in a multi-document stream rather than only the first; empty documents (e.g. a leading or trailing `---`) are ignored (#933) [2773796] +- `MatchXML` ignores namespace prefixes: elements and attributes are compared by namespace URI, and the URIs declared on each element must match whatever prefix they are bound to (#932) [c0dbd89, 2565350] + +## 1.43.1 + +### Maintenance +- Update go.yaml.in/yaml/v3 to v3.0.5 [d547015] +- Releases are now cut by a GitHub Actions workflow (Actions -> Release -> Run workflow) rather than by hand, with changelog entries collected under `## Unreleased` as the work happens. Releases still ship the stripped-down tree on `master-lite` - tests removed and Ginkgo dropped from `go.mod` - and the workflow now builds it and checks it on every push. See RELEASING.md. [e9dc84d] + ## 1.43.0 ### Features diff --git a/vendor/github.com/onsi/gomega/README.md b/vendor/github.com/onsi/gomega/README.md index 6eb36fdf..ac545907 100644 --- a/vendor/github.com/onsi/gomega/README.md +++ b/vendor/github.com/onsi/gomega/README.md @@ -1,6 +1,9 @@ -![Gomega: Ginkgo's Preferred Matcher Library](http://onsi.github.io/gomega/images/gomega.png) + + + Gomega: Ginkgo's Preferred Matcher Library + -[![test](https://github.com/onsi/gomega/actions/workflows/test.yml/badge.svg)](https://github.com/onsi/gomega/actions/workflows/test.yml) +[![test](https://github.com/onsi/gomega/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/onsi/gomega/actions/workflows/test.yml?query=branch%3Amaster) [![Gomega Docs](docs/images/docs-badge.svg)](https://onsi.github.io/gomega/) [![Sponsor onsi](docs/images/sponsor-badge.svg)](https://github.com/sponsors/onsi) Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided-matchers). diff --git a/vendor/github.com/onsi/gomega/RELEASING.md b/vendor/github.com/onsi/gomega/RELEASING.md index 9973fff4..fdbeb37c 100644 --- a/vendor/github.com/onsi/gomega/RELEASING.md +++ b/vendor/github.com/onsi/gomega/RELEASING.md @@ -1,23 +1,143 @@ -A Gomega release is a tagged sha and a GitHub release. To cut a release: - -1. Ensure CHANGELOG.md is up to date. - - Use - ```bash - LAST_VERSION=$(git tag --sort=version:refname | tail -n1) - CHANGES=$(git log --pretty=format:'- %s [%h]' HEAD...$LAST_VERSION) - echo -e "## NEXT\n\n$CHANGES\n\n### Features\n\n### Fixes\n\n### Maintenance\n\n$(cat CHANGELOG.md)" > CHANGELOG.md - ``` - to update the changelog - - Categorize the changes into - - Breaking Changes (requires a major version) - - New Features (minor version) - - Fixes (fix version) - - Maintenance (which in general should not be mentioned in `CHANGELOG.md` as they have no user impact) -1. Update GOMEGA_VERSION in `gomega_dsl.go` -1. Commit, push, and release: - ``` - git commit -m "vM.m.p" - git push - gh release create "vM.m.p" - git fetch --tags origin master - ``` \ No newline at end of file +# Releasing Gomega + +A release is one GitHub Actions run: **Actions → Release → Run workflow → bump: patch | minor**, +from `master`. From the command line: + +```bash +gh workflow run release.yml -f bump=patch # or -f bump=minor +``` + +The run: + +1. Runs the whole test workflow (`test.yml`). Nothing else happens unless it passes. +2. Runs `scripts/release.sh`, which + - computes the next version from `GOMEGA_VERSION` in `gomega_dsl.go`, + - renames `## Unreleased` in `CHANGELOG.md` to `## X.Y.Z` (dropping empty `###` subsections) and + puts a fresh, empty `## Unreleased` above it, + - writes `GOMEGA_VERSION` and stamps the version into every plugin manifest + (`plugins/*/.claude-plugin/plugin.json`), so the Claude Code plugin ships in lockstep, + - commits that on `master` as `vX.Y.Z (full)`, as `github-actions[bot]`, + - builds the released tree on `master-lite` and tags **that** commit `vX.Y.Z` (see below), + - pushes `master`, `master-lite`, and the tag together (`git push --atomic`, fast-forward only), + - creates the GitHub release `vX.Y.Z`, whose notes are the body of `## X.Y.Z`. + +## master and master-lite + +Gomega uses Ginkgo for its own tests, and the go toolchain pulls a dependency's *test* dependencies +into consuming projects as indirect requirements — so a project that uses only Gomega would end up +with all of Ginkgo in its `go.mod`. Since 1.40.0, releases therefore ship a stripped-down tree: + +- `master` holds the real repository, tests and all. The release commit there is `vX.Y.Z (full)`, + and it is **not** what the go toolchain resolves. +- `master-lite` holds the released trees: the same tree with every `_test.go` file deleted and + `go.mod` re-tidied, which drops Ginkgo. `scripts/strip-tests.sh` does that, and asserts that the + result still builds and no longer requires Ginkgo. +- Each release adds one commit to `master-lite`: a merge whose tree is the stripped tree and whose + parents are the previous `master-lite` commit and the `vX.Y.Z (full)` commit on `master`. So + `master-lite`'s history connects to master's, and `git log master-lite` reads as the release + history. +- The `vX.Y.Z` tag is on that `master-lite` commit. It is what `go get github.com/onsi/gomega` + resolves, so `go build` never even sees the test files. + +The test workflow's **Check the released (lite) tree** job runs `scripts/strip-tests.sh` on every +push. If a non-test file ever imports Ginkgo, that job fails long before release day — the released +module could not be built otherwise. + +Release tags are not on `master`, so `git describe` on master does not find them. `GOMEGA_VERSION` +in `gomega_dsl.go` is the version of record, and `git tag --sort=version:refname | tail -n1` names +the latest release. + +If this release process ever causes unexpected changes for a project, please open an issue. + +### Never merge master-lite into master + +`master-lite` always looks ahead of `master`, because its newest commit merges master's release +commit. But its *tree* has the tests taken out, so merging or rebasing it back into master deletes +every `_test.go` file and drops Ginkgo from `go.mod` — cleanly, with no conflicts. GitHub's +"master-lite had recent pushes — Compare & pull request" banner is the one click to avoid. + +Nothing you do locally needs `master-lite`: the release workflow writes it from a fresh checkout. A +stale local `master-lite` branch is harmless, and you can delete it. + +`scripts/check-full-tree.sh` guards against a stripped master: it asserts that a checkout has +`_test.go` files and that `go.mod` requires Ginkgo. The test workflow runs it on every push and +`scripts/release.sh` runs it before cutting a release, so a stripped master gets reported plainly +instead of turning into a test run with nothing in it. + +### Cutting one release after another + +Work on master as usual between releases. The `vX.Y.Z (full)` commit the workflow pushed is an +ordinary commit: `git pull` fast-forwards onto it, it carries the released `CHANGELOG.md` and +`GOMEGA_VERSION`, and you add new `## Unreleased` entries above it. The next release repeats the +same two steps — a new `vX.Y.Z (full)` commit on master, and a new merge on `master-lite` whose +tree is the stripped version of it, carrying the tag. + +Nothing accumulates between releases, and nothing needs reconciling in either direction: each +`master-lite` commit's tree is built from the release commit rather than merged into the previous +one, so `git diff vX.Y.Z vX.Y.Z+1` over two releases shows only the real changes. + +## The changelog + +`CHANGELOG.md` starts with an `## Unreleased` section: + +```markdown +## Unreleased + +### Features + +### Fixes + +### Maintenance + +## 1.43.0 +... +``` + +Add an entry under `## Unreleased` with each user-facing change, as you make it. Never edit a +released section. Empty subsections are dropped when the section is released, so leaving +`### Maintenance` empty costs nothing. + +If `## Unreleased` has no entries (only blank lines and `###` headings), the release stops before +changing anything — and so does a re-run after a successful release. + +To draft entries from the commits since the last release: + +```bash +git log --pretty=format:'- %s [%h]' "$(git tag --sort=version:refname | tail -n1)"..HEAD +``` + +Categorize what you keep into + +- Breaking changes (these need a major version, which this workflow does not cut) +- `### Features` (minor version) +- `### Fixes` (patch version) +- `### Maintenance` (in general, changes with no user impact don't belong in the changelog at all) + +## If a release fails + +Open the failed run and click **Re-run failed jobs**. A re-run checks out the same commit as the +first attempt and computes the same version. It resumes based on what already exists: + +| Where it failed | What a re-run does | +|---|---| +| Tests, or before the push | Nothing reached GitHub. The re-run starts over. | +| The push (master or master-lite moved during the run) | The re-run fails the same way. Start a new run from the current master. | +| After the push (the tag `vX.Y.Z` is on GitHub) | The re-run builds from the tag instead of making new commits. | +| Creating the GitHub release | The release is created, or its notes are refreshed if it already exists. | + +Both branches and the tag are pushed together, so the tag being on GitHub means both release +commits are there too. + +## Trying the release script locally + +`GOMEGA_RELEASE_DRY_RUN=1 scripts/release.sh patch` does the whole release locally — the changelog +and version rewrites, the `master` commit, the stripped `master-lite` commit and the tag on it — +then skips the push and the GitHub release. It still checks `origin` for an existing tag and needs +`origin/master-lite`, so run it in a throwaway clone whose `origin` is a local bare repository, not +in your working copy. + +`scripts/strip-tests.sh` rewrites the working tree in place, so run *it* only on a throwaway +checkout too. + +The version and changelog logic lives in `scripts/release`, with its own Ginkgo suite +(`scripts/release/release_test.go`) that runs with the rest of the suites. diff --git a/vendor/github.com/onsi/gomega/gomega_dsl.go b/vendor/github.com/onsi/gomega/gomega_dsl.go index 3206e05d..cf4eaefa 100644 --- a/vendor/github.com/onsi/gomega/gomega_dsl.go +++ b/vendor/github.com/onsi/gomega/gomega_dsl.go @@ -22,7 +22,7 @@ import ( "github.com/onsi/gomega/types" ) -const GOMEGA_VERSION = "1.43.0" +const GOMEGA_VERSION = "1.44.0" const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler. If you're using Ginkgo then you probably forgot to put your assertion in an It(). diff --git a/vendor/github.com/onsi/gomega/matchers.go b/vendor/github.com/onsi/gomega/matchers.go index bf572260..e3e01f66 100644 --- a/vendor/github.com/onsi/gomega/matchers.go +++ b/vendor/github.com/onsi/gomega/matchers.go @@ -291,6 +291,8 @@ func HaveSuffix(suffix string, args ...any) types.GomegaMatcher { // MatchJSON succeeds if actual is a string or stringer of JSON that matches // the expected JSON. The JSONs are decoded and the resulting objects are compared via // reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. +// Numbers are compared as float64s, except for integers too large to be represented +// exactly by a float64 (beyond ±2^53), which are compared exactly. func MatchJSON(json any) types.GomegaMatcher { return &matchers.MatchJSONMatcher{ JSONToMatch: json, @@ -300,6 +302,8 @@ func MatchJSON(json any) types.GomegaMatcher { // MatchXML succeeds if actual is a string or stringer of XML that matches // the expected XML. The XMLs are decoded and the resulting objects are compared via // reflect.DeepEqual so things like whitespaces shouldn't matter. +// Namespace prefixes are ignored: element and attribute names are compared by +// namespace URI, and the namespace URIs declared on each element must match. func MatchXML(xml any) types.GomegaMatcher { return &matchers.MatchXMLMatcher{ XMLToMatch: xml, @@ -309,6 +313,8 @@ func MatchXML(xml any) types.GomegaMatcher { // MatchYAML succeeds if actual is a string or stringer of YAML that matches // the expected YAML. The YAML's are decoded and the resulting objects are compared via // reflect.DeepEqual so things like key-ordering and whitespace shouldn't matter. +// Multi-document YAML streams are compared document by document; empty documents +// (e.g. from a leading or trailing "---") are ignored. func MatchYAML(yaml any) types.GomegaMatcher { return &matchers.MatchYAMLMatcher{ YAMLToMatch: yaml, diff --git a/vendor/github.com/onsi/gomega/matchers/attributes_slice.go b/vendor/github.com/onsi/gomega/matchers/attributes_slice.go index 355b362f..ea8e5ba1 100644 --- a/vendor/github.com/onsi/gomega/matchers/attributes_slice.go +++ b/vendor/github.com/onsi/gomega/matchers/attributes_slice.go @@ -9,6 +9,12 @@ type attributesSlice []xml.Attr func (attrs attributesSlice) Len() int { return len(attrs) } func (attrs attributesSlice) Less(i, j int) bool { - return strings.Compare(attrs[i].Name.Local, attrs[j].Name.Local) == -1 + if attrs[i].Name.Local != attrs[j].Name.Local { + return strings.Compare(attrs[i].Name.Local, attrs[j].Name.Local) == -1 + } + if attrs[i].Name.Space != attrs[j].Name.Space { + return strings.Compare(attrs[i].Name.Space, attrs[j].Name.Space) == -1 + } + return strings.Compare(attrs[i].Value, attrs[j].Value) == -1 } func (attrs attributesSlice) Swap(i, j int) { attrs[i], attrs[j] = attrs[j], attrs[i] } diff --git a/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go b/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go index 7e6ce154..02839f34 100644 --- a/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/be_numerically_matcher.go @@ -5,6 +5,7 @@ package matchers import ( "fmt" "math" + "math/big" "github.com/onsi/gomega/format" ) @@ -60,18 +61,12 @@ func (matcher *BeNumericallyMatcher) Match(actual any) (success bool, err error) secondOperand = toFloat(matcher.CompareTo[1]) } success = matcher.matchFloats(toFloat(actual), toFloat(matcher.CompareTo[0]), secondOperand) - } else if isInteger(actual) { - var secondOperand int64 = 0 + } else if isInteger(actual) || isUnsignedInteger(actual) { + var threshold any = 0 if len(matcher.CompareTo) == 2 { - secondOperand = toInteger(matcher.CompareTo[1]) + threshold = matcher.CompareTo[1] } - success = matcher.matchIntegers(toInteger(actual), toInteger(matcher.CompareTo[0]), secondOperand) - } else if isUnsignedInteger(actual) { - var secondOperand uint64 = 0 - if len(matcher.CompareTo) == 2 { - secondOperand = toUnsignedInteger(matcher.CompareTo[1]) - } - success = matcher.matchUnsignedIntegers(toUnsignedInteger(actual), toUnsignedInteger(matcher.CompareTo[0]), secondOperand) + success = matcher.matchIntegers(toBigInt(actual), toBigInt(matcher.CompareTo[0]), threshold) } else { return false, fmt.Errorf("Failed to compare:\n%s\n%s:\n%s", format.Object(actual, 1), matcher.Comparator, format.Object(matcher.CompareTo[0], 1)) } @@ -79,40 +74,35 @@ func (matcher *BeNumericallyMatcher) Match(actual any) (success bool, err error) return success, nil } -func (matcher *BeNumericallyMatcher) matchIntegers(actual, compareTo, threshold int64) (success bool) { +// matchIntegers compares signed and unsigned integers by their exact mathematical value, using math/big so +// that neither mixing signedness nor computing the distance between the two values can overflow +func (matcher *BeNumericallyMatcher) matchIntegers(actual, compareTo *big.Int, threshold any) (success bool) { switch matcher.Comparator { case "==", "~": - diff := actual - compareTo - return -threshold <= diff && diff <= threshold + distance := new(big.Int).Sub(actual, compareTo) + return isWithinThreshold(distance.Abs(distance), threshold) case ">": - return (actual > compareTo) + return actual.Cmp(compareTo) > 0 case ">=": - return (actual >= compareTo) + return actual.Cmp(compareTo) >= 0 case "<": - return (actual < compareTo) + return actual.Cmp(compareTo) < 0 case "<=": - return (actual <= compareTo) + return actual.Cmp(compareTo) <= 0 } return false } -func (matcher *BeNumericallyMatcher) matchUnsignedIntegers(actual, compareTo, threshold uint64) (success bool) { - switch matcher.Comparator { - case "==", "~": - if actual < compareTo { - actual, compareTo = compareTo, actual +// isWithinThreshold reports whether the (non-negative) distance is no greater than the threshold, which may be any number +func isWithinThreshold(distance *big.Int, threshold any) bool { + if isFloat(threshold) { + t := toFloat(threshold) + if math.IsNaN(t) { + return false } - return actual-compareTo <= threshold - case ">": - return (actual > compareTo) - case ">=": - return (actual >= compareTo) - case "<": - return (actual < compareTo) - case "<=": - return (actual <= compareTo) + return new(big.Float).SetInt(distance).Cmp(big.NewFloat(t)) <= 0 } - return false + return distance.Cmp(toBigInt(threshold)) <= 0 } func (matcher *BeNumericallyMatcher) matchFloats(actual, compareTo, threshold float64) (success bool) { @@ -120,6 +110,10 @@ func (matcher *BeNumericallyMatcher) matchFloats(actual, compareTo, threshold fl case "~": return math.Abs(actual-compareTo) <= threshold case "==": + // an explicit threshold is honored, as it is for integers; without one == means exact equality + if len(matcher.CompareTo) == 2 { + return math.Abs(actual-compareTo) <= threshold + } return (actual == compareTo) case ">": return (actual > compareTo) diff --git a/vendor/github.com/onsi/gomega/matchers/have_exact_elements.go b/vendor/github.com/onsi/gomega/matchers/have_exact_elements.go index 8b2d297c..18f7b284 100644 --- a/vendor/github.com/onsi/gomega/matchers/have_exact_elements.go +++ b/vendor/github.com/onsi/gomega/matchers/have_exact_elements.go @@ -79,7 +79,7 @@ func (matcher *HaveExactElementsMatcher) Match(actual any) (success bool, err er if i >= lenMatchers { matcher.extraIndex = i success = false - continue + return } if i >= lenValues { @@ -110,10 +110,10 @@ func (matcher *HaveExactElementsMatcher) Match(actual any) (success bool, err er func (matcher *HaveExactElementsMatcher) FailureMessage(actual any) (message string) { message = format.Message(actual, "to have exact elements with", presentable(matcher.Elements)) - if matcher.missingIndex > 0 { + if matcher.missingIndex >= 0 { message = fmt.Sprintf("%s\nthe missing elements start from index %d", message, matcher.missingIndex) } - if matcher.extraIndex > 0 { + if matcher.extraIndex >= 0 { message = fmt.Sprintf("%s\nthe extra elements start from index %d", message, matcher.extraIndex) } if len(matcher.mismatchFailures) != 0 { @@ -131,6 +131,7 @@ func (matcher *HaveExactElementsMatcher) NegatedFailureMessage(actual any) (mess func (matcher *HaveExactElementsMatcher) resetState() { matcher.mismatchFailures = nil - matcher.missingIndex = 0 - matcher.extraIndex = 0 + // -1 means "no missing/extra elements"; 0 is a valid starting index + matcher.missingIndex = -1 + matcher.extraIndex = -1 } diff --git a/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go index 16630c18..5b27babe 100644 --- a/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/have_key_matcher.go @@ -24,31 +24,41 @@ func (matcher *HaveKeyMatcher) Match(actual any) (success bool, err error) { keyMatcher = &EqualMatcher{Expected: matcher.Key} } + // As with ContainElement, a key the key matcher accepts wins regardless of + // errors on other keys; the matcher's (last) error is only reported when + // no key matches. + var lastError error if miter.IsSeq2(actual) { - var success bool - var err error + found := false miter.IterateKV(actual, func(k, v reflect.Value) bool { - success, err = keyMatcher.Match(k.Interface()) + success, err := keyMatcher.Match(k.Interface()) if err != nil { - err = fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) - return false + lastError = err + return true } - return !success + found = success + return !found }) - return success, err - } - - keys := reflect.ValueOf(actual).MapKeys() - for i := range keys { - success, err := keyMatcher.Match(keys[i].Interface()) - if err != nil { - return false, fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) - } - if success { + if found { return true, nil } + } else { + keys := reflect.ValueOf(actual).MapKeys() + for i := range keys { + success, err := keyMatcher.Match(keys[i].Interface()) + if err != nil { + lastError = err + continue + } + if success { + return true, nil + } + } } + if lastError != nil { + return false, fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, lastError.Error()) + } return false, nil } diff --git a/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go index 0cd70815..29410a5f 100644 --- a/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/have_key_with_value_matcher.go @@ -30,44 +30,46 @@ func (matcher *HaveKeyWithValueMatcher) Match(actual any) (success bool, err err valueMatcher = &EqualMatcher{Expected: matcher.Value} } + // As with ContainElement, an entry whose key and value both match wins + // regardless of errors on other entries; the (last) key or value matcher + // error is only reported when no entry matches. + var lastError error + entryMatches := func(k, v reflect.Value) bool { + success, err := keyMatcher.Match(k.Interface()) + if err != nil { + lastError = fmt.Errorf("HaveKeyWithValue's key matcher failed with:\n%s%s", format.Indent, err.Error()) + return false + } + if !success { + return false + } + success, err = valueMatcher.Match(v.Interface()) + if err != nil { + lastError = fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error()) + return false + } + return success + } + if miter.IsSeq2(actual) { - var success bool - var err error + found := false miter.IterateKV(actual, func(k, v reflect.Value) bool { - success, err = keyMatcher.Match(k.Interface()) - if err != nil { - err = fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) - return false - } - if success { - success, err = valueMatcher.Match(v.Interface()) - if err != nil { - err = fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error()) - return false - } - } - return !success + found = entryMatches(k, v) + return !found }) - return success, err - } - - keys := reflect.ValueOf(actual).MapKeys() - for i := range keys { - success, err := keyMatcher.Match(keys[i].Interface()) - if err != nil { - return false, fmt.Errorf("HaveKeyWithValue's key matcher failed with:\n%s%s", format.Indent, err.Error()) + if found { + return true, nil } - if success { - actualValue := reflect.ValueOf(actual).MapIndex(keys[i]) - success, err := valueMatcher.Match(actualValue.Interface()) - if err != nil { - return false, fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error()) + } else { + value := reflect.ValueOf(actual) + for _, k := range value.MapKeys() { + if entryMatches(k, value.MapIndex(k)) { + return true, nil } - return success, nil } } - return false, nil + return false, lastError } func (matcher *HaveKeyWithValueMatcher) FailureMessage(actual any) (message string) { diff --git a/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go index 331f289a..96f5246f 100644 --- a/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/match_json_matcher.go @@ -4,6 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + "math/big" + "strconv" + "strings" "github.com/onsi/gomega/format" ) @@ -22,14 +25,103 @@ func (matcher *MatchJSONMatcher) Match(actual any) (success bool, err error) { var aval any var eval any - // this is guarded by prettyPrint - json.Unmarshal([]byte(actualString), &aval) - json.Unmarshal([]byte(expectedString), &eval) + // prettyPrint has already checked the syntax, so decoding is not expected to fail + if aval, err = decodeJSON(actualString); err != nil { + return false, fmt.Errorf("Actual '%s' should be valid JSON, but it is not.\nUnderlying error:%s", actualString, err) + } + if eval, err = decodeJSON(expectedString); err != nil { + return false, fmt.Errorf("Expected '%s' should be valid JSON, but it is not.\nUnderlying error:%s", expectedString, err) + } var equal bool equal, matcher.firstFailurePath = deepEqual(aval, eval) return equal, nil } +// decodeJSON decodes s as json.Unmarshal would into an any, except that +// integers too large to be represented exactly by a float64 are decoded as +// canonicalJSONNumbers, so that they are compared exactly rather than after +// rounding to the nearest float64. +func decodeJSON(s string) (any, error) { + decoder := json.NewDecoder(strings.NewReader(s)) + decoder.UseNumber() + var value any + if err := decoder.Decode(&value); err != nil { + return nil, err + } + return decodeJSONNumbers(value), nil +} + +func decodeJSONNumbers(value any) any { + switch v := value.(type) { + case []any: + for i, element := range v { + v[i] = decodeJSONNumbers(element) + } + case map[string]any: + for key, element := range v { + v[key] = decodeJSONNumbers(element) + } + case json.Number: + return decodeJSONNumber(v) + } + return value +} + +// canonicalJSONNumber is a JSON number written as its significant digits +// followed by a base-10 exponent, e.g. 12345678901234567890 and +// 1.234567890123456789e19 are both 123456789012345678900e1. Two JSON numbers +// have the same value exactly when they have the same canonical form. +type canonicalJSONNumber string + +// maxExactFloat64Integer is 2^53: every integer with a magnitude no greater +// than this is represented exactly by a float64. +var maxExactFloat64Integer = new(big.Int).Lsh(big.NewInt(1), 53) + +// decodeJSONNumber returns integers whose magnitude exceeds 2^53 in canonical +// form, and every other number as the float64 that json.Unmarshal would +// produce. Numbers that are not integers but are too large for a float64 are +// also returned in canonical form. +func decodeJSONNumber(n json.Number) any { + s, sign := string(n), "" + if rest, negative := strings.CutPrefix(s, "-"); negative { + s, sign = rest, "-" + } + mantissa, exponentString, _ := strings.Cut(strings.ToLower(s), "e") + integerPart, fractionPart, _ := strings.Cut(mantissa, ".") + + digits := strings.TrimLeft(integerPart+fractionPart, "0") + significantDigits := strings.TrimRight(digits, "0") + + // the exponent is parsed as a big.Int as JSON puts no limit on its size + exponent := new(big.Int) + if exponentString != "" { + exponent.SetString(exponentString, 10) // the syntax has been checked by the decoder + } + exponent.Add(exponent, big.NewInt(int64(len(digits)-len(significantDigits)-len(fractionPart)))) + canonical := canonicalJSONNumber(sign + significantDigits + "e" + exponent.String()) + + if significantDigits != "" && exponent.Sign() >= 0 && exceedsMaxExactFloat64Integer(significantDigits, exponent) { + return canonical + } + f, err := strconv.ParseFloat(string(n), 64) + if err != nil { + // the syntax has been checked, so the number must be too large for a float64 + return canonical + } + return f +} + +// exceedsMaxExactFloat64Integer reports whether significantDigits * 10^exponent, +// with exponent >= 0, is greater than 2^53. +func exceedsMaxExactFloat64Integer(significantDigits string, exponent *big.Int) bool { + if exponent.Cmp(big.NewInt(16)) >= 0 { + return true // 10^16 > 2^53 + } + value, _ := new(big.Int).SetString(significantDigits, 10) + value.Mul(value, new(big.Int).Exp(big.NewInt(10), exponent, nil)) + return value.Cmp(maxExactFloat64Integer) > 0 +} + func (matcher *MatchJSONMatcher) FailureMessage(actual any) (message string) { actualString, expectedString, _ := matcher.prettyPrint(actual) return formattedMessage(format.Message(actualString, "to match JSON of", expectedString), matcher.firstFailurePath) diff --git a/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go index f7dcaf6f..86bf2f2d 100644 --- a/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/match_xml_matcher.go @@ -83,9 +83,9 @@ func parseXmlContent(content string) (*xmlNode, error) { switch tok := tok.(type) { case xml.StartElement: - attrs := attributesSlice(tok.Attr) + attrs := withNormalizedNamespaceDeclarations(tok.Attr) sort.Sort(attrs) - allNodes = append(allNodes, &xmlNode{XMLName: tok.Name, XMLAttr: tok.Attr}) + allNodes = append(allNodes, &xmlNode{XMLName: tok.Name, XMLAttr: attrs}) case xml.EndElement: if len(allNodes) > 1 { allNodes[lastNodeIndex-1].Nodes = append(allNodes[lastNodeIndex-1].Nodes, lastNode) @@ -109,6 +109,22 @@ func parseXmlContent(content string) (*xmlNode, error) { return firstNode, nil } +// withNormalizedNamespaceDeclarations replaces the name of each xmlns="..." and +// xmlns:prefix="..." attribute with the same placeholder name, so that only the +// namespace URIs declared on the element are compared, not the prefixes bound +// to them. The decoder has already resolved element and attribute names to +// namespace URIs, so the prefixes play no other part in the comparison. +func withNormalizedNamespaceDeclarations(attrs []xml.Attr) attributesSlice { + normalized := make(attributesSlice, len(attrs)) + for i, attr := range attrs { + if attr.Name.Space == "xmlns" || (attr.Name.Space == "" && attr.Name.Local == "xmlns") { + attr.Name = xml.Name{Space: "xmlns"} + } + normalized[i] = attr + } + return normalized +} + func newXmlDecoder(reader io.Reader) *xml.Decoder { dec := xml.NewDecoder(reader) dec.CharsetReader = charset.NewReaderLabel diff --git a/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go b/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go index c3da9bd4..a6c558d9 100644 --- a/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go @@ -1,7 +1,9 @@ package matchers import ( + "errors" "fmt" + "io" "strings" "github.com/onsi/gomega/format" @@ -11,37 +13,60 @@ import ( type MatchYAMLMatcher struct { YAMLToMatch any firstFailurePath []any + // the 1-based number of the first mismatched document when comparing + // multi-document streams; 0 otherwise + firstFailureDocument int } func (matcher *MatchYAMLMatcher) Match(actual any) (success bool, err error) { + matcher.firstFailurePath = nil + matcher.firstFailureDocument = 0 + actualString, expectedString, err := matcher.toStrings(actual) if err != nil { return false, err } - var aval any - var eval any - - if err := yaml.Unmarshal([]byte(actualString), &aval); err != nil { + adocs, err := decodeYAMLDocuments(actualString) + if err != nil { return false, fmt.Errorf("Actual '%s' should be valid YAML, but it is not.\nUnderlying error:%s", actualString, err) } - if err := yaml.Unmarshal([]byte(expectedString), &eval); err != nil { + edocs, err := decodeYAMLDocuments(expectedString) + if err != nil { return false, fmt.Errorf("Expected '%s' should be valid YAML, but it is not.\nUnderlying error:%s", expectedString, err) } - var equal bool - equal, matcher.firstFailurePath = deepEqual(aval, eval) - return equal, nil + if len(adocs) != len(edocs) { + return false, nil + } + for i := range adocs { + var equal bool + equal, matcher.firstFailurePath = deepEqual(adocs[i], edocs[i]) + if !equal { + if len(adocs) > 1 { + matcher.firstFailureDocument = i + 1 + } + return false, nil + } + } + return true, nil } func (matcher *MatchYAMLMatcher) FailureMessage(actual any) (message string) { actualString, expectedString, _ := matcher.toNormalisedStrings(actual) - return formattedMessage(format.Message(actualString, "to match YAML of", expectedString), matcher.firstFailurePath) + return matcher.formattedMessage(format.Message(actualString, "to match YAML of", expectedString)) } func (matcher *MatchYAMLMatcher) NegatedFailureMessage(actual any) (message string) { actualString, expectedString, _ := matcher.toNormalisedStrings(actual) - return formattedMessage(format.Message(actualString, "not to match YAML of", expectedString), matcher.firstFailurePath) + return matcher.formattedMessage(format.Message(actualString, "not to match YAML of", expectedString)) +} + +func (matcher *MatchYAMLMatcher) formattedMessage(comparisonMessage string) string { + if matcher.firstFailureDocument > 0 { + comparisonMessage = fmt.Sprintf("%s\n\nfirst mismatched document: %d (counting from 1)", comparisonMessage, matcher.firstFailureDocument) + } + return formattedMessage(comparisonMessage, matcher.firstFailurePath) } func (matcher *MatchYAMLMatcher) toNormalisedStrings(actual any) (actualFormatted, expectedFormatted string, err error) { @@ -50,16 +75,63 @@ func (matcher *MatchYAMLMatcher) toNormalisedStrings(actual any) (actualFormatte } func normalise(input string) string { - var val any - err := yaml.Unmarshal([]byte(input), &val) + docs, err := decodeYAMLDocuments(input) if err != nil { - panic(err) // unreachable since Match already calls Unmarshal + panic(err) // unreachable since Match already decodes the input } - output, err := yaml.Marshal(val) - if err != nil { - panic(err) // untested section, unreachable since we Unmarshal above + outputs := make([]string, len(docs)) + for i, doc := range docs { + output, err := yaml.Marshal(doc) + if err != nil { + panic(err) // untested section, unreachable since we decode above + } + outputs[i] = string(output) + } + return strings.TrimSpace(strings.Join(outputs, "---\n")) +} + +// decodeYAMLDocuments decodes every document in a YAML stream. +// +// Empty documents - such as those produced by a leading or trailing "---" - are +// skipped, so "---\na: 1\n---\n" is the single document "a: 1". A stream with +// no documents at all is treated as a single null document, as yaml.Unmarshal +// does. +func decodeYAMLDocuments(input string) ([]any, error) { + docs := []any{} + decoder := yaml.NewDecoder(strings.NewReader(input)) + for { + var node yaml.Node + err := decoder.Decode(&node) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + if isEmptyYAMLDocument(&node) { + continue + } + var doc any + if err := node.Decode(&doc); err != nil { + return nil, err + } + docs = append(docs, doc) + } + if len(docs) == 0 { + docs = append(docs, nil) + } + return docs, nil +} + +// isEmptyYAMLDocument reports whether a document has no content at all. +// Explicit nulls (e.g. "~" or "null") are content. +func isEmptyYAMLDocument(document *yaml.Node) bool { + if len(document.Content) != 1 { + return false } - return strings.TrimSpace(string(output)) + content := document.Content[0] + return content.Kind == yaml.ScalarNode && content.ShortTag() == "!!null" && content.Value == "" && + content.Style == 0 && content.Anchor == "" } func (matcher *MatchYAMLMatcher) toStrings(actual any) (actualFormatted, expectedFormatted string, err error) { diff --git a/vendor/github.com/onsi/gomega/matchers/type_support.go b/vendor/github.com/onsi/gomega/matchers/type_support.go index d020dedc..89056f44 100644 --- a/vendor/github.com/onsi/gomega/matchers/type_support.go +++ b/vendor/github.com/onsi/gomega/matchers/type_support.go @@ -14,6 +14,7 @@ package matchers import ( "encoding/json" "fmt" + "math/big" "reflect" "github.com/onsi/gomega/matchers/internal/miter" @@ -52,26 +53,11 @@ func isFloat(a any) bool { return reflect.Float32 <= kind && kind <= reflect.Float64 } -func toInteger(a any) int64 { +func toBigInt(a any) *big.Int { if isInteger(a) { - return reflect.ValueOf(a).Int() - } else if isUnsignedInteger(a) { - return int64(reflect.ValueOf(a).Uint()) - } else if isFloat(a) { - return int64(reflect.ValueOf(a).Float()) - } - panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) -} - -func toUnsignedInteger(a any) uint64 { - if isInteger(a) { - return uint64(reflect.ValueOf(a).Int()) - } else if isUnsignedInteger(a) { - return reflect.ValueOf(a).Uint() - } else if isFloat(a) { - return uint64(reflect.ValueOf(a).Float()) + return big.NewInt(reflect.ValueOf(a).Int()) } - panic(fmt.Sprintf("Expected a number! Got <%T> %#v", a, a)) + return new(big.Int).SetUint64(reflect.ValueOf(a).Uint()) } func toFloat(a any) float64 { diff --git a/vendor/modules.txt b/vendor/modules.txt index 26598d5f..a382f438 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -684,7 +684,7 @@ github.com/onsi/ginkgo/v2/internal/reporters github.com/onsi/ginkgo/v2/internal/testingtproxy github.com/onsi/ginkgo/v2/reporters github.com/onsi/ginkgo/v2/types -# github.com/onsi/gomega v1.43.0 +# github.com/onsi/gomega v1.44.0 ## explicit; go 1.25.0 github.com/onsi/gomega github.com/onsi/gomega/format