From 2202336ef6b4bfdae86fd5e0c350d2cbddaed327 Mon Sep 17 00:00:00 2001 From: ckbkr <56784875+ckbaker10@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:50:24 +0200 Subject: [PATCH 1/3] Tests to trigger the issue --- matchers/have_patterns_test.go | 62 ++++++++++++++++++++++++++++++++++ resource/validate_test.go | 27 +++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 matchers/have_patterns_test.go diff --git a/matchers/have_patterns_test.go b/matchers/have_patterns_test.go new file mode 100644 index 000000000..9a6d75995 --- /dev/null +++ b/matchers/have_patterns_test.go @@ -0,0 +1,62 @@ +package matchers + +import ( + "bytes" + "strings" + "testing" +) + +// TestHavePatternsFailureResultActualIsString asserts that FailureResult returns +// the file content as a string, not the Go type name "object: *bytes.Reader". +func TestHavePatternsFailureResultActualIsString(t *testing.T) { + content := "Banner /etc/issue.net\nLogLevel INFO\n" + pattern := "nonexistent-pattern" + + m := HavePatterns([]interface{}{pattern}) + + reader := strings.NewReader(content) + success, err := m.Match(reader) + if err != nil { + t.Fatalf("Match returned unexpected error: %v", err) + } + if success { + t.Fatal("expected Match to return false for missing pattern") + } + + // reader is now consumed; FailureResult must still show the content + result := m.FailureResult(reader) + + actual, ok := result.Actual.(string) + if !ok { + t.Fatalf("FailureResult.Actual must be a string, got %T: %v", result.Actual, result.Actual) + } + if strings.Contains(actual, "object:") { + t.Errorf("FailureResult.Actual must not contain Go type repr, got: %q", actual) + } +} + +func TestHavePatternsFailureResultBytesReader(t *testing.T) { + content := "pam_faillock.so preauth\n" + pattern := "nonexistent-pattern" + + m := HavePatterns([]interface{}{pattern}) + + reader := bytes.NewReader([]byte(content)) + success, err := m.Match(reader) + if err != nil { + t.Fatalf("Match returned unexpected error: %v", err) + } + if success { + t.Fatal("expected Match to return false for missing pattern") + } + + result := m.FailureResult(reader) + + actual, ok := result.Actual.(string) + if !ok { + t.Fatalf("FailureResult.Actual must be a string, got %T: %v", result.Actual, result.Actual) + } + if strings.Contains(actual, "object:") { + t.Errorf("FailureResult.Actual must not contain Go type repr, got: %q", actual) + } +} diff --git a/resource/validate_test.go b/resource/validate_test.go index 55a450205..89f57ac8a 100644 --- a/resource/validate_test.go +++ b/resource/validate_test.go @@ -128,6 +128,33 @@ func TestValidateContainsBadRegexErr(t *testing.T) { } } +// TestValidateContainsFailureActual asserts that when a file-contents check fails, +// the MatcherResult.Actual field contains the readable file content — not the +// Go internal type representation "object: *bytes.Reader" / "object: *os.File". +func TestValidateContainsFailureActual(t *testing.T) { + fileContent := "Banner /etc/issue.net\nLogLevel INFO\n" + missingPattern := "nonexistent-pattern-xyz" + + inFunc := func() (io.Reader, error) { + return strings.NewReader(fileContent), nil + } + got := ValidateValue(&FakeResource{""}, "contents", []interface{}{missingPattern}, inFunc, false) + + if got.Result != FAIL { + t.Fatalf("expected FAIL, got %v", got.Result) + } + actual, ok := got.MatcherResult.Actual.(string) + if !ok { + t.Fatalf("MatcherResult.Actual must be a string, got %T: %v", got.MatcherResult.Actual, got.MatcherResult.Actual) + } + if strings.Contains(actual, "object:") { + t.Errorf("MatcherResult.Actual must not contain Go type repr, got: %q", actual) + } + if !strings.Contains(actual, "Banner") { + t.Errorf("MatcherResult.Actual must contain file content, got: %q", actual) + } +} + func TestValidateContainsSkip(t *testing.T) { for _, c := range containsTests { inFunc := func() (io.Reader, error) { From 4f401f8d524818a82c4990ffbe02a983bb804b2d Mon Sep 17 00:00:00 2001 From: ckbkr <56784875+ckbaker10@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:52:42 +0200 Subject: [PATCH 2/3] Fix byte reader issue --- matchers/have_patterns_test.go | 58 +++++++++++++++++++--------------- resource/validate.go | 10 +++++- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/matchers/have_patterns_test.go b/matchers/have_patterns_test.go index 9a6d75995..5f19fb393 100644 --- a/matchers/have_patterns_test.go +++ b/matchers/have_patterns_test.go @@ -6,25 +6,38 @@ import ( "testing" ) -// TestHavePatternsFailureResultActualIsString asserts that FailureResult returns -// the file content as a string, not the Go type name "object: *bytes.Reader". -func TestHavePatternsFailureResultActualIsString(t *testing.T) { +// TestHavePatternsMatchString verifies that HavePatterns correctly matches +// string content (the type it receives after the validate-layer materialization fix). +func TestHavePatternsMatchString(t *testing.T) { + content := "Banner /etc/issue.net\nLogLevel INFO\n" + + m := HavePatterns([]interface{}{"Banner /etc/issue.net"}) + success, err := m.Match(content) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !success { + t.Fatal("expected Match to return true") + } +} + +// TestHavePatternsFailureResultString verifies that FailureResult returns the +// actual content as a string — not a Go type repr — when given string input. +// After the validate-layer fix, FailureResult always receives a materialized string. +func TestHavePatternsFailureResultString(t *testing.T) { content := "Banner /etc/issue.net\nLogLevel INFO\n" pattern := "nonexistent-pattern" m := HavePatterns([]interface{}{pattern}) - - reader := strings.NewReader(content) - success, err := m.Match(reader) + success, err := m.Match(content) if err != nil { - t.Fatalf("Match returned unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } if success { t.Fatal("expected Match to return false for missing pattern") } - // reader is now consumed; FailureResult must still show the content - result := m.FailureResult(reader) + result := m.FailureResult(content) actual, ok := result.Actual.(string) if !ok { @@ -33,30 +46,23 @@ func TestHavePatternsFailureResultActualIsString(t *testing.T) { if strings.Contains(actual, "object:") { t.Errorf("FailureResult.Actual must not contain Go type repr, got: %q", actual) } + if !strings.Contains(actual, "Banner") { + t.Errorf("FailureResult.Actual must contain the file content, got: %q", actual) + } } -func TestHavePatternsFailureResultBytesReader(t *testing.T) { +// TestHavePatternsMatchBytesReader verifies that HavePatterns still accepts +// io.Reader directly (backwards-compat; the validate layer now sends strings). +func TestHavePatternsMatchBytesReader(t *testing.T) { content := "pam_faillock.so preauth\n" - pattern := "nonexistent-pattern" - - m := HavePatterns([]interface{}{pattern}) + m := HavePatterns([]interface{}{"pam_faillock.so"}) reader := bytes.NewReader([]byte(content)) success, err := m.Match(reader) if err != nil { - t.Fatalf("Match returned unexpected error: %v", err) - } - if success { - t.Fatal("expected Match to return false for missing pattern") - } - - result := m.FailureResult(reader) - - actual, ok := result.Actual.(string) - if !ok { - t.Fatalf("FailureResult.Actual must be a string, got %T: %v", result.Actual, result.Actual) + t.Fatalf("unexpected error: %v", err) } - if strings.Contains(actual, "object:") { - t.Errorf("FailureResult.Actual must not contain Go type repr, got: %q", actual) + if !success { + t.Fatal("expected Match to return true") } } diff --git a/resource/validate.go b/resource/validate.go index 720c7b7d5..1914f9406 100644 --- a/resource/validate.go +++ b/resource/validate.go @@ -159,7 +159,15 @@ func ValidateGomegaValue(res ResourceRead, property string, expectedValue any, a case func() (any, error): foundValue, err = f() case func() (io.Reader, error): - foundValue, err = f() + var r io.Reader + r, err = f() + if err == nil { + var i interface{} + i, err = matchers.ReaderToString{}.Transform(r) + if err == nil { + foundValue = i.(string) + } + } gomegaMatcher = matchers.HavePatterns(expectedValue) default: err = fmt.Errorf("Unknown method signature: %t", f) From 50459109a83dc5cb5da187652fa6f2bd9bb15a7b Mon Sep 17 00:00:00 2001 From: ckbkr <56784875+ckbaker10@users.noreply.github.com> Date: Mon, 20 Apr 2026 08:55:04 +0200 Subject: [PATCH 3/3] make test green --- testdata/out_matching_basic_failing.1.documentation | 4 ++-- testdata/out_matching_basic_failing.1.rspecish | 2 +- testdata/out_matching_basic_failing.1.tap | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testdata/out_matching_basic_failing.1.documentation b/testdata/out_matching_basic_failing.1.documentation index 735d3a63f..8c50f2b78 100644 --- a/testdata/out_matching_basic_failing.1.documentation +++ b/testdata/out_matching_basic_failing.1.documentation @@ -31,7 +31,7 @@ to satisfy at least one of these matchers [{"have-len":2}] Matching: basic_reader: matches: Expected - "object: *strings.Reader" + "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were @@ -253,7 +253,7 @@ to satisfy at least one of these matchers Matching: basic_reader: matches: Expected - "object: *strings.Reader" + "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were diff --git a/testdata/out_matching_basic_failing.1.rspecish b/testdata/out_matching_basic_failing.1.rspecish index 3d6ca262f..8ee94905a 100644 --- a/testdata/out_matching_basic_failing.1.rspecish +++ b/testdata/out_matching_basic_failing.1.rspecish @@ -40,7 +40,7 @@ to satisfy at least one of these matchers Matching: basic_reader: matches: Expected - "object: *strings.Reader" + "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were diff --git a/testdata/out_matching_basic_failing.1.tap b/testdata/out_matching_basic_failing.1.tap index a60f1f657..348c081f1 100644 --- a/testdata/out_matching_basic_failing.1.tap +++ b/testdata/out_matching_basic_failing.1.tap @@ -4,7 +4,7 @@ not ok 2 - Matching: basic_array_consists_of: matches: Expected ["foo","bar","mo not ok 3 - Matching: basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}] not ok 4 - Matching: basic_int: matches: Expected 42 to be numerically eq 43 not ok 5 - Matching: basic_len: matches: Expected "123" to satisfy at least one of these matchers [{"have-len":2}] -not ok 6 - Matching: basic_reader: matches: Expected "object: *strings.Reader" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"] +not ok 6 - Matching: basic_reader: matches: Expected "foo bar\nmoo cow\n" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"] not ok 7 - Matching: basic_semver: matches: Expected "1.2.3" to satisfy at least one of these matchers [{"semver-constraint":">=9.9.0"}] not ok 8 - Matching: basic_string: matches: Expected "this is a test" to equal "this is a failing test" not ok 9 - Matching: basic_string_contain_substring: matches: Expected "foo" to contain substring "x"