diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_boundaries.py new file mode 100644 index 000000000..ecb4abdb6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_boundaries.py @@ -0,0 +1,250 @@ +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_NEGATIVE_SUBNORMAL, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_PRECISION_LOSS, + INT32_MAX, + INT32_MAX_MINUS_1, + INT32_MIN, + INT32_MIN_PLUS_1, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN_PLUS_1, +) + +# Property [Int32 Boundaries]: $abs at int32 limits returns the magnitude, promoting to long when +# the result overflows int32. +ABS_INT32_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + doc={"value": INT32_MAX}, + expression={"$abs": ["$value"]}, + expected=INT32_MAX, + msg="$abs should return INT32_MAX for INT32_MAX", + ), + ExpressionTestCase( + "int32_max_minus_1", + doc={"value": INT32_MAX_MINUS_1}, + expression={"$abs": ["$value"]}, + expected=INT32_MAX_MINUS_1, + msg="$abs should return INT32_MAX-1 for INT32_MAX-1", + ), + ExpressionTestCase( + "int32_min_plus_1", + doc={"value": INT32_MIN_PLUS_1}, + expression={"$abs": ["$value"]}, + expected=INT32_MAX, + msg="$abs should return INT32_MAX for INT32_MIN+1", + ), + ExpressionTestCase( + "int32_min", + doc={"value": INT32_MIN}, + expression={"$abs": ["$value"]}, + expected=Int64(INT32_OVERFLOW), + msg="$abs should promote to int64 for INT32_MIN", + ), + ExpressionTestCase( + "int32_overflow", + doc={"value": INT32_OVERFLOW}, + expression={"$abs": ["$value"]}, + expected=Int64(INT32_OVERFLOW), + msg="$abs should return the same long value for INT32_OVERFLOW", + ), + ExpressionTestCase( + "int32_underflow", + doc={"value": INT32_UNDERFLOW}, + expression={"$abs": ["$value"]}, + expected=Int64(-INT32_UNDERFLOW), + msg="$abs should return the negated long value for INT32_UNDERFLOW", + ), +] + +# Property [Int64 Boundaries]: $abs at int64 limits returns the magnitude as a long. +ABS_INT64_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_max", + doc={"value": INT64_MAX}, + expression={"$abs": ["$value"]}, + expected=INT64_MAX, + msg="$abs should return INT64_MAX for INT64_MAX", + ), + ExpressionTestCase( + "int64_max_minus_1", + doc={"value": INT64_MAX_MINUS_1}, + expression={"$abs": ["$value"]}, + expected=INT64_MAX_MINUS_1, + msg="$abs should return INT64_MAX-1 for INT64_MAX-1", + ), + ExpressionTestCase( + "int64_min_plus_1", + doc={"value": INT64_MIN_PLUS_1}, + expression={"$abs": ["$value"]}, + expected=INT64_MAX, + msg="$abs should return INT64_MAX for INT64_MIN+1", + ), +] + +# Property [Double Boundaries]: $abs preserves double magnitude across the representable range. +ABS_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_MIN_SUBNORMAL, + msg="$abs should return the same value for the minimum subnormal double", + ), + ExpressionTestCase( + "double_min_negative_subnormal", + doc={"value": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_MIN_SUBNORMAL, + msg="$abs should return the positive subnormal for the minimum negative subnormal double", + ), + ExpressionTestCase( + "double_near_min", + doc={"value": DOUBLE_NEAR_MIN}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_NEAR_MIN, + msg="$abs should return the same value for a near-min double", + ), + ExpressionTestCase( + "double_near_max", + doc={"value": DOUBLE_NEAR_MAX}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_NEAR_MAX, + msg="$abs should return the same value for a near-max double", + ), + ExpressionTestCase( + "double_max_safe_integer", + doc={"value": float(DOUBLE_MAX_SAFE_INTEGER)}, + expression={"$abs": ["$value"]}, + expected=float(DOUBLE_MAX_SAFE_INTEGER), + msg="$abs should return the same value for the max safe integer double", + ), + ExpressionTestCase( + "double_precision_loss", + doc={"value": float(DOUBLE_PRECISION_LOSS)}, + expression={"$abs": ["$value"]}, + expected=float(DOUBLE_PRECISION_LOSS), + msg="$abs should return the same value for a precision loss double", + ), + ExpressionTestCase( + "double_just_below_half", + doc={"value": DOUBLE_JUST_BELOW_HALF}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_JUST_BELOW_HALF, + msg="$abs should return the same value for a double just below half", + ), + ExpressionTestCase( + "double_just_above_half", + doc={"value": DOUBLE_JUST_ABOVE_HALF}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_JUST_ABOVE_HALF, + msg="$abs should return the same value for a double just above half", + ), +] + +# Property [Decimal128 Boundaries]: $abs preserves decimal128 magnitude and precision, including +# trailing zeros. +ABS_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max", + doc={"value": DECIMAL128_MAX}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_MAX, + msg="$abs should return the same value for decimal128 max", + ), + ExpressionTestCase( + "decimal128_min", + doc={"value": DECIMAL128_MIN}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_MAX, + msg="$abs should return decimal128 max for decimal128 min", + ), + ExpressionTestCase( + "decimal128_large_exponent", + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="$abs should return the same value for a decimal128 with a large exponent", + ), + ExpressionTestCase( + "decimal128_small_exponent", + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_SMALL_EXPONENT, + msg="$abs should return the same value for a decimal128 with a small exponent", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + doc={"value": DECIMAL128_TRAILING_ZERO}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_TRAILING_ZERO, + msg="$abs should preserve a decimal128 trailing zero", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + doc={"value": DECIMAL128_MANY_TRAILING_ZEROS}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_MANY_TRAILING_ZEROS, + msg="$abs should preserve decimal128 many trailing zeros", + ), + ExpressionTestCase( + "decimal128_just_below_half", + doc={"value": DECIMAL128_JUST_BELOW_HALF}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_JUST_BELOW_HALF, + msg="$abs should return the same value for a decimal128 just below half", + ), + ExpressionTestCase( + "decimal128_just_above_half", + doc={"value": DECIMAL128_JUST_ABOVE_HALF}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_JUST_ABOVE_HALF, + msg="$abs should return the same value for a decimal128 just above half", + ), +] + +ABS_BOUNDARY_ALL_TESTS = ( + ABS_INT32_BOUNDARY_TESTS + + ABS_INT64_BOUNDARY_TESTS + + ABS_DOUBLE_BOUNDARY_TESTS + + ABS_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_BOUNDARY_ALL_TESTS)) +def test_abs_boundaries(collection, test_case: ExpressionTestCase): + """Test $abs representable-range boundary cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_errors.py new file mode 100644 index 000000000..a1a35e81e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_errors.py @@ -0,0 +1,90 @@ +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + ABS_OVERFLOW_ERROR, + EXPRESSION_TYPE_MISMATCH_ERROR, + NON_NUMERIC_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + INT64_MIN, +) + +# Property [Type Strictness]: $abs rejects non-numeric input types. +ABS_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + f"type_{tid}", + doc={"value": val}, + expression={"$abs": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg=f"$abs should reject a {tid} input", + ) + for tid, val in [ + ("string", "abc"), + ("bool", True), + ("array", [1, 2]), + ("object", {"a": 1}), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("regex", Regex("abc")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] +] + +# Property [Arity]: $abs requires exactly one argument. +ABS_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_zero", + doc={}, + expression={"$abs": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$abs should reject zero arguments", + ), + ExpressionTestCase( + "arity_two", + doc={}, + expression={"$abs": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$abs should reject two arguments", + ), +] + +# Property [Overflow]: $abs of the minimum int64 errors because the positive result is not +# representable as a long. +ABS_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_min_overflow", + doc={"value": INT64_MIN}, + expression={"$abs": ["$value"]}, + error_code=ABS_OVERFLOW_ERROR, + msg="$abs should error on overflow for INT64_MIN", + ), +] + +ABS_ERROR_ALL_TESTS = ABS_TYPE_ERROR_TESTS + ABS_ARITY_ERROR_TESTS + ABS_OVERFLOW_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_ERROR_ALL_TESTS)) +def test_abs_errors(collection, test_case: ExpressionTestCase): + """Test $abs type, arity, and overflow error cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_input_forms.py new file mode 100644 index 000000000..aa8081262 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_input_forms.py @@ -0,0 +1,66 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Argument Form]: $abs accepts its single argument bare or wrapped in a one-element +# array. +ABS_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "form_array", + doc={"value": -5}, + expression={"$abs": ["$value"]}, + expected=5, + msg="$abs should accept its argument wrapped in a one-element array", + ), + ExpressionTestCase( + "form_bare", + doc={"value": -5}, + expression={"$abs": "$value"}, + expected=5, + msg="$abs should accept its argument without an array wrapper", + ), +] + +# Property [Literal Input]: $abs evaluates an inline literal argument, not only document fields. +ABS_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$abs": [-5]}, + expected=5, + msg="$abs should return the absolute value of an inline literal argument", + ), +] + +# Property [Expression Input]: $abs evaluates a nested expression argument before taking the +# absolute value. +ABS_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_abs", + doc={"value": -4}, + expression={"$abs": {"$abs": "$value"}}, + expected=4, + msg="$abs should evaluate a nested $abs expression argument", + ), +] + +ABS_INPUT_FORM_TESTS = ABS_ARGUMENT_FORM_TESTS + ABS_LITERAL_TESTS + ABS_EXPRESSION_INPUT_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_INPUT_FORM_TESTS)) +def test_abs_input_forms(collection, test_case: ExpressionTestCase): + """Test $abs argument form, literal, and nested expression input cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_magnitude.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_magnitude.py new file mode 100644 index 000000000..361d6ac65 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_magnitude.py @@ -0,0 +1,138 @@ +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + INT64_ZERO, +) + +# Property [Magnitude]: $abs returns the absolute value of a number, preserving its numeric type. +ABS_MAGNITUDE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_int32", + doc={"value": 1}, + expression={"$abs": ["$value"]}, + expected=1, + msg="$abs should return the same value for a positive int32", + ), + ExpressionTestCase( + "negative_int32", + doc={"value": -1}, + expression={"$abs": ["$value"]}, + expected=1, + msg="$abs should return the positive value for a negative int32", + ), + ExpressionTestCase( + "positive_int64", + doc={"value": Int64(1)}, + expression={"$abs": ["$value"]}, + expected=Int64(1), + msg="$abs should return the same value for a positive int64", + ), + ExpressionTestCase( + "negative_int64", + doc={"value": Int64(-1)}, + expression={"$abs": ["$value"]}, + expected=Int64(1), + msg="$abs should return the positive value for a negative int64", + ), + ExpressionTestCase( + "positive_double", + doc={"value": 1.5}, + expression={"$abs": ["$value"]}, + expected=1.5, + msg="$abs should return the same value for a positive double", + ), + ExpressionTestCase( + "negative_double", + doc={"value": -1.5}, + expression={"$abs": ["$value"]}, + expected=1.5, + msg="$abs should return the positive value for a negative double", + ), + ExpressionTestCase( + "positive_decimal", + doc={"value": Decimal128("1")}, + expression={"$abs": ["$value"]}, + expected=Decimal128("1"), + msg="$abs should return the same value for a positive decimal128", + ), + ExpressionTestCase( + "negative_decimal", + doc={"value": Decimal128("-1")}, + expression={"$abs": ["$value"]}, + expected=Decimal128("1"), + msg="$abs should return the positive value for a negative decimal128", + ), +] + +# Property [Zero]: $abs of positive or negative zero returns positive zero for each numeric type. +ABS_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_int32", + doc={"value": 0}, + expression={"$abs": ["$value"]}, + expected=0, + msg="$abs should return zero for int32 zero", + ), + ExpressionTestCase( + "zero_int64", + doc={"value": INT64_ZERO}, + expression={"$abs": ["$value"]}, + expected=INT64_ZERO, + msg="$abs should return zero for int64 zero", + ), + ExpressionTestCase( + "zero_double", + doc={"value": DOUBLE_ZERO}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$abs should return zero for double zero", + ), + ExpressionTestCase( + "negative_zero_double", + doc={"value": DOUBLE_NEGATIVE_ZERO}, + expression={"$abs": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$abs should return positive zero for negative zero double", + ), + ExpressionTestCase( + "zero_decimal", + doc={"value": DECIMAL128_ZERO}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$abs should return zero for decimal128 zero", + ), + ExpressionTestCase( + "negative_zero_decimal", + doc={"value": DECIMAL128_NEGATIVE_ZERO}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$abs should return positive zero for negative zero decimal128", + ), +] + +ABS_MAGNITUDE_ALL_TESTS = ABS_MAGNITUDE_TESTS + ABS_ZERO_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_MAGNITUDE_ALL_TESTS)) +def test_abs_magnitude(collection, test_case: ExpressionTestCase): + """Test $abs magnitude and signed-zero cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_non_finite.py new file mode 100644 index 000000000..f9d8bf10f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_non_finite.py @@ -0,0 +1,82 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# Property [Infinity]: $abs of positive or negative infinity returns positive infinity. +ABS_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_infinity", + doc={"value": FLOAT_INFINITY}, + expression={"$abs": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$abs should return float infinity for float infinity", + ), + ExpressionTestCase( + "float_negative_infinity", + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expression={"$abs": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$abs should return positive infinity for float negative infinity", + ), + ExpressionTestCase( + "decimal128_infinity", + doc={"value": DECIMAL128_INFINITY}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$abs should return decimal128 infinity for decimal128 infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$abs should return decimal128 positive infinity for decimal128 negative infinity", + ), +] + +# Property [NaN]: $abs of NaN returns NaN of the same type. +ABS_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_nan", + doc={"value": FLOAT_NAN}, + expression={"$abs": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$abs should return NaN for float NaN", + ), + ExpressionTestCase( + "decimal128_nan", + doc={"value": DECIMAL128_NAN}, + expression={"$abs": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$abs should return NaN for decimal128 NaN", + ), +] + +ABS_NON_FINITE_TESTS = ABS_INFINITY_TESTS + ABS_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_NON_FINITE_TESTS)) +def test_abs_non_finite(collection, test_case: ExpressionTestCase): + """Test $abs infinity and NaN cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_null.py new file mode 100644 index 000000000..5b6b4fa57 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_null.py @@ -0,0 +1,43 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + MISSING, +) + +# Property [Null Propagation]: $abs of null or a missing field returns null. +ABS_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None}, + expression={"$abs": ["$value"]}, + expected=None, + msg="$abs should return null for null input", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$abs": [MISSING]}, + expected=None, + msg="$abs should return null for a missing field", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_NULL_TESTS)) +def test_abs_null(collection, test_case: ExpressionTestCase): + """Test $abs null and missing field propagation cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_return_type.py new file mode 100644 index 000000000..9dfcb89ef --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/abs/test_abs_return_type.py @@ -0,0 +1,55 @@ +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Return Type]: $abs preserves the numeric type of its input. +ABS_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": -5}, + expression={"$type": {"$abs": "$value"}}, + expected="int", + msg="$abs should preserve int32 type", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(-5)}, + expression={"$type": {"$abs": "$value"}}, + expected="long", + msg="$abs should preserve int64 type", + ), + ExpressionTestCase( + "return_type_double", + doc={"value": -5.0}, + expression={"$type": {"$abs": "$value"}}, + expected="double", + msg="$abs should preserve double type", + ), + ExpressionTestCase( + "return_type_decimal", + doc={"value": Decimal128("-5")}, + expression={"$type": {"$abs": "$value"}}, + expected="decimal", + msg="$abs should preserve decimal128 type", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ABS_RETURN_TYPE_TESTS)) +def test_abs_return_type(collection, test_case: ExpressionTestCase): + """Test $abs return type preservation cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + )