diff --git a/checker.go b/checker.go index e378680..899ab3c 100644 --- a/checker.go +++ b/checker.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "reflect" "regexp" "strings" @@ -505,6 +506,126 @@ func (c *boolChecker) ArgNames() []string { return []string{"got"} } +// IsLessThan is a Checker verifying that the provided value is less than the +// given reference value. +// +// For instance: +// +// c.Assert(value, qt.IsLessThan, 0) +// c.Assert(value, qt.IsLessThan, 99.99) +var IsLessThan Checker = newBinaryArithmeticChecker(func(value, reference *big.Float) error { + if value.Cmp(reference) != -1 { + return errors.New("value is not less than reference") + } + return nil +}) + +// IsLessThanOrEqual is a Checker verifying that the provided value is less than +// or equal the given reference value. +// +// For instance: +// +// c.Assert(value, qt.IsLessThanOrEqual, 0) +// c.Assert(value, qt.IsLessThanOrEqual, 99.99) +var IsLessThanOrEqual Checker = newBinaryArithmeticChecker(func(value, reference *big.Float) error { + if value.Cmp(reference) == 1 { + return errors.New("value is not less than or equal reference") + } + return nil +}) + +// IsGreaterThan is a Checker verifying that the provided value is greater than +// the given reference value. +// +// For instance: +// +// c.Assert(value, qt.IsGreaterThan, 0) +// c.Assert(value, qt.IsGreaterThan, 99.99) +var IsGreaterThan Checker = newBinaryArithmeticChecker(func(value, reference *big.Float) error { + if value.Cmp(reference) != 1 { + return errors.New("value is not greater than reference") + } + return nil +}) + +// IsGreaterThanOrEqual is a Checker verifying that the provided value is +// greater than or equal the given reference value. +// +// For instance: +// +// c.Assert(value, qt.IsGreaterThanOrEqual, 0) +// c.Assert(value, qt.IsGreaterThanOrEqual, 99.99) +var IsGreaterThanOrEqual Checker = newBinaryArithmeticChecker(func(value, reference *big.Float) error { + if value.Cmp(reference) == -1 { + return errors.New("value is not greater than or equal reference") + } + return nil +}) + +// binaryArithmeticChecker is a generic arithmetic binary checker that accepts +// two numeric operands and a check function. The raw operands (got and want) +// are first converted to *big.Float and then will be submitted to the check +// function to perform the assertion/check. +type binaryArithmeticChecker struct { + argNames + f func(value, reference *big.Float) error +} + +func newBinaryArithmeticChecker(f func(value, reference *big.Float) error) *binaryArithmeticChecker { + return &binaryArithmeticChecker{ + argNames: []string{"value", "reference"}, + f: f, + } +} + +// Check implements Checker.Check by checking that args[0](got) == true. +func (c *binaryArithmeticChecker) Check(got interface{}, args []interface{}, note func(key string, value interface{})) (err error) { + reference := args[0] + + bigReference := valueToBigFloat(reference) + if bigReference == nil { + return BadCheckf("reference should be of a numeric type but it is %T", reference) + } + + bigValue := valueToBigFloat(got) + if bigValue == nil { + return BadCheckf("value should be of a numeric type but it is %T", got) + } + + return c.f(bigValue, bigReference) +} + +func valueToBigFloat(value interface{}) *big.Float { + switch v := value.(type) { + case int: + return big.NewFloat(0).SetInt64(int64(v)) + case int8: + return big.NewFloat(0).SetInt64(int64(v)) + case int16: + return big.NewFloat(0).SetInt64(int64(v)) + case int32: + return big.NewFloat(0).SetInt64(int64(v)) + case int64: + return big.NewFloat(0).SetInt64(int64(v)) + case uint: + return big.NewFloat(0).SetUint64(uint64(v)) + case uint8: + return big.NewFloat(0).SetUint64(uint64(v)) + case uint16: + return big.NewFloat(0).SetUint64(uint64(v)) + case uint32: + return big.NewFloat(0).SetUint64(uint64(v)) + case uint64: + return big.NewFloat(0).SetUint64(uint64(v)) + case float32: + return big.NewFloat(float64(v)) + case float64: + return big.NewFloat(v) + default: + return nil + } +} + // Not returns a Checker negating the given Checker. // // For instance: diff --git a/checker_arithmetic_test.go b/checker_arithmetic_test.go new file mode 100644 index 0000000..cbd352a --- /dev/null +++ b/checker_arithmetic_test.go @@ -0,0 +1,159 @@ +// Licensed under the MIT license, see LICENSE file for details. + +package quicktest_test + +import ( + "fmt" + "math/big" + "testing" + + "github.com/frankban/quicktest" +) + +// TestBinaryArithmeticCheckerMatrix verifies that the binary arithmetic checker +// correctly handles all possible types of input arguments (got/reference). +func TestBinaryArithmeticCheckerMatrix(t *testing.T) { + numerics := []interface{}{ + int(99), + int8(99), + int16(99), + int32(99), + int64(99), + uint(99), + uint8(99), + uint16(99), + uint32(99), + uint64(99), + float32(99), + float64(99), + } + + asBigFloat := big.NewFloat(99) + + for _, got := range numerics { + for _, reference := range numerics { + t.Run(fmt.Sprintf("%T and %T", got, reference), func(t *testing.T) { + checker := quicktest.NewBinaryArithmeticChecker(func(value, reference *big.Float) error { + if value.Cmp(asBigFloat) != 0 { + t.Fatal("unexpected value") + } + if reference.Cmp(asBigFloat) != 0 { + t.Fatal("unexpected value") + } + return nil + }) + + err := checker.Check(got, []interface{}{reference}, func(key string, value interface{}) {}) + if err != nil { + t.Fatal("expected nil error from checker") + } + }) + } + } +} + +func TestBinaryArithmeticCheckerValidation(t *testing.T) { + tests := []struct { + about string + value interface{} + reference interface{} + expectedError string + }{{ + about: "value is boolean", + value: true, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is bool", + }, { + about: "reference is boolean", + value: 0, + reference: true, + expectedError: "bad check: reference should be of a numeric type but it is bool", + }, { + about: "value is string", + value: "foo", + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is string", + }, { + about: "reference is string", + value: 0, + reference: "foo", + expectedError: "bad check: reference should be of a numeric type but it is string", + }, { + about: "value is function", + value: func() {}, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is func()", + }, { + about: "reference is function", + value: 0, + reference: func() {}, + expectedError: "bad check: reference should be of a numeric type but it is func()", + }, { + about: "value is slice", + value: []int{}, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is []int", + }, { + about: "reference is slice", + value: 0, + reference: []int{}, + expectedError: "bad check: reference should be of a numeric type but it is []int", + }, { + about: "value is map", + value: map[string]string{}, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is map[string]string", + }, { + about: "reference is map", + value: 0, + reference: map[string]string{}, + expectedError: "bad check: reference should be of a numeric type but it is map[string]string", + }, { + about: "value is struct", + value: struct{}{}, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is struct {}", + }, { + about: "reference is struct", + value: 0, + reference: struct{}{}, + expectedError: "bad check: reference should be of a numeric type but it is struct {}", + }, { + about: "value is pointer", + value: &struct{}{}, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is *struct {}", + }, { + about: "reference is pointer", + value: 0, + reference: &struct{}{}, + expectedError: "bad check: reference should be of a numeric type but it is *struct {}", + }, { + about: "value is nil", + value: nil, + reference: 0, + expectedError: "bad check: value should be of a numeric type but it is ", + }, { + about: "reference is nil", + value: 0, + reference: nil, + expectedError: "bad check: reference should be of a numeric type but it is ", + }, + } + + noopChecker := quicktest.NewBinaryArithmeticChecker(func(value, reference *big.Float) error { + return nil + }) + + for _, test := range tests { + t.Run(test.about, func(t *testing.T) { + err := noopChecker.Check(test.value, []interface{}{test.reference}, func(key string, value interface{}) {}) + if err == nil { + t.Fatal("unexpected nil error") + } + if err.Error() != test.expectedError { + t.Fatal("unexpected error message") + } + }) + } +} diff --git a/checker_test.go b/checker_test.go index c869a62..34b0517 100644 --- a/checker_test.go +++ b/checker_test.go @@ -2498,6 +2498,188 @@ error: value: "bad wolf" `, +}, { + about: "IsLessThan: success", + checker: qt.IsLessThan, + got: 99, + args: []interface{}{9999}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(99) +reference: + int(9999) +`, +}, { + about: "IsLessThan: success (different types)", + checker: qt.IsLessThan, + got: 99, + args: []interface{}{9999.0}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(99) +reference: + float64(9999) +`, +}, { + about: "IsLessThan: failure", + checker: qt.IsLessThan, + got: 9999, + args: []interface{}{99}, + expectedCheckFailure: ` +error: + value is not less than reference +value: + int(9999) +reference: + int(99) +`, +}, { + about: "IsLessThanOrEqual: success (less than)", + checker: qt.IsLessThanOrEqual, + got: 99, + args: []interface{}{9999}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(99) +reference: + int(9999) +`, +}, { + about: "IsLessThanOrEqual: success (equal)", + checker: qt.IsLessThanOrEqual, + got: 99, + args: []interface{}{99}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(99) +reference: + +`, +}, { + about: "IsLessThanOrEqual: success (different types)", + checker: qt.IsLessThanOrEqual, + got: 99, + args: []interface{}{9999.0}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(99) +reference: + float64(9999) +`, +}, { + about: "IsLessThanOrEqual: failure", + checker: qt.IsLessThanOrEqual, + got: 9999, + args: []interface{}{99}, + expectedCheckFailure: ` +error: + value is not less than or equal reference +value: + int(9999) +reference: + int(99) +`, +}, { + about: "IsGreaterThan: success", + checker: qt.IsGreaterThan, + got: 9999, + args: []interface{}{99}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(9999) +reference: + int(99) +`, +}, { + about: "IsGreaterThan: success (different types)", + checker: qt.IsGreaterThan, + got: 9999, + args: []interface{}{99.0}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(9999) +reference: + float64(99) +`, +}, { + about: "IsGreaterThan: failure", + checker: qt.IsGreaterThan, + got: 99, + args: []interface{}{9999}, + expectedCheckFailure: ` +error: + value is not greater than reference +value: + int(99) +reference: + int(9999) +`, +}, { + about: "IsGreaterThanOrEqual: success (greater than)", + checker: qt.IsGreaterThanOrEqual, + got: 9999, + args: []interface{}{99}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(9999) +reference: + int(99) +`, +}, { + about: "IsGreaterThanOrEqual: success (equal)", + checker: qt.IsGreaterThanOrEqual, + got: 99, + args: []interface{}{99}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(99) +reference: + +`, +}, { + about: "IsGreaterThanOrEqual: success (different types)", + checker: qt.IsGreaterThanOrEqual, + got: 9999, + args: []interface{}{99.0}, + expectedNegateFailure: ` +error: + unexpected success +value: + int(9999) +reference: + float64(99) +`, +}, { + about: "IsGreaterThanOrEqual: failure", + checker: qt.IsGreaterThanOrEqual, + got: 99, + args: []interface{}{9999}, + expectedCheckFailure: ` +error: + value is not greater than or equal reference +value: + int(99) +reference: + int(9999) +`, }, { about: "Not: success", checker: qt.Not(qt.IsNil), diff --git a/export_test.go b/export_test.go index 06b17b7..5cf868f 100644 --- a/export_test.go +++ b/export_test.go @@ -3,6 +3,7 @@ package quicktest var ( - Prefixf = prefixf - TestingVerbose = &testingVerbose + Prefixf = prefixf + TestingVerbose = &testingVerbose + NewBinaryArithmeticChecker = newBinaryArithmeticChecker )