diff --git a/go.mod b/go.mod index ab04531..ecc5f09 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/cloudfoundry/bosh-cpi-go v0.0.0-20260620100136-96c1efec01d3 github.com/cloudfoundry/bosh-utils v0.0.653 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.43.1 + github.com/onsi/gomega v1.44.0 github.com/prometheus/common v0.71.0 ) diff --git a/go.sum b/go.sum index f800c8d..fcfb924 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ 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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.43.1 h1:vGIPFuYrIO6/0Z09s0I0QQQgFchiX4+tb1re3MScJYo= -github.com/onsi/gomega v1.43.1/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= +github.com/onsi/gomega v1.44.0 h1:eAiGl3Pw5jz5GQdDff0BcxYpAX1JxW8xD7mFUuwNfZQ= +github.com/onsi/gomega v1.44.0/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8= github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk= diff --git a/vendor/github.com/onsi/gomega/CHANGELOG.md b/vendor/github.com/onsi/gomega/CHANGELOG.md index 51239da..07f3325 100644 --- a/vendor/github.com/onsi/gomega/CHANGELOG.md +++ b/vendor/github.com/onsi/gomega/CHANGELOG.md @@ -6,6 +6,20 @@ ### 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 diff --git a/vendor/github.com/onsi/gomega/gomega_dsl.go b/vendor/github.com/onsi/gomega/gomega_dsl.go index dd1511f..cf4eaef 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.1" +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 bf57226..e3e01f6 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 355b362..ea8e5ba 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 7e6ce15..02839f3 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 8b2d297..18f7b28 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 16630c1..5b27bab 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 0cd7081..29410a5 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 331f289..96f5246 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 f7dcaf6..86bf2f2 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 c3da9bd..a6c558d 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 d020ded..89056f4 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 16e8735..f4ae292 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -66,7 +66,7 @@ github.com/onsi/ginkgo/reporters/stenographer github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable github.com/onsi/ginkgo/reporters/stenographer/support/go-isatty github.com/onsi/ginkgo/types -# github.com/onsi/gomega v1.43.1 +# github.com/onsi/gomega v1.44.0 ## explicit; go 1.25.0 github.com/onsi/gomega github.com/onsi/gomega/format