diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_boundaries.py new file mode 100644 index 000000000..f472fcd13 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_boundaries.py @@ -0,0 +1,166 @@ +"""Tests for $exp at representable-range boundaries, including overflow and underflow.""" + +import pytest +from bson import Decimal128 + +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_LARGE_EXPONENT, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_SMALL_EXPONENT, + DOUBLE_MIN_NEGATIVE_SUBNORMAL, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_ZERO, + FLOAT_INFINITY, + INT32_MAX, + INT64_MAX, + INT64_MIN, +) + +# Property [Overflow]: $exp overflows to infinity past the largest representable exponent and +# underflows to zero for large negative inputs. +EXP_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "just_below_overflow", + doc={"value": 709}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(8.218407461554972e307), + msg="$exp should return a large finite double just below the overflow boundary", + ), + ExpressionTestCase( + "overflow", + doc={"value": 710}, + expression={"$exp": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$exp should return infinity past the overflow boundary", + ), + ExpressionTestCase( + "underflow", + doc={"value": -750}, + expression={"$exp": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$exp should underflow to zero for a large negative input", + ), +] + +# Property [Integer Boundaries]: $exp of extreme integer inputs overflows to infinity or +# underflows to zero. +EXP_INTEGER_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + doc={"value": INT32_MAX}, + expression={"$exp": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$exp should return infinity for INT32_MAX", + ), + ExpressionTestCase( + "int64_max", + doc={"value": INT64_MAX}, + expression={"$exp": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$exp should return infinity for INT64_MAX", + ), + ExpressionTestCase( + "int64_min", + doc={"value": INT64_MIN}, + expression={"$exp": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$exp should underflow to zero for INT64_MIN", + ), +] + +# Property [Double Boundaries]: $exp near the double subnormal range returns one, and a large +# double overflows to infinity. +EXP_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for the minimum subnormal double", + ), + ExpressionTestCase( + "double_min_negative_subnormal", + doc={"value": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for the minimum negative subnormal double", + ), + ExpressionTestCase( + "double_near_min", + doc={"value": DOUBLE_NEAR_MIN}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for a near-zero positive double", + ), + ExpressionTestCase( + "double_near_max", + doc={"value": DOUBLE_NEAR_MAX}, + expression={"$exp": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$exp should overflow to infinity for a large double", + ), +] + +# Property [Decimal128 Boundaries]: $exp of extreme decimal128 exponents overflows to infinity or +# underflows to zero, preserving the decimal type. +EXP_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_small_exponent", + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expression={"$exp": ["$value"]}, + expected=Decimal128("1"), + msg="$exp should return one for a decimal128 with a small exponent", + ), + ExpressionTestCase( + "decimal_large_exponent", + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expression={"$exp": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$exp should overflow to infinity for a decimal128 with a large exponent", + ), + ExpressionTestCase( + "decimal_max", + doc={"value": DECIMAL128_MAX}, + expression={"$exp": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$exp should overflow to infinity for decimal128 max", + ), + ExpressionTestCase( + "decimal_min", + doc={"value": DECIMAL128_MIN}, + expression={"$exp": ["$value"]}, + expected=Decimal128("0E-6176"), + msg="$exp should underflow to zero for decimal128 min", + ), +] + +EXP_BOUNDARY_ALL_TESTS = ( + EXP_OVERFLOW_TESTS + + EXP_INTEGER_BOUNDARY_TESTS + + EXP_DOUBLE_BOUNDARY_TESTS + + EXP_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_BOUNDARY_ALL_TESTS)) +def test_exp_boundaries(collection, test_case: ExpressionTestCase): + """Test $exp 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/exp/test_exp_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_errors.py new file mode 100644 index 000000000..00aebadd6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_errors.py @@ -0,0 +1,76 @@ +"""Tests for $exp type and arity errors.""" + +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 ( + EXPRESSION_TYPE_MISMATCH_ERROR, + NON_NUMERIC_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Type Strictness]: $exp rejects non-numeric input types. +EXP_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + f"type_{tid}", + doc={"value": val}, + expression={"$exp": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg=f"$exp 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]: $exp requires exactly one argument. +EXP_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_zero", + doc={}, + expression={"$exp": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$exp should reject zero arguments", + ), + ExpressionTestCase( + "arity_two", + doc={}, + expression={"$exp": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$exp should reject two arguments", + ), +] + +EXP_ERROR_ALL_TESTS = EXP_TYPE_ERROR_TESTS + EXP_ARITY_ERROR_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_ERROR_ALL_TESTS)) +def test_exp_errors(collection, test_case: ExpressionTestCase): + """Test $exp type and arity 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/exp/test_exp_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_input_forms.py new file mode 100644 index 000000000..729d7713b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_input_forms.py @@ -0,0 +1,97 @@ +"""Tests for $exp argument forms, literal input, field paths, and nested expression input.""" + +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.error_codes import NON_NUMERIC_TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Argument Form]: $exp accepts its single argument bare or wrapped in a one-element +# array. +EXP_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "form_array", + doc={"value": 1}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(2.718281828459045), + msg="$exp should accept its argument wrapped in a one-element array", + ), + ExpressionTestCase( + "form_bare", + doc={"value": 1}, + expression={"$exp": "$value"}, + expected=pytest.approx(2.718281828459045), + msg="$exp should accept its argument without an array wrapper", + ), +] + +# Property [Literal Input]: $exp evaluates an inline literal argument, not only document fields. +EXP_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$exp": [1]}, + expected=pytest.approx(2.718281828459045), + msg="$exp should return e for an inline literal argument", + ), +] + +# Property [Expression Input]: $exp evaluates a nested expression argument before exponentiating. +EXP_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_exp", + doc={}, + expression={"$exp": {"$exp": 1}}, + expected=pytest.approx(15.154262241479262), + msg="$exp should evaluate a nested $exp expression argument", + ), +] + +# Property [Field Path Input]: $exp resolves a field path argument. A dotted path into a nested +# object yields the referenced value; a path over an array (an array-of-objects path, or a numeric +# component applied over an array) resolves to an array, which $exp rejects as a non-numeric type. +EXP_FIELD_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field_path", + doc={"a": {"b": 1}}, + expression={"$exp": "$a.b"}, + expected=pytest.approx(2.718281828459045), + msg="$exp should resolve a dotted field path into a nested object", + ), + ExpressionTestCase( + "composite_array_field_path", + doc={"a": [{"b": 1}, {"b": 2}]}, + expression={"$exp": "$a.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$exp should reject a field path that resolves to an array from an array of objects", + ), + ExpressionTestCase( + "array_index_field_path", + doc={"a": [1, 2]}, + expression={"$exp": "$a.0"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$exp should reject a numeric path component over an array, which resolves non-numeric", + ), +] + +EXP_INPUT_FORM_TESTS = ( + EXP_ARGUMENT_FORM_TESTS + EXP_LITERAL_TESTS + EXP_EXPRESSION_INPUT_TESTS + EXP_FIELD_PATH_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_INPUT_FORM_TESTS)) +def test_exp_input_forms(collection, test_case: ExpressionTestCase): + """Test $exp argument form, literal, field path, 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/exp/test_exp_magnitude.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_magnitude.py new file mode 100644 index 000000000..204537099 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_magnitude.py @@ -0,0 +1,140 @@ +"""Tests for $exp core exponentiation values, including signed zero.""" + +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 [Exponent Value]: $exp returns e raised to the input power. +EXP_VALUE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_int32", + doc={"value": 1}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(2.718281828459045), + msg="$exp should return e for int32 one", + ), + ExpressionTestCase( + "positive_int64", + doc={"value": Int64(1)}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(2.718281828459045), + msg="$exp should return e for int64 one", + ), + ExpressionTestCase( + "positive_double", + doc={"value": 1.5}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(4.4816890703380645), + msg="$exp should return e raised to 1.5 for double 1.5", + ), + ExpressionTestCase( + "positive_decimal", + doc={"value": Decimal128("1")}, + expression={"$exp": ["$value"]}, + expected=Decimal128("2.718281828459045235360287471352662"), + msg="$exp should return e for decimal128 one", + ), + ExpressionTestCase( + "negative_int32", + doc={"value": -1}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(0.36787944117144233), + msg="$exp should return e raised to -1 for int32 negative one", + ), + ExpressionTestCase( + "negative_int64", + doc={"value": Int64(-1)}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(0.36787944117144233), + msg="$exp should return e raised to -1 for int64 negative one", + ), + ExpressionTestCase( + "negative_double", + doc={"value": -1.5}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(0.22313016014842982), + msg="$exp should return e raised to -1.5 for double -1.5", + ), + ExpressionTestCase( + "negative_decimal", + doc={"value": Decimal128("-1")}, + expression={"$exp": ["$value"]}, + expected=Decimal128("0.3678794411714423215955237701614609"), + msg="$exp should return e raised to -1 for decimal128 negative one", + ), +] + +# Property [Zero]: $exp of any zero, including negative zero, returns one. +EXP_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_int32", + doc={"value": 0}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for int32 zero", + ), + ExpressionTestCase( + "zero_int64", + doc={"value": INT64_ZERO}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for int64 zero", + ), + ExpressionTestCase( + "zero_double", + doc={"value": DOUBLE_ZERO}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for double zero", + ), + ExpressionTestCase( + "negative_zero_double", + doc={"value": DOUBLE_NEGATIVE_ZERO}, + expression={"$exp": ["$value"]}, + expected=1.0, + msg="$exp should return one for negative zero double", + ), + ExpressionTestCase( + "zero_decimal", + doc={"value": DECIMAL128_ZERO}, + expression={"$exp": ["$value"]}, + expected=Decimal128("1"), + msg="$exp should return one for decimal128 zero", + ), + ExpressionTestCase( + "negative_zero_decimal", + doc={"value": DECIMAL128_NEGATIVE_ZERO}, + expression={"$exp": ["$value"]}, + expected=Decimal128("1"), + msg="$exp should return one for negative zero decimal128", + ), +] + +EXP_MAGNITUDE_ALL_TESTS = EXP_VALUE_TESTS + EXP_ZERO_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_MAGNITUDE_ALL_TESTS)) +def test_exp_magnitude(collection, test_case: ExpressionTestCase): + """Test $exp exponentiation value 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/exp/test_exp_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_non_finite.py new file mode 100644 index 000000000..c5cf04512 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_non_finite.py @@ -0,0 +1,86 @@ +"""Tests for $exp of infinity and NaN inputs.""" + +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, + DECIMAL128_ZERO, + DOUBLE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# Property [Infinity]: $exp of positive infinity is infinity and of negative infinity is zero. +EXP_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_infinity", + doc={"value": FLOAT_INFINITY}, + expression={"$exp": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$exp should return infinity for float infinity", + ), + ExpressionTestCase( + "float_negative_infinity", + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expression={"$exp": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$exp should return zero for float negative infinity", + ), + ExpressionTestCase( + "decimal128_infinity", + doc={"value": DECIMAL128_INFINITY}, + expression={"$exp": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$exp should return decimal128 infinity for decimal128 infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$exp": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$exp should return decimal128 zero for decimal128 negative infinity", + ), +] + +# Property [NaN]: $exp of NaN returns NaN of the same type. +EXP_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_nan", + doc={"value": FLOAT_NAN}, + expression={"$exp": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$exp should return NaN for float NaN", + ), + ExpressionTestCase( + "decimal128_nan", + doc={"value": DECIMAL128_NAN}, + expression={"$exp": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$exp should return NaN for decimal128 NaN", + ), +] + +EXP_NON_FINITE_TESTS = EXP_INFINITY_TESTS + EXP_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_NON_FINITE_TESTS)) +def test_exp_non_finite(collection, test_case: ExpressionTestCase): + """Test $exp 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/exp/test_exp_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_null.py new file mode 100644 index 000000000..180aea15f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_null.py @@ -0,0 +1,45 @@ +"""Tests for $exp null and missing field propagation.""" + +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]: $exp of null or a missing field returns null. +EXP_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None}, + expression={"$exp": ["$value"]}, + expected=None, + msg="$exp should return null for null input", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$exp": [MISSING]}, + expected=None, + msg="$exp should return null for a missing field", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_NULL_TESTS)) +def test_exp_null(collection, test_case: ExpressionTestCase): + """Test $exp 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/exp/test_exp_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_return_type.py new file mode 100644 index 000000000..e54827dee --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/exp/test_exp_return_type.py @@ -0,0 +1,58 @@ +"""Tests for $exp return type across numeric input types.""" + +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]: $exp returns double for int32, int64, and double inputs, and decimal for +# decimal128 inputs. +EXP_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": 1}, + expression={"$type": {"$exp": "$value"}}, + expected="double", + msg="$exp should return a double for an int32 input", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(1)}, + expression={"$type": {"$exp": "$value"}}, + expected="double", + msg="$exp should return a double for an int64 input", + ), + ExpressionTestCase( + "return_type_double", + doc={"value": 1.0}, + expression={"$type": {"$exp": "$value"}}, + expected="double", + msg="$exp should return a double for a double input", + ), + ExpressionTestCase( + "return_type_decimal", + doc={"value": Decimal128("1")}, + expression={"$type": {"$exp": "$value"}}, + expected="decimal", + msg="$exp should return a decimal for a decimal128 input", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(EXP_RETURN_TYPE_TESTS)) +def test_exp_return_type(collection, test_case: ExpressionTestCase): + """Test $exp return type 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, + )