From ef7b95c9eea2dc546b9b3c60375ca240af378ed4 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Wed, 1 Jul 2026 14:19:20 -0700 Subject: [PATCH 1/3] Add $floor arithmetic expression tests This change adds tests for the $floor arithmetic expression. It was originally authored by @vic-tsang. Closes #276 Co-authored-by: Victor Tsang Signed-off-by: Daniel Frankcom --- .../arithmetic/floor/test_floor_boundaries.py | 224 ++++++++++++++++++ .../arithmetic/floor/test_floor_core.py | 159 +++++++++++++ .../floor/test_floor_nan_infinity.py | 84 +++++++ .../floor/test_floor_null_missing.py | 41 ++++ .../floor/test_floor_return_type.py | 63 +++++ .../floor/test_floor_type_errors.py | 75 ++++++ 6 files changed, 646 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_nan_infinity.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_null_missing.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_return_type.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_type_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py new file mode 100644 index 000000000..5013b7dd5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py @@ -0,0 +1,224 @@ +"""Tests for $floor at representable-range, subnormal, and decimal128 exponent boundaries.""" + +from __future__ import annotations + +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_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NAN, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_ZERO, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_MIN_NEGATIVE_SUBNORMAL, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_ZERO, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +# Property [Integer Boundaries]: floor preserves integer-type values at representable-range +# boundaries, and a value just past the int32 range stays a long. +FLOOR_INT_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "boundary_int32_max", + doc={"value": INT32_MAX}, + expression={"$floor": ["$value"]}, + expected=INT32_MAX, + msg="$floor should return INT32_MAX unchanged as an int", + ), + ExpressionTestCase( + "boundary_int32_min", + doc={"value": INT32_MIN}, + expression={"$floor": ["$value"]}, + expected=INT32_MIN, + msg="$floor should return INT32_MIN unchanged as an int", + ), + ExpressionTestCase( + "boundary_int32_overflow", + doc={"value": INT32_OVERFLOW}, + expression={"$floor": ["$value"]}, + expected=Int64(INT32_OVERFLOW), + msg="$floor should return a value just above the int32 range unchanged as a long", + ), + ExpressionTestCase( + "boundary_int32_underflow", + doc={"value": INT32_UNDERFLOW}, + expression={"$floor": ["$value"]}, + expected=Int64(INT32_UNDERFLOW), + msg="$floor should return a value just below the int32 range unchanged as a long", + ), + ExpressionTestCase( + "boundary_int64_max", + doc={"value": INT64_MAX}, + expression={"$floor": ["$value"]}, + expected=INT64_MAX, + msg="$floor should return INT64_MAX unchanged as a long", + ), + ExpressionTestCase( + "boundary_int64_max_minus_1", + doc={"value": INT64_MAX_MINUS_1}, + expression={"$floor": ["$value"]}, + expected=INT64_MAX_MINUS_1, + msg="$floor should return INT64_MAX-1 unchanged as a long", + ), + ExpressionTestCase( + "boundary_int64_min", + doc={"value": INT64_MIN}, + expression={"$floor": ["$value"]}, + expected=INT64_MIN, + msg="$floor should return INT64_MIN unchanged as a long", + ), + ExpressionTestCase( + "boundary_int64_min_plus_1", + doc={"value": INT64_MIN_PLUS_1}, + expression={"$floor": ["$value"]}, + expected=INT64_MIN_PLUS_1, + msg="$floor should return INT64_MIN+1 unchanged as a long", + ), +] + +# Property [Double Boundaries]: floor handles subnormal and extreme double magnitudes, +# including values on either side of the 0.5 rounding boundary. +FLOOR_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "boundary_double_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$floor should floor the smallest positive subnormal double to DOUBLE_ZERO", + ), + ExpressionTestCase( + "boundary_double_min_negative_subnormal", + doc={"value": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expression={"$floor": ["$value"]}, + expected=-1.0, + msg="$floor should floor the smallest negative subnormal double to -1.0", + ), + ExpressionTestCase( + "boundary_double_near_min", + doc={"value": DOUBLE_NEAR_MIN}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$floor should floor a near-min positive double to DOUBLE_ZERO", + ), + ExpressionTestCase( + "boundary_double_near_max", + doc={"value": DOUBLE_NEAR_MAX}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_NEAR_MAX, + msg="$floor should return a near-max whole-magnitude double unchanged", + ), + ExpressionTestCase( + "boundary_double_just_below_half", + doc={"value": DOUBLE_JUST_BELOW_HALF}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$floor should floor a double just below 0.5 to DOUBLE_ZERO", + ), + ExpressionTestCase( + "boundary_double_just_above_half", + doc={"value": DOUBLE_JUST_ABOVE_HALF}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$floor should floor a double just above 0.5 to DOUBLE_ZERO", + ), +] + +# Property [Decimal Boundaries]: floor handles decimal128 exponent extremes, trailing-zero +# normalization, and values around the 0.5 boundary, returning NaN when the magnitude overflows. +FLOOR_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "boundary_decimal_max", + doc={"value": DECIMAL128_MAX}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$floor should return NaN for the maximum decimal128 magnitude", + ), + ExpressionTestCase( + "boundary_decimal_min", + doc={"value": DECIMAL128_MIN}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$floor should return NaN for the minimum decimal128 magnitude", + ), + ExpressionTestCase( + "boundary_decimal_large_exponent", + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$floor should return NaN for a decimal128 with a large positive exponent", + ), + ExpressionTestCase( + "boundary_decimal_small_exponent", + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$floor should floor a decimal128 with a large negative exponent to 0", + ), + ExpressionTestCase( + "boundary_decimal_trailing_zero", + doc={"value": DECIMAL128_TRAILING_ZERO}, + expression={"$floor": ["$value"]}, + expected=Decimal128("1"), + msg="$floor should normalize a whole decimal128 with a trailing zero to 1", + ), + ExpressionTestCase( + "boundary_decimal_many_trailing_zeros", + doc={"value": DECIMAL128_MANY_TRAILING_ZEROS}, + expression={"$floor": ["$value"]}, + expected=Decimal128("1"), + msg="$floor should normalize a whole decimal128 with many trailing zeros to 1", + ), + ExpressionTestCase( + "boundary_decimal_just_below_half", + doc={"value": DECIMAL128_JUST_BELOW_HALF}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$floor should floor a decimal128 just below 0.5 to 0", + ), + ExpressionTestCase( + "boundary_decimal_just_above_half", + doc={"value": DECIMAL128_JUST_ABOVE_HALF}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$floor should floor a decimal128 just above 0.5 to 0", + ), +] + +FLOOR_BOUNDARY_TESTS = ( + FLOOR_INT_BOUNDARY_TESTS + FLOOR_DOUBLE_BOUNDARY_TESTS + FLOOR_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FLOOR_BOUNDARY_TESTS)) +def test_floor_boundaries(collection, test): + """Test $floor at numeric boundaries.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py new file mode 100644 index 000000000..327301414 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py @@ -0,0 +1,159 @@ +"""Tests for $floor core flooring behavior across integer, double, and decimal128 inputs.""" + +from __future__ import annotations + +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, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + INT64_ZERO, +) + +# Property [Integer Identity]: floor returns integer inputs unchanged, preserving int/long type. +FLOOR_INTEGER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "identity_positive_int32", + doc={"value": 1}, + expression={"$floor": ["$value"]}, + expected=1, + msg="$floor should return a positive int32 unchanged", + ), + ExpressionTestCase( + "identity_negative_int32", + doc={"value": -1}, + expression={"$floor": ["$value"]}, + expected=-1, + msg="$floor should return a negative int32 unchanged", + ), + ExpressionTestCase( + "identity_zero_int32", + doc={"value": 0}, + expression={"$floor": ["$value"]}, + expected=0, + msg="$floor should return int32 zero unchanged", + ), + ExpressionTestCase( + "identity_positive_int64", + doc={"value": Int64(1)}, + expression={"$floor": ["$value"]}, + expected=Int64(1), + msg="$floor should return a positive int64 unchanged", + ), + ExpressionTestCase( + "identity_negative_int64", + doc={"value": Int64(-1)}, + expression={"$floor": ["$value"]}, + expected=Int64(-1), + msg="$floor should return a negative int64 unchanged", + ), + ExpressionTestCase( + "identity_zero_int64", + doc={"value": INT64_ZERO}, + expression={"$floor": ["$value"]}, + expected=INT64_ZERO, + msg="$floor should return int64 zero unchanged", + ), +] + +# Property [Double Flooring]: floor rounds a double down to the nearest integer, returning a double. +FLOOR_DOUBLE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_positive", + doc={"value": 1.5}, + expression={"$floor": ["$value"]}, + expected=1.0, + msg="$floor should round a positive double down", + ), + ExpressionTestCase( + "double_negative", + doc={"value": -1.5}, + expression={"$floor": ["$value"]}, + expected=-2.0, + msg="$floor should round a negative double down, away from zero", + ), + ExpressionTestCase( + "double_zero", + doc={"value": DOUBLE_ZERO}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$floor should return double zero unchanged", + ), + ExpressionTestCase( + "double_negative_zero", + doc={"value": DOUBLE_NEGATIVE_ZERO}, + expression={"$floor": ["$value"]}, + expected=DOUBLE_NEGATIVE_ZERO, + msg="$floor should preserve negative zero for a double", + ), +] + +# Property [Decimal Flooring]: floor rounds a decimal128 down, returning decimal128. +FLOOR_DECIMAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_positive_fraction", + doc={"value": DECIMAL128_ONE_AND_HALF}, + expression={"$floor": ["$value"]}, + expected=Decimal128("1"), + msg="$floor should round a positive decimal128 down", + ), + ExpressionTestCase( + "decimal_negative_fraction", + doc={"value": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + expression={"$floor": ["$value"]}, + expected=Decimal128("-2"), + msg="$floor should round a negative decimal128 down, away from zero", + ), + ExpressionTestCase( + "decimal_whole", + doc={"value": Decimal128("1")}, + expression={"$floor": ["$value"]}, + expected=Decimal128("1"), + msg="$floor should return a whole decimal128 unchanged", + ), + ExpressionTestCase( + "decimal_zero", + doc={"value": DECIMAL128_ZERO}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$floor should return decimal128 zero unchanged", + ), + ExpressionTestCase( + "decimal_negative_zero", + doc={"value": DECIMAL128_NEGATIVE_ZERO}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NEGATIVE_ZERO, + msg="$floor should preserve negative zero for a decimal128", + ), +] + +FLOOR_CORE_TESTS = FLOOR_INTEGER_TESTS + FLOOR_DOUBLE_TESTS + FLOOR_DECIMAL_TESTS + + +@pytest.mark.parametrize("test", pytest_params(FLOOR_CORE_TESTS)) +def test_floor_core(collection, test): + """Test $floor core flooring behavior.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +def test_floor_nested_expression(collection): + """Test $floor accepts an expression as its input.""" + result = execute_expression(collection, {"$floor": {"$floor": -4.1}}) + assert_expression_result(result, expected=-5.0, msg="$floor should evaluate a nested $floor") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_nan_infinity.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_nan_infinity.py new file mode 100644 index 000000000..c90716782 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_nan_infinity.py @@ -0,0 +1,84 @@ +"""Tests for $floor handling of infinities and NaN across double and decimal128 inputs.""" + +from __future__ import annotations + +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]: floor returns floating-point infinities unchanged, while decimal128 +# infinities floor to NaN. +FLOOR_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "infinity_float_positive", + doc={"value": FLOAT_INFINITY}, + expression={"$floor": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$floor should return positive infinity unchanged", + ), + ExpressionTestCase( + "infinity_float_negative", + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expression={"$floor": ["$value"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$floor should return negative infinity unchanged", + ), + ExpressionTestCase( + "infinity_decimal_positive", + doc={"value": DECIMAL128_INFINITY}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$floor should return NaN for decimal128 positive infinity", + ), + ExpressionTestCase( + "infinity_decimal_negative", + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$floor should return NaN for decimal128 negative infinity", + ), +] + +# Property [NaN]: floor of NaN is NaN, preserving the input's floating-point family. +FLOOR_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_float", + doc={"value": FLOAT_NAN}, + expression={"$floor": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$floor should return NaN for a double NaN", + ), + ExpressionTestCase( + "nan_decimal", + doc={"value": DECIMAL128_NAN}, + expression={"$floor": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$floor should return NaN for a decimal128 NaN", + ), +] + +FLOOR_NAN_INFINITY_TESTS = FLOOR_INFINITY_TESTS + FLOOR_NAN_TESTS + + +@pytest.mark.parametrize("test", pytest_params(FLOOR_NAN_INFINITY_TESTS)) +def test_floor_nan_infinity(collection, test): + """Test $floor handling of infinities and NaN.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_null_missing.py new file mode 100644 index 000000000..213b8fdfd --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_null_missing.py @@ -0,0 +1,41 @@ +"""Tests for $floor returning null on null and missing-field inputs.""" + +from __future__ import annotations + +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 [Null and Missing]: floor returns null when the input is null or a missing field. +FLOOR_NULL_MISSING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None}, + expression={"$floor": ["$value"]}, + expected=None, + msg="$floor should return null for a null input", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$floor": ["$value"]}, + expected=None, + msg="$floor should return null for a missing field input", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FLOOR_NULL_MISSING_TESTS)) +def test_floor_null_missing(collection, test): + """Test $floor null and missing propagation.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_return_type.py new file mode 100644 index 000000000..cf0ac8f2d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_return_type.py @@ -0,0 +1,63 @@ +"""Tests for $floor preserving the input numeric type in its result.""" + +from __future__ import annotations + +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_ONE_AND_HALF + +# Property [Return Type]: floor preserves the input's numeric type, and a whole-number double +# stays a double rather than being coerced to an integer. +FLOOR_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": 5}, + expression={"$type": {"$floor": ["$value"]}}, + expected="int", + msg="$floor should return an int for an int32 input", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(5)}, + expression={"$type": {"$floor": ["$value"]}}, + expected="long", + msg="$floor should return a long for an int64 input", + ), + ExpressionTestCase( + "return_type_double_fraction", + doc={"value": 1.5}, + expression={"$type": {"$floor": ["$value"]}}, + expected="double", + msg="$floor should return a double for a fractional double input", + ), + ExpressionTestCase( + "return_type_double_whole", + doc={"value": 3.0}, + expression={"$type": {"$floor": ["$value"]}}, + expected="double", + msg="$floor should keep a whole-number double as a double, not coerce it to an int", + ), + ExpressionTestCase( + "return_type_decimal", + doc={"value": DECIMAL128_ONE_AND_HALF}, + expression={"$type": {"$floor": ["$value"]}}, + expected="decimal", + msg="$floor should return a decimal for a decimal128 input", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FLOOR_RETURN_TYPE_TESTS)) +def test_floor_return_type(collection, test): + """Test $floor preserves the input numeric type.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_type_errors.py new file mode 100644 index 000000000..d9e600ec2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_type_errors.py @@ -0,0 +1,75 @@ +"""Tests for $floor rejection of non-numeric input types and wrong argument counts.""" + +from __future__ import annotations + +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]: a non-numeric input produces NON_NUMERIC_TYPE_MISMATCH_ERROR. +FLOOR_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + f"type_error_{tid}", + doc={"value": val}, + expression={"$floor": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg=f"$floor should reject a {tid} input as non-numeric", + ) + for tid, val in [ + ("string", "string"), + ("bool", True), + ("array", [1, 2]), + ("object", {"a": 1}), + ("date", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"data")), + ("regex", Regex("abc")), + ("timestamp", Timestamp(1, 1)), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("javascript", Code("function(){}")), + ] +] + +# Property [Arity]: floor requires exactly one argument. +FLOOR_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_no_args", + doc={}, + expression={"$floor": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$floor should reject a call with no arguments", + ), + ExpressionTestCase( + "arity_two_args", + doc={}, + expression={"$floor": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$floor should reject a call with two arguments", + ), +] + +FLOOR_ERROR_TESTS = FLOOR_TYPE_ERROR_TESTS + FLOOR_ARITY_ERROR_TESTS + + +@pytest.mark.parametrize("test", pytest_params(FLOOR_ERROR_TESTS)) +def test_floor_type_errors(collection, test): + """Test $floor type and arity rejection.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) From ba1536fbbec6e05d99b924034292786b4e5cf50b Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Thu, 2 Jul 2026 14:54:01 -0700 Subject: [PATCH 2/3] Add input-form and field-path tests Signed-off-by: Daniel Frankcom --- .../arithmetic/floor/test_floor_core.py | 7 -- .../floor/test_floor_input_forms.py | 100 ++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_input_forms.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py index 327301414..9a280cdd8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_core.py @@ -10,7 +10,6 @@ ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params @@ -151,9 +150,3 @@ def test_floor_core(collection, test): assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) - - -def test_floor_nested_expression(collection): - """Test $floor accepts an expression as its input.""" - result = execute_expression(collection, {"$floor": {"$floor": -4.1}}) - assert_expression_result(result, expected=-5.0, msg="$floor should evaluate a nested $floor") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_input_forms.py new file mode 100644 index 000000000..f9d8f3d5c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_input_forms.py @@ -0,0 +1,100 @@ +"""Tests for $floor argument forms, literals, field paths, and nested expression 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.error_codes import NON_NUMERIC_TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Argument Form]: $floor accepts its single argument bare or wrapped in a one-element +# array. +FLOOR_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "form_array", + doc={"value": -1.5}, + expression={"$floor": ["$value"]}, + expected=-2.0, + msg="$floor should accept its argument wrapped in a one-element array", + ), + ExpressionTestCase( + "form_bare", + doc={"value": -1.5}, + expression={"$floor": "$value"}, + expected=-2.0, + msg="$floor should accept its argument without an array wrapper", + ), +] + +# Property [Literal Input]: $floor evaluates an inline literal argument, not only document fields. +FLOOR_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$floor": [1.5]}, + expected=1.0, + msg="$floor should round down an inline literal argument", + ), +] + +# Property [Expression Input]: $floor evaluates a nested expression argument before flooring. +FLOOR_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_expression", + doc={}, + expression={"$floor": {"$floor": -4.1}}, + expected=-5.0, + msg="$floor should evaluate a nested $floor expression argument", + ), +] + +# Property [Field Path Input]: $floor 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 $floor rejects as a non-numeric type. +FLOOR_FIELD_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field_path", + doc={"a": {"b": -1.5}}, + expression={"$floor": "$a.b"}, + expected=-2.0, + msg="$floor should resolve a dotted field path into a nested object", + ), + ExpressionTestCase( + "composite_array_field_path", + doc={"a": [{"b": -1.5}, {"b": -2.5}]}, + expression={"$floor": "$a.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$floor should reject a field path that resolves to an array from an array of objects", + ), + ExpressionTestCase( + "array_index_field_path", + doc={"a": [-1.5, -2.5]}, + expression={"$floor": "$a.0"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$floor should reject a numeric path component over an array, resolving non-numeric", + ), +] + +FLOOR_INPUT_FORM_TESTS = ( + FLOOR_ARGUMENT_FORM_TESTS + + FLOOR_LITERAL_TESTS + + FLOOR_EXPRESSION_INPUT_TESTS + + FLOOR_FIELD_PATH_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(FLOOR_INPUT_FORM_TESTS)) +def test_floor_input_forms(collection, test_case: ExpressionTestCase): + """Test $floor 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, + ) From 6b1d5fa11a6efc369200391e0dd8150019cf5525 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Thu, 2 Jul 2026 14:54:51 -0700 Subject: [PATCH 3/3] Add integer-boundary tests Signed-off-by: Daniel Frankcom --- .../arithmetic/floor/test_floor_boundaries.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py index 5013b7dd5..729c8e31b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/floor/test_floor_boundaries.py @@ -26,10 +26,12 @@ DECIMAL128_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, DOUBLE_ZERO, INT32_MAX, INT32_MIN, @@ -103,7 +105,8 @@ ] # Property [Double Boundaries]: floor handles subnormal and extreme double magnitudes, -# including values on either side of the 0.5 rounding boundary. +# including values on either side of the 0.5 rounding boundary and at the integer-representability +# limit, where doubles have no fractional part and floor is an identity. FLOOR_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "boundary_double_min_subnormal", @@ -133,6 +136,20 @@ expected=DOUBLE_NEAR_MAX, msg="$floor should return a near-max whole-magnitude double unchanged", ), + ExpressionTestCase( + "boundary_double_max_safe_integer", + doc={"value": float(DOUBLE_MAX_SAFE_INTEGER)}, + expression={"$floor": ["$value"]}, + expected=float(DOUBLE_MAX_SAFE_INTEGER), + msg="$floor should return the max safe integer double unchanged, having no fractional part", + ), + ExpressionTestCase( + "boundary_double_precision_loss", + doc={"value": float(DOUBLE_PRECISION_LOSS)}, + expression={"$floor": ["$value"]}, + expected=float(DOUBLE_PRECISION_LOSS), + msg="$floor should return a precision-loss double unchanged, having no fractional part", + ), ExpressionTestCase( "boundary_double_just_below_half", doc={"value": DOUBLE_JUST_BELOW_HALF},