From 08a44a99a03fa2359eac126e5184a5de8379136c Mon Sep 17 00:00:00 2001 From: Adam Luzsi Date: Fri, 16 Jun 2023 21:04:54 +0200 Subject: [PATCH] introducing the assert.Match assertion helper The assert.Match enables text testing against regular expressions. --- assert/Asserter.go | 56 +++++++++++++++++++++++++++++++++++++++++ assert/Asserter_test.go | 56 ++++++++++++++++++++++++++++++++++++++++- assert/example_test.go | 26 +++++++++++++++++++ assert/pkgfunc.go | 10 ++++++++ assert/pkgfunc_test.go | 48 +++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 1 deletion(-) diff --git a/assert/Asserter.go b/assert/Asserter.go index a0d6719..cb9e5a1 100644 --- a/assert/Asserter.go +++ b/assert/Asserter.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "reflect" + "regexp" "strings" "sync/atomic" "testing" @@ -506,6 +507,61 @@ searching: } } +// Match will match an expression against a given value. +// Match will fail for both receiving an invalid expression +// or having the value not matched by the expression. +// If the expression is invalid, test will fail early, regardless if Should or Must was used. +func (a Asserter) Match(v, expr string, msg ...any) { + a.TB.Helper() + if a.toRegexp(expr).MatchString(v) { + return + } + a.fn(fmterror.Message{ + Method: "Match", + Cause: "failed to match the expected expression", + Message: msg, + Values: []fmterror.Value{ + {Label: "value", Value: v}, + {Label: "expression", Value: expr}, + }, + }) +} + +// NotMatch will check if an expression is not matching a given value. +// NotMatch will fail the test early for receiving an invalid expression. +func (a Asserter) NotMatch(v, expr string, msg ...any) { + a.TB.Helper() + if !a.toRegexp(expr).MatchString(v) { + return + } + a.fn(fmterror.Message{ + Method: "NotMatch", + Cause: "value is matching the expression", + Message: msg, + Values: []fmterror.Value{ + {Label: "value", Value: v}, + {Label: "expression", Value: expr}, + }, + }) +} + +func (a Asserter) toRegexp(expr string) *regexp.Regexp { + a.TB.Helper() + rgx, err := regexp.Compile(expr) + if err != nil { + a.TB.Log(fmterror.Message{ + Method: "NotMatch", + Cause: "invalid expression given", + Values: []fmterror.Value{ + {Label: "expression", Value: expr}, + {Label: "regexp compile error", Value: err}, + }, + }) + a.TB.FailNow() + } + return rgx +} + func (a Asserter) mapContainsSubMap(src reflect.Value, has reflect.Value, msg []any) { a.TB.Helper() for _, key := range has.MapKeys() { diff --git a/assert/Asserter_test.go b/assert/Asserter_test.go index c329eaf..0da4859 100644 --- a/assert/Asserter_test.go +++ b/assert/Asserter_test.go @@ -937,6 +937,60 @@ func TestAsserter_Subset(t *testing.T) { } } +func TestAsserter_Match(t *testing.T) { + t.Run("rgx is incorrect", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { + asserter(dtb).Match("val", `[a-z`) + }) + assert.True(t, dtb.IsFailed) + assert.False(t, out.OK) + }) + t.Run("when value doesn't match the expression", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { + asserter(dtb).Match("42", `[a-z]+`) + }) + assert.True(t, dtb.IsFailed) + assert.True(t, out.OK) + }) + t.Run("when value match the expression", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { + asserter(dtb).Match("42", `[0-9]+`) + }) + assert.False(t, dtb.IsFailed) + assert.True(t, out.OK) + }) +} + +func TestAsserter_NotMatch(t *testing.T) { + t.Run("rgx is incorrect", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { + asserter(dtb).NotMatch("val", `[a-z`) + }) + assert.True(t, dtb.IsFailed) + assert.False(t, out.OK) + }) + t.Run("when value doesn't match the expression", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { + asserter(dtb).NotMatch("42", `[a-z]+`) + }) + assert.False(t, dtb.IsFailed) + assert.True(t, out.OK) + }) + t.Run("when value match the expression", func(t *testing.T) { + dtb := &doubles.TB{} + out := sandbox.Run(func() { + asserter(dtb).NotMatch("42", `[0-9]+`) + }) + assert.True(t, dtb.IsFailed) + assert.True(t, out.OK) + }) +} + func TestAsserter_Contain_map(t *testing.T) { type TestCase struct { Desc string @@ -1685,7 +1739,7 @@ func TestAsserter_NotEmpty(t *testing.T) { doConcurrently(t, func() { *v.V = rnd.Int() }) blk := func() { assert.NotEmpty(t, &v) } - + testcase.Race(blk, blk, blk) }) } diff --git a/assert/example_test.go b/assert/example_test.go index 3f0857c..4097c9c 100644 --- a/assert/example_test.go +++ b/assert/example_test.go @@ -687,3 +687,29 @@ func ExampleOneOf() { it.Must.Equal("bar", got) }, "optional assertion explanation") } + +func ExampleMatch() { + var tb testing.TB + assert.Match(tb, "42", "[0-9]+") + assert.Match(tb, "forty-two", "[a-z]+") + assert.Match(tb, []byte("forty-two"), "[a-z]+") +} + +func ExampleAsserter_Match() { + var tb testing.TB + assert.Must(tb).Match("42", "[0-9]+") + assert.Must(tb).Match("forty-two", "[a-z]+") +} + +func ExampleNotMatch() { + var tb testing.TB + assert.NotMatch(tb, "42", "^[a-z]+") + assert.NotMatch(tb, "forty-two", "^[0-9]+") + assert.NotMatch(tb, []byte("forty-two"), "^[0-9]+") +} + +func ExampleAsserter_NotMatch() { + var tb testing.TB + assert.Must(tb).NotMatch("42", "^[a-z]+") + assert.Must(tb).NotMatch("forty-two", "^[0-9]+") +} diff --git a/assert/pkgfunc.go b/assert/pkgfunc.go index e46fe78..b12a734 100644 --- a/assert/pkgfunc.go +++ b/assert/pkgfunc.go @@ -111,3 +111,13 @@ func NotWithin(tb testing.TB, timeout time.Duration, blk func(context.Context), tb.Helper() Must(tb).NotWithin(timeout, blk, msg...) } + +func Match[T string | []byte](tb testing.TB, v T, expr string, msg ...any) { + tb.Helper() + Must(tb).Match(string(v), expr, msg...) +} + +func NotMatch[T string | []byte](tb testing.TB, v T, expr string, msg ...any) { + tb.Helper() + Must(tb).NotMatch(string(v), expr, msg...) +} diff --git a/assert/pkgfunc_test.go b/assert/pkgfunc_test.go index 56d7282..f19ced8 100644 --- a/assert/pkgfunc_test.go +++ b/assert/pkgfunc_test.go @@ -355,6 +355,54 @@ func TestPublicFunctions(t *testing.T) { assert.NotWithin(tb, 128*time.Millisecond, func(ctx context.Context) {}) }, }, + // .Match + { + Desc: ".Match - happy", + Failed: false, + Assert: func(tb testing.TB) { + assert.Match(tb, "42", "[0-9]+") + assert.Match(tb, "forty-two", "[a-z]+") + assert.Match(tb, []byte("forty-two"), "[a-z]+") + }, + }, + { + Desc: ".Match - rainy value", + Failed: true, + Assert: func(tb testing.TB) { + assert.Match(tb, "42", "[a-z]+") + }, + }, + { + Desc: ".Match - rainy pattern", + Failed: true, + Assert: func(tb testing.TB) { + assert.Match(tb, "42", "[0-9") + }, + }, + // .NotMatch + { + Desc: ".NotMatch - happy", + Failed: false, + Assert: func(tb testing.TB) { + assert.NotMatch(tb, "forty-two", "^[0-9]+") + assert.NotMatch(tb, "42", "^[a-z]+") + assert.NotMatch(tb, []byte("forty-two"), "^[0-9]+") + }, + }, + { + Desc: ".NotMatch - rainy value", + Failed: true, + Assert: func(tb testing.TB) { + assert.NotMatch(tb, "42", "[0-9]+") + }, + }, + { + Desc: ".NotMatch - rainy pattern", + Failed: true, + Assert: func(tb testing.TB) { + assert.NotMatch(tb, "forty-two", "[0-9") + }, + }, } { t.Run(tc.Desc, func(t *testing.T) { stub := &doubles.TB{}