diff --git a/.claude/skills/add-assertion/SKILL.md b/.claude/skills/add-assertion/SKILL.md index 5d002b72e..139ddbb89 100644 --- a/.claude/skills/add-assertion/SKILL.md +++ b/.claude/skills/add-assertion/SKILL.md @@ -1,3 +1,8 @@ +--- +name: add-assertion +description: adding a new assertion to the library +--- + # Adding a New Assertion Step-by-step workflow for adding a new assertion function to testify. diff --git a/.claude/skills/codegen/SKILL.md b/.claude/skills/codegen/SKILL.md index 7b613d7ff..36fb75935 100644 --- a/.claude/skills/codegen/SKILL.md +++ b/.claude/skills/codegen/SKILL.md @@ -1,3 +1,8 @@ +--- +name: regenerate-code-and-doc +description: How to regenerate code and documentation after a maintenance that changes the API +--- + # Code Generation How the testify code and documentation generator works. diff --git a/.claude/skills/doc-site/SKILL.md b/.claude/skills/doc-site/SKILL.md index 2cc53232f..7a46be8cc 100644 --- a/.claude/skills/doc-site/SKILL.md +++ b/.claude/skills/doc-site/SKILL.md @@ -1,3 +1,8 @@ +--- +name: documentation-site +description: How to run and test the hugo doc site locally +--- + # Documentation Site Hugo-based documentation site for testify, auto-generated from source code. diff --git a/.claude/skills/testing-generic-functions/SKILL.md b/.claude/skills/testing-generic-functions/SKILL.md index 9fc0c1892..e9610ac04 100644 --- a/.claude/skills/testing-generic-functions/SKILL.md +++ b/.claude/skills/testing-generic-functions/SKILL.md @@ -1,3 +1,8 @@ +--- +name: writing-test-for-generics +description: How to write tests that cover both generic and reflection-based assertions +--- + # Testing Generic Functions with Table-Driven Tests ## The Challenge diff --git a/assert/assert_assertions.go b/assert/assert_assertions.go index fad856330..f31057ac2 100644 --- a/assert/assert_assertions.go +++ b/assert/assert_assertions.go @@ -1146,6 +1146,8 @@ func InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs . // InDeltaSlice is the same as [InDelta], except it compares two slices. // +// It returns false if the compared slices are not of the same length. +// // See [InDelta]. // // # Usage diff --git a/codegen/internal/generator/doc_generator.go b/codegen/internal/generator/doc_generator.go index f7319e3dd..c6aef72fe 100644 --- a/codegen/internal/generator/doc_generator.go +++ b/codegen/internal/generator/doc_generator.go @@ -143,7 +143,8 @@ func (d *DocGenerator) reorganizeByDomain() (iter.Seq2[string, model.Document], } weight++ - // populate document context in all children + // populate document context in all children: at doc generation time, + // we need the full context to be available when iterating over functions. doc.Package.Context = &doc for i, fn := range doc.Package.Functions { fn.Context = &doc diff --git a/codegen/internal/generator/domains/domains.go b/codegen/internal/generator/domains/domains.go index dc7e4b88c..e57853b9d 100644 --- a/codegen/internal/generator/domains/domains.go +++ b/codegen/internal/generator/domains/domains.go @@ -4,6 +4,7 @@ package domains import ( + "fmt" "iter" "path" "slices" @@ -15,25 +16,30 @@ import ( const ( nodomain = "common" assertions = "assertions" + maxDepth = 10 ) // FlattenDocumentation flattens a nested documentation structure into a map of packages. func FlattenDocumentation(documentation model.Documentation) map[string]model.Document { index := make(map[string]model.Document, len(documentation.Documents)) - flattenDocuments(index, documentation.Documents) + flattenDocuments(index, documentation.Documents, 0) return index } -func flattenDocuments(index map[string]model.Document, docs []model.Document) { +func flattenDocuments(index map[string]model.Document, docs []model.Document, depth int) { + if depth > maxDepth { + panic(fmt.Errorf("dev error: there is no sensible reason why we should recurse more than %d here", maxDepth)) + } + for _, doc := range docs { key := doc.Package.Package if _, ok := index[key]; !ok { index[key] = doc } - flattenDocuments(index, doc.Documents) + flattenDocuments(index, doc.Documents, depth+1) } } diff --git a/codegen/internal/generator/funcmaps/funcmaps.go b/codegen/internal/generator/funcmaps/funcmaps.go index a06576fcc..5eae1f784 100644 --- a/codegen/internal/generator/funcmaps/funcmaps.go +++ b/codegen/internal/generator/funcmaps/funcmaps.go @@ -283,7 +283,8 @@ func relocate(values []model.TestValue, pkg string) string { return strings.Join(parts, ", ") } - // Relocate each value + // Relocate each value: the original "assertions" package moves to the + // target package where the value is generated. relocated := make([]string, 0, len(values)) for _, tv := range values { // If parse failed, use original (fallback) diff --git a/codegen/internal/generator/funcmaps/markdown.go b/codegen/internal/generator/funcmaps/markdown.go index 5597d03cf..4dc8239cc 100644 --- a/codegen/internal/generator/funcmaps/markdown.go +++ b/codegen/internal/generator/funcmaps/markdown.go @@ -58,7 +58,8 @@ func markdownLinks(in string) (string, []string) { // Pattern: [text]: url (at start of line or after whitespace) refLinks := make(map[string]string) - // Extract all reference links + // Extract all reference links: godoc-style links need to be reworked as proper markdown. + // We extract the detected links (regexp match) before further reprocessing (below). matches := refLinkPattern.FindAllStringSubmatch(in, -1) const expectedGroups = 2 for _, match := range matches { @@ -70,7 +71,7 @@ func markdownLinks(in string) (string, []string) { // Remove reference link definitions from input processed := refLinkPattern.ReplaceAllString(in, "") - // Convert reference-style links to inline links + // Convert reference-style links to inline links. // Replace [text] with [text](url) where we have the reference usedRefs := make(map[string]bool) for refText, refURL := range refLinks { diff --git a/codegen/internal/scanner/comments-parser/examples.go b/codegen/internal/scanner/comments-parser/examples.go index 245d4b973..96e92bff2 100644 --- a/codegen/internal/scanner/comments-parser/examples.go +++ b/codegen/internal/scanner/comments-parser/examples.go @@ -72,7 +72,12 @@ func ParseTestExamples(text string) []model.Test { continue } - // skip until we find the Examples section + // skip until we find the Examples section. + // + // This assumes that the godoc comment is organized in sections ("# {section}"): + // we'll start processing only when we find an "# Example[s]" section. + // + // In this section, we expect to find example values that fit the documented assertion (both success and fail). if !inExamplesSection { continue } diff --git a/codegen/internal/scanner/comments-parser/expressions.go b/codegen/internal/scanner/comments-parser/expressions.go index 959251561..7eccdf0b8 100644 --- a/codegen/internal/scanner/comments-parser/expressions.go +++ b/codegen/internal/scanner/comments-parser/expressions.go @@ -46,7 +46,11 @@ func ParseTestValues(input string) []model.TestValue { }} } - // Extract elements from the composite literal + // Extract elements from the composite literal. + // The godoc comment contains testable values as legit go literals. + // + // We parse this snippet and produce a documented version of it [model.TestValue] + // for proper rendering (either as code or as documentation). compositeLit, ok := expr.(*ast.CompositeLit) if !ok { // Should never happen if parser succeeded @@ -58,29 +62,11 @@ func ParseTestValues(input string) []model.TestValue { } // Convert each element to TestValue - result := make([]model.TestValue, 0, len(compositeLit.Elts)) // We need to extract the original source text for each element // Since we don't have position info for the original input, we'll format the AST fset := token.NewFileSet() - for _, elt := range compositeLit.Elts { - // Format the expression back to source code - var buf strings.Builder - if err := format.Node(&buf, fset, elt); err != nil { - result = append(result, model.TestValue{ - Raw: "", - Expr: elt, - Error: fmt.Errorf("failed to format expression: %w", err), - }) - continue - } - - result = append(result, model.TestValue{ - Raw: buf.String(), - Expr: elt, - Error: nil, - }) - } + result := formatLiteralExpression(compositeLit, fset) return result } @@ -120,6 +106,16 @@ func ParseExprWithFileSet(fset *token.FileSet, filename string, input string) ([ } // Convert each element to TestValue + result := formatLiteralExpression(compositeLit, fset) + + return result, nil +} + +func formatLiteralExpression(compositeLit *ast.CompositeLit, fset *token.FileSet) []model.TestValue { + if compositeLit == nil { + return nil + } + result := make([]model.TestValue, 0, len(compositeLit.Elts)) for _, elt := range compositeLit.Elts { @@ -141,5 +137,5 @@ func ParseExprWithFileSet(fset *token.FileSet, filename string, input string) ([ }) } - return result, nil + return result } diff --git a/codegen/internal/scanner/examples-parser/parser.go b/codegen/internal/scanner/examples-parser/parser.go index fabf9c1de..59cfd4aae 100644 --- a/codegen/internal/scanner/examples-parser/parser.go +++ b/codegen/internal/scanner/examples-parser/parser.go @@ -289,12 +289,6 @@ func (x TestableExample) Render() string { } return x.renderBody() - - // Previous routing: stripped package/imports/main scaffolding. - // if x.WholeFile && x.play != nil { - // return x.renderWholeFile() - // } - // return x.renderBody() } // renderPlay renders the Play AST as-is: @@ -323,7 +317,9 @@ func (x TestableExample) renderBody() string { return "" } - // Print the raw AST node. + // Print the raw AST node as valid, formatted go code. + // This allows to generate code from the testable values + // captured in the godoc of an assertion. var buf bytes.Buffer p := printer.Config{Mode: printer.UseSpaces, Tabwidth: tabWidth} if err := p.Fprint(&buf, x.fset, x.code); err != nil { @@ -352,36 +348,6 @@ func (x TestableExample) renderBody() string { return extractFuncBody(string(formatted)) } -/* -// renderWholeFile renders a whole-file example, stripping the package clause -// and imports, and renaming "func main()" back to the example function name. -func (x TestableExample) renderWholeFile() string { - // Print the entire Play file. - var buf bytes.Buffer - p := printer.Config{Mode: printer.UseSpaces, Tabwidth: tabWidth} - if err := p.Fprint(&buf, x.fset, x.play); err != nil { - return "" - } - - raw := buf.String() - - // Remove "// Output:" comments. - raw = stripOutputComments(raw) - - // Format with goimports. - formatted, err := imports.Process("example.go", []byte(raw), &imports.Options{ - Fragment: true, - FormatOnly: true, - }) - if err != nil { - formatted = []byte(raw) - } - - // Strip package clause and imports, rename main -> Example function. - return extractWholeFileBody(string(formatted), "Example"+x.Name) -} -*/ - // extractWholeFileBody strips the package clause and import blocks from a // formatted Go file, and renames "func main()" to the given example function name. func extractWholeFileBody(src, exampleFuncName string) string { diff --git a/docs/doc-site/api/number.md b/docs/doc-site/api/number.md index 697444fd5..7d49ed8c1 100644 --- a/docs/doc-site/api/number.md +++ b/docs/doc-site/api/number.md @@ -281,13 +281,15 @@ func main() { |--|--| | [`assertions.InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InDeltaMapValues) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDeltaMapValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L367) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDeltaMapValues](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L375) {{% /tab %}} {{< /tabs >}} ### InDeltaSlice{#indeltaslice} InDeltaSlice is the same as [InDelta](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDelta), except it compares two slices. +It returns false if the compared slices are not of the same length. + See [InDelta](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert#InDelta). {{% expand title="Examples" %}} @@ -394,7 +396,7 @@ func main() { |--|--| | [`assertions.InDeltaSlice(t T, expected any, actual any, delta float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InDeltaSlice) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDeltaSlice](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L331) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InDeltaSlice](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L333) {{% /tab %}} {{< /tabs >}} @@ -752,7 +754,7 @@ func main() { |--|--| | [`assertions.InEpsilonSlice(t T, expected any, actual any, epsilon float64, msgAndArgs ...any) bool`](https://pkg.go.dev/github.com/go-openapi/testify/v2/internal/assertions#InEpsilonSlice) | internal implementation | -**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InEpsilonSlice](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L422) +**Source:** [github.com/go-openapi/testify/v2/internal/assertions#InEpsilonSlice](https://github.com/go-openapi/testify/blob/master/internal/assertions/number.go#L430) {{% /tab %}} {{< /tabs >}} diff --git a/internal/assertions/enable/colors/colors.go b/internal/assertions/enable/colors/colors.go index 39442b15a..3e2882e57 100644 --- a/internal/assertions/enable/colors/colors.go +++ b/internal/assertions/enable/colors/colors.go @@ -20,18 +20,6 @@ const ( brightYellowMark = "\033[0;93m" // aka yellow brightCyanMark = "\033[0;96m" // aka turquoise - // color codes for future use. - - // blackMark = "\033[0;30m" - // blueMark = "\033[0;34m" - // magentaMark = "\033[0;35m" - // greyMark = "\033[0;37m". - - // darkGreyMark = "\033[0;90m" - // brightBlueMark = "\033[0;94m" - // brightMagentaMark = "\033[0;95m" - // brightWhiteMark = "\033[0;97m". - endMark = "\033[0m" ) @@ -56,13 +44,9 @@ func noopColorizer(s string) string { var ( greenColorizer = makeColorizer(greenMark) redColorizer = makeColorizer(redMark) - // yellowColorizer = makeColorizer(yellowMark) - // cyanColorizer = makeColorizer(cyanMark). brightGreenColorizer = makeColorizer(brightGreenMark) brightRedColorizer = makeColorizer(brightRedMark) - // brightYellowColorizer = makeColorizer(brightYellowMark) - // brightCyanColorizer = makeColorizer(brightCyanMark). ) //nolint:gochecknoglobals // internal printer builders may safely be shared at the package-level @@ -76,12 +60,6 @@ var ( brightRedPrinterBuilder = ansiPrinterBuilder(brightRedMark) brightYellowPrinterBuilder = ansiPrinterBuilder(brightYellowMark) brightCyanPrinterBuilder = ansiPrinterBuilder(brightCyanMark) - - // magentaPrinterBuilder = ansiPrinterBuilder(magentaMark) - // bluePrinterBuilder = ansiPrinterBuilder(blueMark). - - // brightMagentaPrinterBuilder = ansiPrinterBuilder(brightMagentaMark) - // brightBluePrinterBuilder = ansiPrinterBuilder(brightBlueMark). ) func ansiPrinterBuilder(mark string) difflib.PrinterBuilder { diff --git a/internal/assertions/equal.go b/internal/assertions/equal.go index 2765cb543..66747e834 100644 --- a/internal/assertions/equal.go +++ b/internal/assertions/equal.go @@ -12,6 +12,9 @@ import ( "github.com/go-openapi/testify/v2/internal/assertions/enable/colors" ) +// maxDepth is the maximum exploration depth for copyExportedFields. +const maxDepth = 1000 + // Equal asserts that two objects are equal. // // Pointer variable equality is determined based on the equality of the @@ -220,7 +223,8 @@ func EqualExportedValues(t T, expected, actual any, msgAndArgs ...any) bool { h.Helper() } - if err := validateEqualArgs(expected, actual); err != nil { + err := validateEqualArgs(expected, actual) + if err != nil { return Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)", expected, actual, err), msgAndArgs...) } @@ -232,8 +236,15 @@ func EqualExportedValues(t T, expected, actual any, msgAndArgs ...any) bool { return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) } - expected = copyExportedFields(expected) - actual = copyExportedFields(actual) + expected, err = copyExportedFields(expected) + if err != nil { + return Fail(t, fmt.Sprintf("An error occurred while exploring the expected value: %v", err), msgAndArgs...) + } + + actual, err = copyExportedFields(actual) + if err != nil { + return Fail(t, fmt.Sprintf("An error occurred while exploring the actual value: %v", err), msgAndArgs...) + } if !ObjectsAreEqualValues(expected, actual) { diff := diff(expected, actual) @@ -349,16 +360,23 @@ func formatUnequalValues(expected, actual any) (e string, a string) { // copyExportedFields iterates downward through nested data structures and creates a copy // that only contains the exported struct fields. -func copyExportedFields(expected any) any { - return copyExportedFieldsRec(expected, make(map[uintptr]struct{})) +// +// Exploration down the rabbit hole is limited to a depth of 1000. +// An error is returned if the introspection goes wrong. +func copyExportedFields(expected any) (any, error) { + return copyExportedFieldsRec(expected, make(map[uintptr]struct{}), 0) } // copyExportedFieldsRec carries a set of pointers currently being visited on the // recursion path, so that cyclic pointer references break the recursion instead // of overflowing the goroutine stack. -func copyExportedFieldsRec(expected any, visited map[uintptr]struct{}) any { +func copyExportedFieldsRec(expected any, visited map[uintptr]struct{}, depth int) (any, error) { + if depth > maxDepth { + return nil, fmt.Errorf("stopped recursing value after %d nested levels", maxDepth) + } + if isNil(expected) { - return expected + return expected, nil } expectedType := reflect.TypeOf(expected) @@ -366,19 +384,23 @@ func copyExportedFieldsRec(expected any, visited map[uintptr]struct{}) any { switch expectedType.Kind() { case reflect.Struct: - return copyExportedStruct(expectedType, expectedValue, visited) + return copyExportedStruct(expectedType, expectedValue, visited, depth+1) case reflect.Pointer: - return copyExportedPointer(expected, expectedType, expectedValue, visited) + return copyExportedPointer(expected, expectedType, expectedValue, visited, depth+1) case reflect.Array, reflect.Slice: - return copyExportedSequence(expectedType, expectedValue, visited) + return copyExportedSequence(expectedType, expectedValue, visited, depth+1) case reflect.Map: - return copyExportedMap(expectedType, expectedValue, visited) + return copyExportedMap(expectedType, expectedValue, visited, depth+1) default: - return expected + return expected, nil } } -func copyExportedStruct(expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}) any { +func copyExportedStruct(expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}, depth int) (any, error) { + if depth > maxDepth { + return nil, fmt.Errorf("stopped recursing value after %d nested levels", maxDepth) + } + result := reflect.New(expectedType).Elem() for i := range expectedType.NumField() { if !expectedType.Field(i).IsExported() { @@ -388,52 +410,77 @@ func copyExportedStruct(expectedType reflect.Type, expectedValue reflect.Value, if isNil(fieldValue) || isNil(fieldValue.Interface()) { continue } - newValue := copyExportedFieldsRec(fieldValue.Interface(), visited) + newValue, err := copyExportedFieldsRec(fieldValue.Interface(), visited, depth+1) + if err != nil { + return nil, err + } result.Field(i).Set(reflect.ValueOf(newValue)) } - return result.Interface() + + return result.Interface(), nil } -func copyExportedPointer(expected any, expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}) any { +func copyExportedPointer(expected any, expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}, depth int) (any, error) { + if depth > maxDepth { + return nil, fmt.Errorf("stopped recursing value after %d nested levels", maxDepth) + } + // Guard against cyclic pointer references: if this pointer is already // on the current recursion path, return it as-is to break the cycle. ptr := expectedValue.Pointer() if _, ok := visited[ptr]; ok { - return expected + return expected, nil } visited[ptr] = struct{}{} defer delete(visited, ptr) result := reflect.New(expectedType.Elem()) - unexportedRemoved := copyExportedFieldsRec(expectedValue.Elem().Interface(), visited) + unexportedRemoved, err := copyExportedFieldsRec(expectedValue.Elem().Interface(), visited, depth+1) + if err != nil { + return nil, err + } if unexportedRemoved != nil { result.Elem().Set(reflect.ValueOf(unexportedRemoved)) } - return result.Interface() + + return result.Interface(), nil } -func copyExportedSequence(expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}) any { +func copyExportedSequence(expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}, depth int) (any, error) { + if depth > maxDepth { + return nil, fmt.Errorf("stopped recursing value after %d nested levels", maxDepth) + } + var result reflect.Value if expectedType.Kind() == reflect.Array { result = reflect.New(reflect.ArrayOf(expectedValue.Len(), expectedType.Elem())).Elem() } else { result = reflect.MakeSlice(expectedType, expectedValue.Len(), expectedValue.Len()) } + for i := range expectedValue.Len() { index := expectedValue.Index(i) if !index.CanInterface() { // this should not be possible with current reflect, since values are retrieved from an array or slice, not a struct panic(fmt.Errorf("internal error: can't resolve Interface() for value %v", index)) } - unexportedRemoved := copyExportedFieldsRec(index.Interface(), visited) + unexportedRemoved, err := copyExportedFieldsRec(index.Interface(), visited, depth+1) + if err != nil { + return nil, err + } if unexportedRemoved != nil { result.Index(i).Set(reflect.ValueOf(unexportedRemoved)) } } - return result.Interface() + + return result.Interface(), nil } -func copyExportedMap(expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}) any { +func copyExportedMap(expectedType reflect.Type, expectedValue reflect.Value, visited map[uintptr]struct{}, depth int) (any, error) { + if depth > maxDepth { + return nil, fmt.Errorf("stopped recursing value after %d nested levels", maxDepth) + } + result := reflect.MakeMap(expectedType) for _, k := range expectedValue.MapKeys() { index := expectedValue.MapIndex(k) @@ -441,17 +488,22 @@ func copyExportedMap(expectedType reflect.Type, expectedValue reflect.Value, vis // this should not be possible with current reflect, since values are retrieved from a map, not a struct panic(fmt.Errorf("internal error: can't resolve Interface() for value %v", index)) } - unexportedRemoved := copyExportedFieldsRec(index.Interface(), visited) + unexportedRemoved, err := copyExportedFieldsRec(index.Interface(), visited, depth+1) + if err != nil { + return nil, err + } if unexportedRemoved != nil { result.SetMapIndex(k, reflect.ValueOf(unexportedRemoved)) } } - return result.Interface() + + return result.Interface(), nil } func isFunction(arg any) bool { if arg == nil { return false } + return reflect.TypeOf(arg).Kind() == reflect.Func } diff --git a/internal/assertions/equal_unary.go b/internal/assertions/equal_unary.go index e18326a13..44cc08877 100644 --- a/internal/assertions/equal_unary.go +++ b/internal/assertions/equal_unary.go @@ -130,6 +130,9 @@ func isNil(object any) bool { } // isEmpty gets whether the specified object is considered empty or not. +// +// It stops following pointers with a depth of 1000, assuming a "not-empty" state +// when the recursion depth is reached. func isEmpty(object any) bool { // get nil case out of the way if object == nil { @@ -141,16 +144,21 @@ func isEmpty(object any) bool { // isEmptyValue gets whether the specified reflect.Value is considered empty or not. func isEmptyValue(objValue reflect.Value) bool { - return isEmptyValueRec(objValue, nil) + return isEmptyValueRec(objValue, nil, 0) } // isEmptyValueRec carries the set of pointers already followed on the current // recursion path, so that a cyclic pointer chain (e.g. type P *P with p = &p) // breaks the recursion instead of overflowing the goroutine stack. -func isEmptyValueRec(objValue reflect.Value, visited map[uintptr]struct{}) bool { +func isEmptyValueRec(objValue reflect.Value, visited map[uintptr]struct{}, depth int) bool { + if depth > maxDepth { + return false + } + if objValue.IsZero() { return true } + // Special cases of non-zero values that we consider empty switch objValue.Kind() { // collection types are empty when they have no element @@ -168,7 +176,7 @@ func isEmptyValueRec(objValue reflect.Value, visited map[uintptr]struct{}) bool visited = make(map[uintptr]struct{}) } visited[ptr] = struct{}{} - return isEmptyValueRec(objValue.Elem(), visited) + return isEmptyValueRec(objValue.Elem(), visited, depth+1) default: return false } diff --git a/internal/assertions/number.go b/internal/assertions/number.go index d8fc6d6e6..94acb2938 100644 --- a/internal/assertions/number.go +++ b/internal/assertions/number.go @@ -318,6 +318,8 @@ func InEpsilonSymmetricT[Number Measurable](t T, x, y Number, epsilon float64, m // InDeltaSlice is the same as [InDelta], except it compares two slices. // +// It returns false if the compared slices are not of the same length. +// // See [InDelta]. // // # Usage @@ -342,7 +344,13 @@ func InDeltaSlice(t T, expected, actual any, delta float64, msgAndArgs ...any) b actualSlice := reflect.ValueOf(actual) expectedSlice := reflect.ValueOf(expected) - for i := range actualSlice.Len() { + lenActual := actualSlice.Len() + lenExpected := expectedSlice.Len() + if lenActual != lenExpected { + return Fail(t, "Parameters must be slice", msgAndArgs...) + } + + for i := range lenActual { result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta, msgAndArgs...) if !result { return result diff --git a/internal/assertions/number_test.go b/internal/assertions/number_test.go index 895fe0064..3ae2c143e 100644 --- a/internal/assertions/number_test.go +++ b/internal/assertions/number_test.go @@ -774,7 +774,24 @@ func deltaSliceCases() iter.Seq[genericTestCase] { false, ), }, - + { + "slices-of-different-lengths-1", + testDeltaSlice( + []float64{1}, + []float64{1, 1}, + 0.1, + false, + ), + }, + { + "slices-of-different-lengths-2", + testDeltaSlice( + []float64{1, 1}, + []float64{1}, + 0.1, + false, + ), + }, // Edge cases - invalid inputs { "invalid-non-slice-inputs", diff --git a/internal/assertions/object_test.go b/internal/assertions/object_test.go index 41c3ed2ff..50877ddb7 100644 --- a/internal/assertions/object_test.go +++ b/internal/assertions/object_test.go @@ -45,7 +45,13 @@ func TestObjectsCopyExportedFields(t *testing.T) { for c := range objectCopyExportedFieldsCases() { t.Run("", func(t *testing.T) { - output := copyExportedFields(c.input) + output, err := copyExportedFields(c.input) + if err != nil { + t.Errorf("should not error") + + return + } + if !ObjectsAreEqualValues(c.expected, output) { t.Errorf("%#v, %#v should be equal", c.expected, output) } diff --git a/internal/assertions/recursion_cycles_test.go b/internal/assertions/recursion_cycles_test.go index f01bdca68..643ec7160 100644 --- a/internal/assertions/recursion_cycles_test.go +++ b/internal/assertions/recursion_cycles_test.go @@ -66,7 +66,10 @@ func TestCopyExportedFieldsCycle(t *testing.T) { a.Next = a // Direct call: the only property under test is that it returns. - _ = copyExportedFields(a) + _, err := copyExportedFields(a) + if err != nil { + t.Errorf("should not error") + } }) } diff --git a/internal/testintegration/spew/generator.go b/internal/testintegration/spew/generator.go index 015eaa2f3..9c7a09744 100644 --- a/internal/testintegration/spew/generator.go +++ b/internal/testintegration/spew/generator.go @@ -68,8 +68,6 @@ func NoPanicProp(ctx context.Context, g *rapid.Generator[any]) func(*rapid.T) { }() value = spew.Sdump(value) - // fmt.Printf("%v", value) - close(done) }() diff --git a/require/require_assertions.go b/require/require_assertions.go index 870964e8b..60d5c62af 100644 --- a/require/require_assertions.go +++ b/require/require_assertions.go @@ -1314,6 +1314,8 @@ func InDeltaMapValues(t T, expected any, actual any, delta float64, msgAndArgs . // InDeltaSlice is the same as [InDelta], except it compares two slices. // +// It returns false if the compared slices are not of the same length. +// // See [InDelta]. // // # Usage