From dfdf73bd3ecd5d03589da8d1e9a7ed6e4dde8063 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 16 Jun 2026 13:50:10 -0700 Subject: [PATCH 1/8] Add a skip localhost mark to conditionally skip the destructive shutdown command Signed-off-by: PatersonProjects --- .../commands/shutdown/test_smoke_shutdown.py | 2 +- documentdb_tests/conftest.py | 18 ++++++++++++++++++ documentdb_tests/pytest.ini | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py b/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py index 5385d8ec2..c29b6b8f3 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py @@ -9,7 +9,7 @@ from documentdb_tests.framework.assertions import assertFailure from documentdb_tests.framework.executor import execute_admin_command -pytestmark = pytest.mark.smoke +pytestmark = [pytest.mark.smoke, pytest.mark.skip_localhost] def test_smoke_shutdown(collection): diff --git a/documentdb_tests/conftest.py b/documentdb_tests/conftest.py index b88a99733..8292f57ef 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -207,6 +207,8 @@ def pytest_collection_modifyitems(session, config, items): Or run them manually with: pytest -m no_parallel -p no:xdist Tests marked 'replica_set' are skipped when the server is not a replica set member. + + Tests marked 'skip_localhost' are skipped when connected to a localhost server. """ # Skip replica_set tests when not connected to a replica set conn_str = getattr(config, "connection_string", "") or "" @@ -227,6 +229,22 @@ def pytest_collection_modifyitems(session, config, items): ) ) + # Skip skip_localhost tests when connected to a localhost server + try: + from pymongo.uri_parser import parse_uri + + localhost_hosts = {"localhost", "127.0.0.1", "::1"} + nodes = parse_uri(conn_str)["nodelist"] if conn_str else [] + is_localhost = any(host in localhost_hosts for host, _ in nodes) + except Exception: + is_localhost = False + if is_localhost: + for item in items: + if item.get_closest_marker("skip_localhost"): + item.add_marker( + pytest.mark.skip(reason="skipped on localhost (server connection is local)") + ) + # Deselect no_parallel tests when running under xdist is_xdist = bool(getattr(config.option, "numprocesses", None)) or hasattr(config, "workerinput") if is_xdist: diff --git a/documentdb_tests/pytest.ini b/documentdb_tests/pytest.ini index de4799073..63219b817 100644 --- a/documentdb_tests/pytest.ini +++ b/documentdb_tests/pytest.ini @@ -45,6 +45,7 @@ markers = engine_xcrash(engine, reason): test crashes the server on a specific engine no_parallel: Tests that must run sequentially (not in parallel) replica_set: Tests that need to run on replica set + skip_localhost: Tests skipped when connected to a localhost server # Timeout for tests (seconds) timeout = 300 From 51a482ab4db4faa743125c3ac8699d126a8fbf33 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:32:35 -0700 Subject: [PATCH 2/8] Added tests Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_date.py | 162 +++++++++++ .../arithmetic/add/test_add_errors.py | 203 ++++++++++++++ .../arithmetic/add/test_add_input_forms.py | 47 ++++ .../arithmetic/add/test_add_non_finite.py | 154 +++++++++++ .../arithmetic/add/test_add_null.py | 86 ++++++ .../arithmetic/add/test_add_numeric.py | 259 ++++++++++++++++++ .../arithmetic/add/test_add_overflow.py | 95 +++++++ .../arithmetic/add/test_add_precision.py | 103 +++++++ .../arithmetic/add/test_add_return_type.py | 114 ++++++++ 9 files changed, 1223 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py new file mode 100644 index 000000000..7cd0f09f2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -0,0 +1,162 @@ +from datetime import datetime, timedelta, timezone + +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 [Date Arithmetic]: $add accepts exactly one date operand and one or more numeric +# operands (in milliseconds). The date may appear in any position. +ADD_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int32", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 86400000}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add int32 milliseconds to a date", + ), + ExpressionTestCase( + "date_int64", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Int64(86400000)}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add int64 milliseconds to a date", + ), + ExpressionTestCase( + "date_decimal", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 2000, tzinfo=timezone.utc), + msg="$add should round a decimal128 fractional millisecond value when adding to a date", + ), + ExpressionTestCase( + "date_double_round_up", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), + msg="$add should round up a double fractional millisecond value (.5) when adding to a date", + ), + ExpressionTestCase( + "date_double_truncates", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 4.4}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 4000, tzinfo=timezone.utc), + msg="$add should truncate a double fractional millisecond value (<.5) when adding to a date", # noqa: E501 + ), +] + +# Property [Date Rounding Boundaries]: $add rounds fractional millisecond offsets using +# round-half-away-from-zero. Values with |frac| < 0.5 truncate toward zero; values with +# |frac| >= 0.5 round away from zero. +ADD_DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_double_0_1", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.1}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should truncate 0.1ms and leave the date unchanged", + ), + ExpressionTestCase( + "date_double_0_49", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.49}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should truncate 0.49ms and leave the date unchanged", + ), + ExpressionTestCase( + "date_double_0_51", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.51}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$add should round up 0.51ms to 1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_0_6", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.6}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$add should round up 0.6ms to 1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_neg_0_5", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.5}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), + msg="$add should round -0.5ms away from zero to -1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_neg_0_51", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.51}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), + msg="$add should round -0.51ms away from zero to -1ms when adding to a date", + ), +] + +# Property [Date Operand Position]: the date operand may appear in any position among the +# operands; only one date is permitted. +ADD_DATE_POSITION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "number_then_date", + doc={"a": 86400000, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add a date when the numeric operand appears before the date", + ), + ExpressionTestCase( + "date_in_middle", + doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc), "c": 1000}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=datetime(2026, 1, 1, 0, 0, 1, 1000, tzinfo=timezone.utc), + msg="$add should add a date when it appears in the middle of the operand list", + ), +] + +# Property [Date Sign Handling]: adding zero or a negative number of milliseconds to a date +# returns the date unchanged or subtracted. +ADD_DATE_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_negative", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -86400000}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2025, 12, 31, tzinfo=timezone.utc), + msg="$add should subtract milliseconds from a date when adding a negative number", + ), + ExpressionTestCase( + "date_zero", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should return the same date when adding zero milliseconds", + ), + ExpressionTestCase( + "date_negative_zero", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.0}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should return the same date when adding negative zero", + ), +] + +ADD_DATE_ALL_TESTS = ( + ADD_DATE_NUMERIC_TESTS + ADD_DATE_POSITION_TESTS + ADD_DATE_SIGN_TESTS + ADD_DATE_ROUNDING_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_DATE_ALL_TESTS)) +def test_add_date(collection, test_case: ExpressionTestCase): + """Test $add date arithmetic: numeric types, operand position, and sign handling.""" + 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/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py new file mode 100644 index 000000000..f5c817b3d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -0,0 +1,203 @@ +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 ( + MORE_THAN_ONE_DATE_ERROR, + OVERFLOW_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + INT64_MAX, +) + +# Property [Type Strictness]: $add rejects non-numeric, non-date operand types. +ADD_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + f"type_{tid}", + doc={"a": 1, "b": val}, + expression={"$add": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"$add should reject a {tid} operand", + ) + for tid, val in [ + ("string", "string"), + ("bool", True), + ("array", [2, 3]), + ("object", {"a": 2}), + ("regex", Regex("abc")), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] +] + +# Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among +# valid numeric operands. +ADD_MIXED_VALID_INVALID_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "mixed_valid_invalid", + doc={"a": 1, "b": 2, "c": "string"}, + expression={"$add": ["$a", "$b", "$c"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should error when a string appears among numeric operands", + ), +] + +# Property [Single Invalid Operand]: $add rejects a single operand of an invalid type. +ADD_SINGLE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "single_string", + doc={"a": "string"}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single string operand", + ), + ExpressionTestCase( + "single_boolean", + doc={"a": True}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single boolean operand", + ), + ExpressionTestCase( + "single_array", + doc={"a": [1, 2]}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single array operand", + ), + ExpressionTestCase( + "single_object", + doc={"a": {"x": 1}}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single object operand", + ), +] + +# Property [Multiple Dates]: $add rejects expressions with more than one date operand. +ADD_MULTIPLE_DATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "add_two_identical_dates", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 1, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when adding two identical date operands", + ), + ExpressionTestCase( + "two_different_dates", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when adding two different date operands", + ), + ExpressionTestCase( + "two_dates_with_numbers", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": [1, 2, 3, "$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when two dates appear among numeric operands", + ), + ExpressionTestCase( + "dates_separated_by_number", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", 1, "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when two dates are separated by a numeric operand", + ), +] + +# Property [Date with Non-Finite]: $add rejects NaN and Infinity as numeric operands when a +# date is also present, since the resulting date would be non-representable. +ADD_DATE_NON_FINITE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and float NaN", + ), + ExpressionTestCase( + "date_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and float infinity", + ), + ExpressionTestCase( + "date_decimal_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and decimal128 NaN", + ), + ExpressionTestCase( + "date_decimal_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_INFINITY}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and decimal128 infinity", + ), +] + +# Property [Date Overflow]: $add errors when the millisecond offset would push the date result +# beyond the representable date range. +ADD_DATE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int64_max", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": INT64_MAX}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding INT64_MAX milliseconds to a date overflows the date range", # noqa: E501 + ), +] + +ADD_ERROR_ALL_TESTS = ( + ADD_TYPE_ERROR_TESTS + + ADD_MIXED_VALID_INVALID_TESTS + + ADD_SINGLE_TYPE_ERROR_TESTS + + ADD_MULTIPLE_DATE_TESTS + + ADD_DATE_NON_FINITE_ERROR_TESTS + + ADD_DATE_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) +def test_add_errors(collection, test_case: ExpressionTestCase): + """Test $add type, multiple-date, and date non-finite 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/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py new file mode 100644 index 000000000..7019c1b0b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -0,0 +1,47 @@ +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 [Expression Input]: $add evaluates a nested expression argument before summing. +ADD_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_add", + doc={"a": 1, "b": 2}, + expression={"$add": [{"$add": ["$a", "$b"]}, 3]}, + expected=6, + msg="$add should evaluate a nested $add expression as an operand", + ), +] + +# Property [Mixed Literal and Field]: $add accepts a mix of field references and inline literals +# in the same operand list. +ADD_MIXED_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "mixed_literal_and_field", + doc={"a": 10}, + expression={"$add": ["$a", 5]}, + expected=15, + msg="$add should sum a field reference and an inline literal operand", + ), +] + +ADD_INPUT_FORM_ALL_TESTS = ADD_EXPRESSION_INPUT_TESTS + ADD_MIXED_INPUT_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_INPUT_FORM_ALL_TESTS)) +def test_add_input_forms(collection, test_case: ExpressionTestCase): + """Test $add literal, nested expression, and mixed input form 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/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py new file mode 100644 index 000000000..0343c3316 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -0,0 +1,154 @@ +import math + +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]: $add propagates infinity according to IEEE 754 rules. +ADD_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "infinity", + doc={"a": FLOAT_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding infinity and a finite number", + ), + ExpressionTestCase( + "negative_infinity", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding negative infinity and a finite number", # noqa: E501 + ), + ExpressionTestCase( + "single_infinity", + doc={"a": FLOAT_INFINITY}, + expression={"$add": ["$a"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity for a single infinity operand", + ), + ExpressionTestCase( + "inf_plus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding two positive infinities", + ), + ExpressionTestCase( + "neg_inf_plus_neg_inf", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding two negative infinities", + ), + ExpressionTestCase( + "inf_plus_zero", + doc={"a": FLOAT_INFINITY, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding infinity and zero", + ), + ExpressionTestCase( + "neg_inf_plus_zero", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding negative infinity and zero", + ), + ExpressionTestCase( + "decimal_infinity", + doc={"a": DECIMAL128_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$add should return decimal128 infinity when adding decimal128 infinity and a number", + ), + ExpressionTestCase( + "decimal_negative_infinity", + doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$add should return decimal128 negative infinity when adding decimal128 negative infinity and a number", # noqa: E501 + ), +] + +# Property [NaN]: $add propagates NaN according to IEEE 754 rules. +ADD_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_add_one", + doc={"a": FLOAT_NAN, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float NaN and a finite number", + ), + ExpressionTestCase( + "inf_minus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float infinity and negative infinity", + ), + ExpressionTestCase( + "nan_plus_nan", + doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding two float NaN values", + ), + ExpressionTestCase( + "nan_plus_inf", + doc={"a": FLOAT_NAN, "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float NaN and infinity", + ), + ExpressionTestCase( + "decimal_nan", + doc={"a": DECIMAL128_NAN, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding decimal128 NaN and a number", + ), + ExpressionTestCase( + "decimal_nan_plus_nan", + doc={"a": DECIMAL128_NAN, "b": DECIMAL128_NAN}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding two decimal128 NaN values", + ), + ExpressionTestCase( + "decimal_inf_minus_inf", + doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding decimal128 infinity and negative infinity", # noqa: E501 + ), +] + +ADD_NON_FINITE_ALL_TESTS = ADD_INFINITY_TESTS + ADD_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NON_FINITE_ALL_TESTS)) +def test_add_non_finite(collection, test_case: ExpressionTestCase): + """Test $add infinity and NaN 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/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py new file mode 100644 index 000000000..9651d52d3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -0,0 +1,86 @@ +from datetime import datetime, timezone + +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]: $add returns null if any operand is null or refers to a missing +# field. +ADD_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "single_null", + doc={"a": None}, + expression={"$add": ["$a"]}, + expected=None, + msg="$add should return null for a single null operand", + ), + ExpressionTestCase( + "null_operand", + doc={"a": 1, "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when any operand is null", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$add": [1, MISSING]}, + expected=None, + msg="$add should return null when any operand is a missing field", + ), + ExpressionTestCase( + "null_with_multiple", + doc={"a": 1, "b": 2, "c": None}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=None, + msg="$add should return null when null appears among multiple operands", + ), + ExpressionTestCase( + "null_in_middle", + doc={"a": 1, "b": 2, "c": 3, "e": 5}, + expression={"$add": ["$a", "$b", "$c", None, "$e"]}, + expected=None, + msg="$add should return null when null appears in the middle of operands", + ), + ExpressionTestCase( + "all_null", + doc={"a": None, "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when all operands are null", + ), + ExpressionTestCase( + "all_missing", + doc={}, + expression={"$add": [MISSING, MISSING]}, + expected=None, + msg="$add should return null when all operands are missing fields", + ), + ExpressionTestCase( + "date_and_null", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when a date is paired with a null operand", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NULL_TESTS)) +def test_add_null(collection, test_case: ExpressionTestCase): + """Test $add 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/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py new file mode 100644 index 000000000..3c4d6f305 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -0,0 +1,259 @@ +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 [Same-Type Addition]: $add of two values of the same numeric type returns a value of +# that type. +ADD_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_type_int32", + doc={"a": 1, "b": 2}, + expression={"$add": ["$a", "$b"]}, + expected=3, + msg="$add should add two int32 values", + ), + ExpressionTestCase( + "same_type_int64", + doc={"a": Int64(10), "b": Int64(20)}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(30), + msg="$add should add two int64 values", + ), + ExpressionTestCase( + "same_type_double", + doc={"a": 1.5, "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=4.0, + msg="$add should add two double values", + ), + ExpressionTestCase( + "same_type_decimal", + doc={"a": Decimal128("10.5"), "b": Decimal128("20.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("31.0"), + msg="$add should add two decimal128 values", + ), +] + +# Property [Type Promotion]: $add promotes to the wider type when operands have different numeric +# types. Precedence: decimal128 > double > int64 > int32. +ADD_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_int64", + doc={"a": 1, "b": Int64(20)}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(21), + msg="$add should return int64 when adding int32 and int64", + ), + ExpressionTestCase( + "int32_double", + doc={"a": 1, "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=3.5, + msg="$add should return double when adding int32 and double", + ), + ExpressionTestCase( + "int32_decimal", + doc={"a": 1, "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("3.5"), + msg="$add should return decimal128 when adding int32 and decimal128", + ), + ExpressionTestCase( + "int64_double", + doc={"a": Int64(10), "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=12.5, + msg="$add should return double when adding int64 and double", + ), + ExpressionTestCase( + "int64_decimal", + doc={"a": Int64(10), "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("12.5"), + msg="$add should return decimal128 when adding int64 and decimal128", + ), + ExpressionTestCase( + "double_decimal", + doc={"a": 1.5, "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(Decimal128("4.00000000000000")), + msg="$add should return decimal128 when adding double and decimal128", + ), + ExpressionTestCase( + "three_mixed_types", + doc={"a": Decimal128("1.5"), "b": 2.5, "c": Int64(3)}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=pytest.approx(Decimal128("7.00000000000000")), + msg="$add should return decimal128 when adding decimal128, double, and int64", + ), +] + +# Property [Multiple Operands]: $add correctly sums three or more operands. +ADD_MULTIPLE_OPERANDS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "multiple_int32", + doc={"a": 1, "b": 2, "c": 3}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=6, + msg="$add should add multiple int32 values", + ), + ExpressionTestCase( + "multiple_int64", + doc={"a": Int64(1), "b": Int64(2), "c": Int64(3)}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=Int64(6), + msg="$add should add multiple int64 values", + ), + ExpressionTestCase( + "multiple_double", + doc={"a": 1.1, "b": 2.2, "c": 3.3}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=pytest.approx(6.6), + msg="$add should add multiple double values", + ), + ExpressionTestCase( + "multiple_decimal", + doc={ + "a": Decimal128("1"), + "b": Decimal128("2"), + "c": Decimal128("3"), + "d": Decimal128("4"), + }, + expression={"$add": ["$a", "$b", "$c", "$d"]}, + expected=Decimal128("10"), + msg="$add should add multiple decimal128 values", + ), + ExpressionTestCase( + "five_operands", + doc={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + expression={"$add": ["$a", "$b", "$c", "$d", "$e"]}, + expected=15, + msg="$add should correctly sum five int32 operands", + ), + ExpressionTestCase( + "ten_operands", + doc={ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + "i": 9, + "j": 10, + }, + expression={"$add": ["$a", "$b", "$c", "$d", "$e", "$f", "$g", "$h", "$i", "$j"]}, + expected=55, + msg="$add should correctly sum ten int32 operands", + ), +] + +# Property [Empty and Single Operand]: $add of zero operands returns 0; single operand returns +# that value unchanged. +ADD_SINGLE_AND_EMPTY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty", + doc={}, + expression={"$add": []}, + expected=0, + msg="$add should return 0 for empty operand list", + ), + ExpressionTestCase( + "single_int32", + doc={"a": 5}, + expression={"$add": ["$a"]}, + expected=5, + msg="$add should return the value for a single int32 operand", + ), + ExpressionTestCase( + "single_int64", + doc={"a": Int64(0)}, + expression={"$add": ["$a"]}, + expected=Int64(0), + msg="$add should return the value for a single int64 operand", + ), + ExpressionTestCase( + "single_double", + doc={"a": 0.0}, + expression={"$add": ["$a"]}, + expected=0.0, + msg="$add should return the value for a single double operand", + ), + ExpressionTestCase( + "single_decimal", + doc={"a": Decimal128("0")}, + expression={"$add": ["$a"]}, + expected=Decimal128("0"), + msg="$add should return the value for a single decimal128 operand", + ), +] + +# Property [Sign Handling]: $add handles negative values and zero correctly. +ADD_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "negative_positive", + doc={"a": -5, "b": 3}, + expression={"$add": ["$a", "$b"]}, + expected=-2, + msg="$add should add a negative and a positive int32 value", + ), + ExpressionTestCase( + "both_negative", + doc={"a": -10, "b": -20}, + expression={"$add": ["$a", "$b"]}, + expected=-30, + msg="$add should add two negative int32 values", + ), + ExpressionTestCase( + "zeros", + doc={"a": 0, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=0, + msg="$add should return 0 when adding two int32 zeros", + ), + ExpressionTestCase( + "zero_negative_zero", + doc={"a": 0, "b": -0.0}, + expression={"$add": ["$a", "$b"]}, + expected=0.0, + msg="$add should return 0.0 when adding int32 zero and negative zero double", + ), + ExpressionTestCase( + "sum_to_zero", + doc={"a": 1, "b": 0, "c": -1}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=0, + msg="$add should return 0 when operands sum to zero", + ), +] + +ADD_NUMERIC_ALL_TESTS = ( + ADD_SAME_TYPE_TESTS + + ADD_MIXED_TYPE_TESTS + + ADD_MULTIPLE_OPERANDS_TESTS + + ADD_SINGLE_AND_EMPTY_TESTS + + ADD_SIGN_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NUMERIC_ALL_TESTS)) +def test_add_numeric(collection, test_case: ExpressionTestCase): + """Test $add numeric type combinations, multiple operands, and sign handling.""" + 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/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py new file mode 100644 index 000000000..29a992905 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -0,0 +1,95 @@ +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 ( + DOUBLE_FROM_INT64_MAX, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MIN, +) + +# Property [Int32 Overflow]: when an int32 result exceeds the int32 range, $add promotes to +# int64. +ADD_INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_overflow", + doc={"a": INT32_MAX, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(INT32_OVERFLOW), + msg="$add should promote to int64 when the int32 result overflows INT32_MAX", + ), + ExpressionTestCase( + "int32_underflow", + doc={"a": INT32_MIN, "b": -1}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(INT32_UNDERFLOW), + msg="$add should promote to int64 when the int32 result underflows INT32_MIN", + ), +] + +# Property [Int64 Overflow]: when an int64 result exceeds the int64 range, $add promotes to +# double. +ADD_INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_overflow", + doc={"a": INT64_MAX, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(DOUBLE_FROM_INT64_MAX), + msg="$add should promote to double when the int64 result overflows INT64_MAX", + ), + ExpressionTestCase( + "int64_underflow", + doc={"a": INT64_MIN, "b": -1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(-DOUBLE_FROM_INT64_MAX), + msg="$add should promote to double when the int64 result underflows INT64_MIN", + ), +] + +# Property [Double Overflow]: when a double result exceeds the double range, $add returns +# infinity. +ADD_DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_overflow", + doc={"a": 1e308, "b": 1e308}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return positive infinity on double overflow", + ), + ExpressionTestCase( + "double_underflow", + doc={"a": -1e308, "b": -1e308}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity on double underflow", + ), +] + +ADD_OVERFLOW_ALL_TESTS = ( + ADD_INT32_OVERFLOW_TESTS + ADD_INT64_OVERFLOW_TESTS + ADD_DOUBLE_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_OVERFLOW_ALL_TESTS)) +def test_add_overflow(collection, test_case: ExpressionTestCase): + """Test $add integer and double overflow and underflow 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/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py new file mode 100644 index 000000000..f0cdd34ab --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -0,0 +1,103 @@ +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_MAX, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_INFINITY, +) + +# Property [Decimal128 Precision]: $add preserves decimal128 precision, including exact +# representation of values that are inexact in double. +ADD_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision", + doc={"a": Decimal128("1.5"), "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("4.0"), + msg="$add should preserve decimal128 precision", + ), + ExpressionTestCase( + "decimal_precision_small", + doc={"a": Decimal128("0.1"), "b": Decimal128("0.2")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("0.3"), + msg="$add should exactly represent 0.1 + 0.2 with decimal128", + ), + ExpressionTestCase( + "decimal_large_precision", + doc={ + "a": Decimal128("999999999999999999999999999999999"), + "b": Decimal128("1"), + }, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("1000000000000000000000000000000000"), + msg="$add should handle large decimal128 addition with full precision", + ), + ExpressionTestCase( + "decimal_large_negative_precision", + doc={ + "a": Decimal128("-999999999999999999999999999999999"), + "b": Decimal128("-1"), + }, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("-1000000000000000000000000000000000"), + msg="$add should handle large negative decimal128 addition with full precision", + ), +] + +# Property [Decimal128 Boundaries]: $add at decimal128 boundary values promotes to infinity when +# the result overflows, and returns zero when max and min cancel. +ADD_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max_plus_zero", + doc={"a": DECIMAL128_MAX, "b": Decimal128("0")}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_MAX, + msg="$add should return decimal128 max when adding zero to decimal128 max", + ), + ExpressionTestCase( + "decimal128_max_plus_max", + doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MAX}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$add should return decimal128 infinity when adding two decimal128 max values", + ), + ExpressionTestCase( + "decimal128_min_plus_min", + doc={"a": DECIMAL128_MIN, "b": DECIMAL128_MIN}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$add should return decimal128 negative infinity when adding two decimal128 min values", + ), + ExpressionTestCase( + "decimal128_max_plus_min", + doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MIN}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("0E+6111"), + msg="$add should return zero when adding decimal128 max and decimal128 min", + ), +] + +ADD_PRECISION_ALL_TESTS = ADD_DECIMAL_PRECISION_TESTS + ADD_DECIMAL_BOUNDARY_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_PRECISION_ALL_TESTS)) +def test_add_precision(collection, test_case: ExpressionTestCase): + """Test $add decimal128 precision and boundary value 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/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py new file mode 100644 index 000000000..7af9e0b66 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -0,0 +1,114 @@ +from datetime import datetime, timezone + +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]: $add follows numeric type promotion rules to determine the result type. +# Precedence: decimal128 > double > int64 > int32. Date + numeric always returns date. +ADD_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int_int", + doc={"a": 1, "b": 2}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="int", + msg="$add should return int type when adding two int32 values", + ), + ExpressionTestCase( + "return_type_int_long", + doc={"a": 1, "b": Int64(2)}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="long", + msg="$add should return long type when adding int32 and int64", + ), + ExpressionTestCase( + "return_type_int_double", + doc={"a": 1, "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding int32 and double", + ), + ExpressionTestCase( + "return_type_int_decimal", + doc={"a": 1, "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding int32 and decimal128", + ), + ExpressionTestCase( + "return_type_long_long", + doc={"a": Int64(1), "b": Int64(2)}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="long", + msg="$add should return long type when adding two int64 values", + ), + ExpressionTestCase( + "return_type_long_double", + doc={"a": Int64(1), "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding int64 and double", + ), + ExpressionTestCase( + "return_type_long_decimal", + doc={"a": Int64(1), "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding int64 and decimal128", + ), + ExpressionTestCase( + "return_type_double_double", + doc={"a": 1.0, "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding two double values", + ), + ExpressionTestCase( + "return_type_double_decimal", + doc={"a": 1.0, "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding double and decimal128", + ), + ExpressionTestCase( + "return_type_decimal_decimal", + doc={"a": Decimal128("1"), "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding two decimal128 values", + ), + ExpressionTestCase( + "return_type_date_int", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 1000}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="date", + msg="$add should return date type when adding a date and an int32", + ), + ExpressionTestCase( + "return_type_empty", + doc={}, + expression={"$type": {"$add": []}}, + expected="int", + msg="$add should return int type for an empty operand list", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_RETURN_TYPE_TESTS)) +def test_add_return_type(collection, test_case: ExpressionTestCase): + """Test $add return type promotion rules for all numeric type combinations.""" + 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 014f2bceb8c06ba36a9e652a57ac7d81397bd67e Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:40:50 -0700 Subject: [PATCH 3/8] Added docStrings to all files Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/add/test_add_date.py | 4 ++++ .../operator/expressions/arithmetic/add/test_add_errors.py | 2 ++ .../expressions/arithmetic/add/test_add_input_forms.py | 2 ++ .../expressions/arithmetic/add/test_add_non_finite.py | 2 ++ .../core/operator/expressions/arithmetic/add/test_add_null.py | 2 ++ .../operator/expressions/arithmetic/add/test_add_numeric.py | 4 ++++ .../operator/expressions/arithmetic/add/test_add_overflow.py | 2 ++ .../operator/expressions/arithmetic/add/test_add_precision.py | 2 ++ .../expressions/arithmetic/add/test_add_return_type.py | 2 ++ 9 files changed, 22 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py index 7cd0f09f2..eff8f4af1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -1,3 +1,7 @@ +"""Tests for $add date arithmetic including numeric offsets, rounding boundaries, operand +position, and sign handling. +""" + from datetime import datetime, timedelta, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py index f5c817b3d..127f0e5de 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -1,3 +1,5 @@ +"""Tests for $add error cases including invalid operand types, multiple dates, and date overflow.""" + from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py index 7019c1b0b..fa2f73df4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -1,3 +1,5 @@ +"""Tests for $add input forms including nested expressions and mixed literal/field operands.""" + import pytest from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py index 0343c3316..958818e98 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -1,3 +1,5 @@ +"""Tests for $add infinity and NaN propagation.""" + import math import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py index 9651d52d3..3854d72d8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -1,3 +1,5 @@ +"""Tests for $add null and missing field propagation.""" + from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py index 3c4d6f305..971b01581 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -1,3 +1,7 @@ +"""Tests for $add numeric operations including same-type and mixed-type addition, multiple +operands, empty/single operands, and sign handling. +""" + import pytest from bson import Decimal128, Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py index 29a992905..2da84306e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -1,3 +1,5 @@ +"""Tests for $add integer and double overflow and underflow promotion.""" + import pytest from bson import Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py index f0cdd34ab..6658eb03a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -1,3 +1,5 @@ +"""Tests for $add decimal128 precision and boundary value handling.""" + import pytest from bson import Decimal128 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py index 7af9e0b66..d681c7fd2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -1,3 +1,5 @@ +"""Tests for $add return type promotion rules across numeric and date operand combinations.""" + from datetime import datetime, timezone import pytest From 37407ae57c9912cf17ff31f5885fafea01f7db38 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:47:01 -0700 Subject: [PATCH 4/8] Revert "Added docStrings to all files" This reverts commit 014f2bceb8c06ba36a9e652a57ac7d81397bd67e. Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/add/test_add_date.py | 4 ---- .../operator/expressions/arithmetic/add/test_add_errors.py | 2 -- .../expressions/arithmetic/add/test_add_input_forms.py | 2 -- .../expressions/arithmetic/add/test_add_non_finite.py | 2 -- .../core/operator/expressions/arithmetic/add/test_add_null.py | 2 -- .../operator/expressions/arithmetic/add/test_add_numeric.py | 4 ---- .../operator/expressions/arithmetic/add/test_add_overflow.py | 2 -- .../operator/expressions/arithmetic/add/test_add_precision.py | 2 -- .../expressions/arithmetic/add/test_add_return_type.py | 2 -- 9 files changed, 22 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py index eff8f4af1..7cd0f09f2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -1,7 +1,3 @@ -"""Tests for $add date arithmetic including numeric offsets, rounding boundaries, operand -position, and sign handling. -""" - from datetime import datetime, timedelta, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py index 127f0e5de..f5c817b3d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -1,5 +1,3 @@ -"""Tests for $add error cases including invalid operand types, multiple dates, and date overflow.""" - from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py index fa2f73df4..7019c1b0b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -1,5 +1,3 @@ -"""Tests for $add input forms including nested expressions and mixed literal/field operands.""" - import pytest from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py index 958818e98..0343c3316 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -1,5 +1,3 @@ -"""Tests for $add infinity and NaN propagation.""" - import math import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py index 3854d72d8..9651d52d3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -1,5 +1,3 @@ -"""Tests for $add null and missing field propagation.""" - from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py index 971b01581..3c4d6f305 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -1,7 +1,3 @@ -"""Tests for $add numeric operations including same-type and mixed-type addition, multiple -operands, empty/single operands, and sign handling. -""" - import pytest from bson import Decimal128, Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py index 2da84306e..29a992905 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -1,5 +1,3 @@ -"""Tests for $add integer and double overflow and underflow promotion.""" - import pytest from bson import Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py index 6658eb03a..f0cdd34ab 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -1,5 +1,3 @@ -"""Tests for $add decimal128 precision and boundary value handling.""" - import pytest from bson import Decimal128 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py index d681c7fd2..7af9e0b66 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -1,5 +1,3 @@ -"""Tests for $add return type promotion rules across numeric and date operand combinations.""" - from datetime import datetime, timezone import pytest From f851712aaa40362d3ada515b2c50fd52ffc9b3c4 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:47:24 -0700 Subject: [PATCH 5/8] Revert "Added tests" This reverts commit 51a482ab4db4faa743125c3ac8699d126a8fbf33. Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_date.py | 162 ----------- .../arithmetic/add/test_add_errors.py | 203 -------------- .../arithmetic/add/test_add_input_forms.py | 47 ---- .../arithmetic/add/test_add_non_finite.py | 154 ----------- .../arithmetic/add/test_add_null.py | 86 ------ .../arithmetic/add/test_add_numeric.py | 259 ------------------ .../arithmetic/add/test_add_overflow.py | 95 ------- .../arithmetic/add/test_add_precision.py | 103 ------- .../arithmetic/add/test_add_return_type.py | 114 -------- 9 files changed, 1223 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py deleted file mode 100644 index 7cd0f09f2..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ /dev/null @@ -1,162 +0,0 @@ -from datetime import datetime, timedelta, timezone - -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 [Date Arithmetic]: $add accepts exactly one date operand and one or more numeric -# operands (in milliseconds). The date may appear in any position. -ADD_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_int32", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 86400000}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add int32 milliseconds to a date", - ), - ExpressionTestCase( - "date_int64", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Int64(86400000)}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add int64 milliseconds to a date", - ), - ExpressionTestCase( - "date_decimal", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 2000, tzinfo=timezone.utc), - msg="$add should round a decimal128 fractional millisecond value when adding to a date", - ), - ExpressionTestCase( - "date_double_round_up", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), - msg="$add should round up a double fractional millisecond value (.5) when adding to a date", - ), - ExpressionTestCase( - "date_double_truncates", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 4.4}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 4000, tzinfo=timezone.utc), - msg="$add should truncate a double fractional millisecond value (<.5) when adding to a date", # noqa: E501 - ), -] - -# Property [Date Rounding Boundaries]: $add rounds fractional millisecond offsets using -# round-half-away-from-zero. Values with |frac| < 0.5 truncate toward zero; values with -# |frac| >= 0.5 round away from zero. -ADD_DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_double_0_1", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.1}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should truncate 0.1ms and leave the date unchanged", - ), - ExpressionTestCase( - "date_double_0_49", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.49}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should truncate 0.49ms and leave the date unchanged", - ), - ExpressionTestCase( - "date_double_0_51", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.51}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$add should round up 0.51ms to 1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_0_6", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.6}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$add should round up 0.6ms to 1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_neg_0_5", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.5}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), - msg="$add should round -0.5ms away from zero to -1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_neg_0_51", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.51}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), - msg="$add should round -0.51ms away from zero to -1ms when adding to a date", - ), -] - -# Property [Date Operand Position]: the date operand may appear in any position among the -# operands; only one date is permitted. -ADD_DATE_POSITION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "number_then_date", - doc={"a": 86400000, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add a date when the numeric operand appears before the date", - ), - ExpressionTestCase( - "date_in_middle", - doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc), "c": 1000}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=datetime(2026, 1, 1, 0, 0, 1, 1000, tzinfo=timezone.utc), - msg="$add should add a date when it appears in the middle of the operand list", - ), -] - -# Property [Date Sign Handling]: adding zero or a negative number of milliseconds to a date -# returns the date unchanged or subtracted. -ADD_DATE_SIGN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_negative", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -86400000}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2025, 12, 31, tzinfo=timezone.utc), - msg="$add should subtract milliseconds from a date when adding a negative number", - ), - ExpressionTestCase( - "date_zero", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should return the same date when adding zero milliseconds", - ), - ExpressionTestCase( - "date_negative_zero", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.0}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should return the same date when adding negative zero", - ), -] - -ADD_DATE_ALL_TESTS = ( - ADD_DATE_NUMERIC_TESTS + ADD_DATE_POSITION_TESTS + ADD_DATE_SIGN_TESTS + ADD_DATE_ROUNDING_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_DATE_ALL_TESTS)) -def test_add_date(collection, test_case: ExpressionTestCase): - """Test $add date arithmetic: numeric types, operand position, and sign handling.""" - 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/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py deleted file mode 100644 index f5c817b3d..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ /dev/null @@ -1,203 +0,0 @@ -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 ( - MORE_THAN_ONE_DATE_ERROR, - OVERFLOW_ERROR, - TYPE_MISMATCH_ERROR, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_NAN, - FLOAT_INFINITY, - FLOAT_NAN, - INT64_MAX, -) - -# Property [Type Strictness]: $add rejects non-numeric, non-date operand types. -ADD_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - f"type_{tid}", - doc={"a": 1, "b": val}, - expression={"$add": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg=f"$add should reject a {tid} operand", - ) - for tid, val in [ - ("string", "string"), - ("bool", True), - ("array", [2, 3]), - ("object", {"a": 2}), - ("regex", Regex("abc")), - ("objectid", ObjectId("507f1f77bcf86cd799439011")), - ("binary", Binary(b"data")), - ("minkey", MinKey()), - ("maxkey", MaxKey()), - ("timestamp", Timestamp(1, 1)), - ("code", Code("function(){}")), - ] -] - -# Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among -# valid numeric operands. -ADD_MIXED_VALID_INVALID_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "mixed_valid_invalid", - doc={"a": 1, "b": 2, "c": "string"}, - expression={"$add": ["$a", "$b", "$c"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should error when a string appears among numeric operands", - ), -] - -# Property [Single Invalid Operand]: $add rejects a single operand of an invalid type. -ADD_SINGLE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "single_string", - doc={"a": "string"}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single string operand", - ), - ExpressionTestCase( - "single_boolean", - doc={"a": True}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single boolean operand", - ), - ExpressionTestCase( - "single_array", - doc={"a": [1, 2]}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single array operand", - ), - ExpressionTestCase( - "single_object", - doc={"a": {"x": 1}}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single object operand", - ), -] - -# Property [Multiple Dates]: $add rejects expressions with more than one date operand. -ADD_MULTIPLE_DATE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "add_two_identical_dates", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 1, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when adding two identical date operands", - ), - ExpressionTestCase( - "two_different_dates", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when adding two different date operands", - ), - ExpressionTestCase( - "two_dates_with_numbers", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": [1, 2, 3, "$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when two dates appear among numeric operands", - ), - ExpressionTestCase( - "dates_separated_by_number", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", 1, "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when two dates are separated by a numeric operand", - ), -] - -# Property [Date with Non-Finite]: $add rejects NaN and Infinity as numeric operands when a -# date is also present, since the resulting date would be non-representable. -ADD_DATE_NON_FINITE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_nan", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and float NaN", - ), - ExpressionTestCase( - "date_infinity", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and float infinity", - ), - ExpressionTestCase( - "date_decimal_nan", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and decimal128 NaN", - ), - ExpressionTestCase( - "date_decimal_infinity", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_INFINITY}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and decimal128 infinity", - ), -] - -# Property [Date Overflow]: $add errors when the millisecond offset would push the date result -# beyond the representable date range. -ADD_DATE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_int64_max", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": INT64_MAX}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding INT64_MAX milliseconds to a date overflows the date range", # noqa: E501 - ), -] - -ADD_ERROR_ALL_TESTS = ( - ADD_TYPE_ERROR_TESTS - + ADD_MIXED_VALID_INVALID_TESTS - + ADD_SINGLE_TYPE_ERROR_TESTS - + ADD_MULTIPLE_DATE_TESTS - + ADD_DATE_NON_FINITE_ERROR_TESTS - + ADD_DATE_OVERFLOW_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) -def test_add_errors(collection, test_case: ExpressionTestCase): - """Test $add type, multiple-date, and date non-finite 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/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py deleted file mode 100644 index 7019c1b0b..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ /dev/null @@ -1,47 +0,0 @@ -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 [Expression Input]: $add evaluates a nested expression argument before summing. -ADD_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nested_add", - doc={"a": 1, "b": 2}, - expression={"$add": [{"$add": ["$a", "$b"]}, 3]}, - expected=6, - msg="$add should evaluate a nested $add expression as an operand", - ), -] - -# Property [Mixed Literal and Field]: $add accepts a mix of field references and inline literals -# in the same operand list. -ADD_MIXED_INPUT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "mixed_literal_and_field", - doc={"a": 10}, - expression={"$add": ["$a", 5]}, - expected=15, - msg="$add should sum a field reference and an inline literal operand", - ), -] - -ADD_INPUT_FORM_ALL_TESTS = ADD_EXPRESSION_INPUT_TESTS + ADD_MIXED_INPUT_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_INPUT_FORM_ALL_TESTS)) -def test_add_input_forms(collection, test_case: ExpressionTestCase): - """Test $add literal, nested expression, and mixed input form 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/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py deleted file mode 100644 index 0343c3316..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ /dev/null @@ -1,154 +0,0 @@ -import math - -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]: $add propagates infinity according to IEEE 754 rules. -ADD_INFINITY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "infinity", - doc={"a": FLOAT_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding infinity and a finite number", - ), - ExpressionTestCase( - "negative_infinity", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding negative infinity and a finite number", # noqa: E501 - ), - ExpressionTestCase( - "single_infinity", - doc={"a": FLOAT_INFINITY}, - expression={"$add": ["$a"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity for a single infinity operand", - ), - ExpressionTestCase( - "inf_plus_inf", - doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding two positive infinities", - ), - ExpressionTestCase( - "neg_inf_plus_neg_inf", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding two negative infinities", - ), - ExpressionTestCase( - "inf_plus_zero", - doc={"a": FLOAT_INFINITY, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding infinity and zero", - ), - ExpressionTestCase( - "neg_inf_plus_zero", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding negative infinity and zero", - ), - ExpressionTestCase( - "decimal_infinity", - doc={"a": DECIMAL128_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_INFINITY, - msg="$add should return decimal128 infinity when adding decimal128 infinity and a number", - ), - ExpressionTestCase( - "decimal_negative_infinity", - doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NEGATIVE_INFINITY, - msg="$add should return decimal128 negative infinity when adding decimal128 negative infinity and a number", # noqa: E501 - ), -] - -# Property [NaN]: $add propagates NaN according to IEEE 754 rules. -ADD_NAN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nan_add_one", - doc={"a": FLOAT_NAN, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float NaN and a finite number", - ), - ExpressionTestCase( - "inf_minus_inf", - doc={"a": FLOAT_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float infinity and negative infinity", - ), - ExpressionTestCase( - "nan_plus_nan", - doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding two float NaN values", - ), - ExpressionTestCase( - "nan_plus_inf", - doc={"a": FLOAT_NAN, "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float NaN and infinity", - ), - ExpressionTestCase( - "decimal_nan", - doc={"a": DECIMAL128_NAN, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding decimal128 NaN and a number", - ), - ExpressionTestCase( - "decimal_nan_plus_nan", - doc={"a": DECIMAL128_NAN, "b": DECIMAL128_NAN}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding two decimal128 NaN values", - ), - ExpressionTestCase( - "decimal_inf_minus_inf", - doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding decimal128 infinity and negative infinity", # noqa: E501 - ), -] - -ADD_NON_FINITE_ALL_TESTS = ADD_INFINITY_TESTS + ADD_NAN_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NON_FINITE_ALL_TESTS)) -def test_add_non_finite(collection, test_case: ExpressionTestCase): - """Test $add infinity and NaN 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/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py deleted file mode 100644 index 9651d52d3..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime, timezone - -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]: $add returns null if any operand is null or refers to a missing -# field. -ADD_NULL_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "single_null", - doc={"a": None}, - expression={"$add": ["$a"]}, - expected=None, - msg="$add should return null for a single null operand", - ), - ExpressionTestCase( - "null_operand", - doc={"a": 1, "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when any operand is null", - ), - ExpressionTestCase( - "missing_field", - doc={}, - expression={"$add": [1, MISSING]}, - expected=None, - msg="$add should return null when any operand is a missing field", - ), - ExpressionTestCase( - "null_with_multiple", - doc={"a": 1, "b": 2, "c": None}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=None, - msg="$add should return null when null appears among multiple operands", - ), - ExpressionTestCase( - "null_in_middle", - doc={"a": 1, "b": 2, "c": 3, "e": 5}, - expression={"$add": ["$a", "$b", "$c", None, "$e"]}, - expected=None, - msg="$add should return null when null appears in the middle of operands", - ), - ExpressionTestCase( - "all_null", - doc={"a": None, "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when all operands are null", - ), - ExpressionTestCase( - "all_missing", - doc={}, - expression={"$add": [MISSING, MISSING]}, - expected=None, - msg="$add should return null when all operands are missing fields", - ), - ExpressionTestCase( - "date_and_null", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when a date is paired with a null operand", - ), -] - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NULL_TESTS)) -def test_add_null(collection, test_case: ExpressionTestCase): - """Test $add 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/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py deleted file mode 100644 index 3c4d6f305..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ /dev/null @@ -1,259 +0,0 @@ -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 [Same-Type Addition]: $add of two values of the same numeric type returns a value of -# that type. -ADD_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "same_type_int32", - doc={"a": 1, "b": 2}, - expression={"$add": ["$a", "$b"]}, - expected=3, - msg="$add should add two int32 values", - ), - ExpressionTestCase( - "same_type_int64", - doc={"a": Int64(10), "b": Int64(20)}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(30), - msg="$add should add two int64 values", - ), - ExpressionTestCase( - "same_type_double", - doc={"a": 1.5, "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=4.0, - msg="$add should add two double values", - ), - ExpressionTestCase( - "same_type_decimal", - doc={"a": Decimal128("10.5"), "b": Decimal128("20.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("31.0"), - msg="$add should add two decimal128 values", - ), -] - -# Property [Type Promotion]: $add promotes to the wider type when operands have different numeric -# types. Precedence: decimal128 > double > int64 > int32. -ADD_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int32_int64", - doc={"a": 1, "b": Int64(20)}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(21), - msg="$add should return int64 when adding int32 and int64", - ), - ExpressionTestCase( - "int32_double", - doc={"a": 1, "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=3.5, - msg="$add should return double when adding int32 and double", - ), - ExpressionTestCase( - "int32_decimal", - doc={"a": 1, "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("3.5"), - msg="$add should return decimal128 when adding int32 and decimal128", - ), - ExpressionTestCase( - "int64_double", - doc={"a": Int64(10), "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=12.5, - msg="$add should return double when adding int64 and double", - ), - ExpressionTestCase( - "int64_decimal", - doc={"a": Int64(10), "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("12.5"), - msg="$add should return decimal128 when adding int64 and decimal128", - ), - ExpressionTestCase( - "double_decimal", - doc={"a": 1.5, "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(Decimal128("4.00000000000000")), - msg="$add should return decimal128 when adding double and decimal128", - ), - ExpressionTestCase( - "three_mixed_types", - doc={"a": Decimal128("1.5"), "b": 2.5, "c": Int64(3)}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=pytest.approx(Decimal128("7.00000000000000")), - msg="$add should return decimal128 when adding decimal128, double, and int64", - ), -] - -# Property [Multiple Operands]: $add correctly sums three or more operands. -ADD_MULTIPLE_OPERANDS_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "multiple_int32", - doc={"a": 1, "b": 2, "c": 3}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=6, - msg="$add should add multiple int32 values", - ), - ExpressionTestCase( - "multiple_int64", - doc={"a": Int64(1), "b": Int64(2), "c": Int64(3)}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=Int64(6), - msg="$add should add multiple int64 values", - ), - ExpressionTestCase( - "multiple_double", - doc={"a": 1.1, "b": 2.2, "c": 3.3}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=pytest.approx(6.6), - msg="$add should add multiple double values", - ), - ExpressionTestCase( - "multiple_decimal", - doc={ - "a": Decimal128("1"), - "b": Decimal128("2"), - "c": Decimal128("3"), - "d": Decimal128("4"), - }, - expression={"$add": ["$a", "$b", "$c", "$d"]}, - expected=Decimal128("10"), - msg="$add should add multiple decimal128 values", - ), - ExpressionTestCase( - "five_operands", - doc={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, - expression={"$add": ["$a", "$b", "$c", "$d", "$e"]}, - expected=15, - msg="$add should correctly sum five int32 operands", - ), - ExpressionTestCase( - "ten_operands", - doc={ - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - "i": 9, - "j": 10, - }, - expression={"$add": ["$a", "$b", "$c", "$d", "$e", "$f", "$g", "$h", "$i", "$j"]}, - expected=55, - msg="$add should correctly sum ten int32 operands", - ), -] - -# Property [Empty and Single Operand]: $add of zero operands returns 0; single operand returns -# that value unchanged. -ADD_SINGLE_AND_EMPTY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "empty", - doc={}, - expression={"$add": []}, - expected=0, - msg="$add should return 0 for empty operand list", - ), - ExpressionTestCase( - "single_int32", - doc={"a": 5}, - expression={"$add": ["$a"]}, - expected=5, - msg="$add should return the value for a single int32 operand", - ), - ExpressionTestCase( - "single_int64", - doc={"a": Int64(0)}, - expression={"$add": ["$a"]}, - expected=Int64(0), - msg="$add should return the value for a single int64 operand", - ), - ExpressionTestCase( - "single_double", - doc={"a": 0.0}, - expression={"$add": ["$a"]}, - expected=0.0, - msg="$add should return the value for a single double operand", - ), - ExpressionTestCase( - "single_decimal", - doc={"a": Decimal128("0")}, - expression={"$add": ["$a"]}, - expected=Decimal128("0"), - msg="$add should return the value for a single decimal128 operand", - ), -] - -# Property [Sign Handling]: $add handles negative values and zero correctly. -ADD_SIGN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "negative_positive", - doc={"a": -5, "b": 3}, - expression={"$add": ["$a", "$b"]}, - expected=-2, - msg="$add should add a negative and a positive int32 value", - ), - ExpressionTestCase( - "both_negative", - doc={"a": -10, "b": -20}, - expression={"$add": ["$a", "$b"]}, - expected=-30, - msg="$add should add two negative int32 values", - ), - ExpressionTestCase( - "zeros", - doc={"a": 0, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=0, - msg="$add should return 0 when adding two int32 zeros", - ), - ExpressionTestCase( - "zero_negative_zero", - doc={"a": 0, "b": -0.0}, - expression={"$add": ["$a", "$b"]}, - expected=0.0, - msg="$add should return 0.0 when adding int32 zero and negative zero double", - ), - ExpressionTestCase( - "sum_to_zero", - doc={"a": 1, "b": 0, "c": -1}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=0, - msg="$add should return 0 when operands sum to zero", - ), -] - -ADD_NUMERIC_ALL_TESTS = ( - ADD_SAME_TYPE_TESTS - + ADD_MIXED_TYPE_TESTS - + ADD_MULTIPLE_OPERANDS_TESTS - + ADD_SINGLE_AND_EMPTY_TESTS - + ADD_SIGN_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NUMERIC_ALL_TESTS)) -def test_add_numeric(collection, test_case: ExpressionTestCase): - """Test $add numeric type combinations, multiple operands, and sign handling.""" - 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/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py deleted file mode 100644 index 29a992905..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ /dev/null @@ -1,95 +0,0 @@ -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 ( - DOUBLE_FROM_INT64_MAX, - FLOAT_INFINITY, - FLOAT_NEGATIVE_INFINITY, - INT32_MAX, - INT32_MIN, - INT32_OVERFLOW, - INT32_UNDERFLOW, - INT64_MAX, - INT64_MIN, -) - -# Property [Int32 Overflow]: when an int32 result exceeds the int32 range, $add promotes to -# int64. -ADD_INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int32_overflow", - doc={"a": INT32_MAX, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(INT32_OVERFLOW), - msg="$add should promote to int64 when the int32 result overflows INT32_MAX", - ), - ExpressionTestCase( - "int32_underflow", - doc={"a": INT32_MIN, "b": -1}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(INT32_UNDERFLOW), - msg="$add should promote to int64 when the int32 result underflows INT32_MIN", - ), -] - -# Property [Int64 Overflow]: when an int64 result exceeds the int64 range, $add promotes to -# double. -ADD_INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int64_overflow", - doc={"a": INT64_MAX, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(DOUBLE_FROM_INT64_MAX), - msg="$add should promote to double when the int64 result overflows INT64_MAX", - ), - ExpressionTestCase( - "int64_underflow", - doc={"a": INT64_MIN, "b": -1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(-DOUBLE_FROM_INT64_MAX), - msg="$add should promote to double when the int64 result underflows INT64_MIN", - ), -] - -# Property [Double Overflow]: when a double result exceeds the double range, $add returns -# infinity. -ADD_DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "double_overflow", - doc={"a": 1e308, "b": 1e308}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return positive infinity on double overflow", - ), - ExpressionTestCase( - "double_underflow", - doc={"a": -1e308, "b": -1e308}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity on double underflow", - ), -] - -ADD_OVERFLOW_ALL_TESTS = ( - ADD_INT32_OVERFLOW_TESTS + ADD_INT64_OVERFLOW_TESTS + ADD_DOUBLE_OVERFLOW_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_OVERFLOW_ALL_TESTS)) -def test_add_overflow(collection, test_case: ExpressionTestCase): - """Test $add integer and double overflow and underflow 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/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py deleted file mode 100644 index f0cdd34ab..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ /dev/null @@ -1,103 +0,0 @@ -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_MAX, - DECIMAL128_MIN, - DECIMAL128_NEGATIVE_INFINITY, -) - -# Property [Decimal128 Precision]: $add preserves decimal128 precision, including exact -# representation of values that are inexact in double. -ADD_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "decimal_precision", - doc={"a": Decimal128("1.5"), "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("4.0"), - msg="$add should preserve decimal128 precision", - ), - ExpressionTestCase( - "decimal_precision_small", - doc={"a": Decimal128("0.1"), "b": Decimal128("0.2")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("0.3"), - msg="$add should exactly represent 0.1 + 0.2 with decimal128", - ), - ExpressionTestCase( - "decimal_large_precision", - doc={ - "a": Decimal128("999999999999999999999999999999999"), - "b": Decimal128("1"), - }, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("1000000000000000000000000000000000"), - msg="$add should handle large decimal128 addition with full precision", - ), - ExpressionTestCase( - "decimal_large_negative_precision", - doc={ - "a": Decimal128("-999999999999999999999999999999999"), - "b": Decimal128("-1"), - }, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("-1000000000000000000000000000000000"), - msg="$add should handle large negative decimal128 addition with full precision", - ), -] - -# Property [Decimal128 Boundaries]: $add at decimal128 boundary values promotes to infinity when -# the result overflows, and returns zero when max and min cancel. -ADD_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "decimal128_max_plus_zero", - doc={"a": DECIMAL128_MAX, "b": Decimal128("0")}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_MAX, - msg="$add should return decimal128 max when adding zero to decimal128 max", - ), - ExpressionTestCase( - "decimal128_max_plus_max", - doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MAX}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_INFINITY, - msg="$add should return decimal128 infinity when adding two decimal128 max values", - ), - ExpressionTestCase( - "decimal128_min_plus_min", - doc={"a": DECIMAL128_MIN, "b": DECIMAL128_MIN}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NEGATIVE_INFINITY, - msg="$add should return decimal128 negative infinity when adding two decimal128 min values", - ), - ExpressionTestCase( - "decimal128_max_plus_min", - doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MIN}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("0E+6111"), - msg="$add should return zero when adding decimal128 max and decimal128 min", - ), -] - -ADD_PRECISION_ALL_TESTS = ADD_DECIMAL_PRECISION_TESTS + ADD_DECIMAL_BOUNDARY_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_PRECISION_ALL_TESTS)) -def test_add_precision(collection, test_case: ExpressionTestCase): - """Test $add decimal128 precision and boundary value 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/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py deleted file mode 100644 index 7af9e0b66..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ /dev/null @@ -1,114 +0,0 @@ -from datetime import datetime, timezone - -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]: $add follows numeric type promotion rules to determine the result type. -# Precedence: decimal128 > double > int64 > int32. Date + numeric always returns date. -ADD_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "return_type_int_int", - doc={"a": 1, "b": 2}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="int", - msg="$add should return int type when adding two int32 values", - ), - ExpressionTestCase( - "return_type_int_long", - doc={"a": 1, "b": Int64(2)}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="long", - msg="$add should return long type when adding int32 and int64", - ), - ExpressionTestCase( - "return_type_int_double", - doc={"a": 1, "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding int32 and double", - ), - ExpressionTestCase( - "return_type_int_decimal", - doc={"a": 1, "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding int32 and decimal128", - ), - ExpressionTestCase( - "return_type_long_long", - doc={"a": Int64(1), "b": Int64(2)}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="long", - msg="$add should return long type when adding two int64 values", - ), - ExpressionTestCase( - "return_type_long_double", - doc={"a": Int64(1), "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding int64 and double", - ), - ExpressionTestCase( - "return_type_long_decimal", - doc={"a": Int64(1), "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding int64 and decimal128", - ), - ExpressionTestCase( - "return_type_double_double", - doc={"a": 1.0, "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding two double values", - ), - ExpressionTestCase( - "return_type_double_decimal", - doc={"a": 1.0, "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding double and decimal128", - ), - ExpressionTestCase( - "return_type_decimal_decimal", - doc={"a": Decimal128("1"), "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding two decimal128 values", - ), - ExpressionTestCase( - "return_type_date_int", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 1000}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="date", - msg="$add should return date type when adding a date and an int32", - ), - ExpressionTestCase( - "return_type_empty", - doc={}, - expression={"$type": {"$add": []}}, - expected="int", - msg="$add should return int type for an empty operand list", - ), -] - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_RETURN_TYPE_TESTS)) -def test_add_return_type(collection, test_case: ExpressionTestCase): - """Test $add return type promotion rules for all numeric type combinations.""" - 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 6cfdd1dd82e8a1c1ca90f9f90e4360896b368b9b Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 2 Jul 2026 13:00:51 -0700 Subject: [PATCH 6/8] Base Tests Signed-off-by: PatersonProjects --- .../subtract/test_subtract_basic.py | 218 ++++++++++ .../subtract/test_subtract_boundaries.py | 374 ++++++++++++++++++ .../subtract/test_subtract_dates.py | 147 +++++++ .../subtract/test_subtract_errors.py | 171 ++++++++ .../subtract/test_subtract_input_forms.py | 91 +++++ .../subtract/test_subtract_non_finite.py | 183 +++++++++ .../arithmetic/subtract/test_subtract_null.py | 69 ++++ 7 files changed, 1253 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py new file mode 100644 index 000000000..2d25127ef --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py @@ -0,0 +1,218 @@ +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 + +# Property [Same Type]: $subtract returns the correct difference for each numeric type. +SUBTRACT_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_type_int32", + doc={"a": 10, "b": 3}, + expression={"$subtract": ["$a", "$b"]}, + expected=7, + msg="Should subtract int32 values", + ), + ExpressionTestCase( + "same_type_int64", + doc={"a": Int64(20), "b": Int64(5)}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(15), + msg="Should subtract int64 values", + ), + ExpressionTestCase( + "same_type_double", + doc={"a": 10.5, "b": 2.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=8.0, + msg="Should subtract double values", + ), + ExpressionTestCase( + "same_type_decimal", + doc={"a": Decimal128("20.5"), "b": Decimal128("10.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("10.0"), + msg="Should subtract decimal128 values", + ), +] + +# Property [Mixed Type]: $subtract promotes mixed numeric types per the arithmetic rules. +SUBTRACT_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_int64", + doc={"a": 10, "b": Int64(3)}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(7), + msg="Should subtract int64 from int32", + ), + ExpressionTestCase( + "int32_double", + doc={"a": 10, "b": 2.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=7.5, + msg="Should subtract double from int32", + ), + ExpressionTestCase( + "int32_decimal", + doc={"a": 10, "b": Decimal128("2.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("7.5"), + msg="Should subtract decimal128 from int32", + ), + ExpressionTestCase( + "int64_double", + doc={"a": Int64(20), "b": 5.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=14.5, + msg="Should subtract double from int64", + ), + ExpressionTestCase( + "int64_decimal", + doc={"a": Int64(20), "b": Decimal128("5.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("14.5"), + msg="Should subtract decimal128 from int64", + ), + ExpressionTestCase( + "double_decimal", + doc={"a": 10.5, "b": Decimal128("2.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("8.0000000000000"), + msg="Should subtract decimal128 from double", + ), +] + +# Property [Sign and Zero]: $subtract handles positive, negative, and zero operands. +SUBTRACT_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_positive", + doc={}, + expression={"$subtract": [10, 3]}, + expected=7, + msg="Should subtract two positive values", + ), + ExpressionTestCase( + "negative_positive", + doc={}, + expression={"$subtract": [-10, 3]}, + expected=-13, + msg="Should subtract positive from negative", + ), + ExpressionTestCase( + "positive_negative", + doc={}, + expression={"$subtract": [10, -3]}, + expected=13, + msg="Should subtract negative from positive", + ), + ExpressionTestCase( + "both_negative", + doc={}, + expression={"$subtract": [-10, -3]}, + expected=-7, + msg="Should subtract two negative values", + ), + ExpressionTestCase( + "zero_minuend", + doc={}, + expression={"$subtract": [0, 5]}, + expected=-5, + msg="Should subtract from zero", + ), + ExpressionTestCase( + "zero_subtrahend", + doc={}, + expression={"$subtract": [5, 0]}, + expected=5, + msg="Should subtract zero", + ), + ExpressionTestCase( + "zeros", + doc={}, + expression={"$subtract": [0, 0]}, + expected=0, + msg="Should subtract zero from zero", + ), + ExpressionTestCase( + "zero_negative_zero", + doc={}, + expression={"$subtract": [0, -0.0]}, + expected=0.0, + msg="Should subtract negative zero from zero", + ), + ExpressionTestCase( + "negative_zero_zero", + doc={}, + expression={"$subtract": [-0.0, 0]}, + expected=-0.0, + msg="Should subtract zero from negative zero", + ), + ExpressionTestCase( + "result_negative", + doc={}, + expression={"$subtract": [5, 10]}, + expected=-5, + msg="Should produce a negative result", + ), + ExpressionTestCase( + "result_negative_double", + doc={}, + expression={"$subtract": [5.5, 10]}, + expected=-4.5, + msg="Should produce a negative double result", + ), +] + +# Property [Precision]: $subtract preserves decimal128 precision. +SUBTRACT_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision", + doc={}, + expression={"$subtract": [Decimal128("10.5"), Decimal128("2.5")]}, + expected=Decimal128("8.0"), + msg="Should preserve decimal128 precision", + ), + ExpressionTestCase( + "decimal_precision_small", + doc={}, + expression={"$subtract": [Decimal128("0.3"), Decimal128("0.1")]}, + expected=Decimal128("0.2"), + msg="Should preserve decimal128 precision for small values", + ), + ExpressionTestCase( + "decimal_large_precision", + doc={}, + expression={ + "$subtract": [ + Decimal128("1000000000000000000000000000000000"), + Decimal128("1"), + ] + }, + expected=Decimal128("999999999999999999999999999999999"), + msg="Should preserve decimal128 precision for large values", + ), +] + +SUBTRACT_BASIC_TESTS = ( + SUBTRACT_SAME_TYPE_TESTS + + SUBTRACT_MIXED_TYPE_TESTS + + SUBTRACT_SIGN_TESTS + + SUBTRACT_PRECISION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_BASIC_TESTS)) +def test_subtract_basic(collection, test_case: ExpressionTestCase): + """Test $subtract basic numeric 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/subtract/test_subtract_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py new file mode 100644 index 000000000..2600d3c96 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py @@ -0,0 +1,374 @@ +"""Boundary and overflow tests for the $subtract operator.""" + +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_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + INT32_MAX, + INT32_MAX_MINUS_1, + INT32_MIN, + INT32_MIN_PLUS_1, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +# Property [Int32 Boundaries]: $subtract at int32 limits promotes results when needed. +SUBTRACT_INT32_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_overflow", + doc={}, + expression={"$subtract": [INT32_MAX, -1]}, + expected=Int64(INT32_OVERFLOW), + msg="Should promote int32 overflow result to int64", + ), + ExpressionTestCase( + "int32_underflow", + doc={}, + expression={"$subtract": [INT32_MIN, 1]}, + expected=Int64(INT32_UNDERFLOW), + msg="Should promote int32 underflow result to int64", + ), + ExpressionTestCase( + "int32_max_minuend", + doc={}, + expression={"$subtract": [INT32_MAX, 10]}, + expected=2147483637, + msg="Should subtract from INT32_MAX", + ), + ExpressionTestCase( + "int32_max_minus_1_minuend", + doc={}, + expression={"$subtract": [INT32_MAX_MINUS_1, 10]}, + expected=2147483636, + msg="Should subtract from INT32_MAX-1", + ), + ExpressionTestCase( + "int32_min_minuend", + doc={}, + expression={"$subtract": [INT32_MIN, 10]}, + expected=Int64(-2147483658), + msg="Should subtract from INT32_MIN", + ), + ExpressionTestCase( + "int32_min_plus_1_minuend", + doc={}, + expression={"$subtract": [INT32_MIN_PLUS_1, 10]}, + expected=Int64(-2147483657), + msg="Should subtract from INT32_MIN+1", + ), + ExpressionTestCase( + "int32_max_subtrahend", + doc={}, + expression={"$subtract": [10, INT32_MAX]}, + expected=-2147483637, + msg="Should subtract INT32_MAX", + ), + ExpressionTestCase( + "int32_min_subtrahend", + doc={}, + expression={"$subtract": [10, INT32_MIN]}, + expected=Int64(2147483658), + msg="Should subtract INT32_MIN", + ), +] + +# Property [Int64 Boundaries]: $subtract at int64 limits overflows to double. +SUBTRACT_INT64_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_overflow", + doc={}, + expression={"$subtract": [INT64_MAX, -1]}, + expected=9.223372036854776e18, + msg="Should overflow int64 subtraction to double", + ), + ExpressionTestCase( + "int64_underflow", + doc={}, + expression={"$subtract": [INT64_MIN, 1]}, + expected=-9.223372036854776e18, + msg="Should underflow int64 subtraction to double", + ), + ExpressionTestCase( + "int64_max_minuend", + doc={}, + expression={"$subtract": [INT64_MAX, Int64(10)]}, + expected=Int64(9223372036854775797), + msg="Should subtract from INT64_MAX", + ), + ExpressionTestCase( + "int64_max_minus_1_minuend", + doc={}, + expression={"$subtract": [INT64_MAX_MINUS_1, Int64(10)]}, + expected=Int64(9223372036854775796), + msg="Should subtract from INT64_MAX-1", + ), + ExpressionTestCase( + "int64_min_minuend", + doc={}, + expression={"$subtract": [INT64_MIN, Int64(10)]}, + expected=-9.223372036854776e18, + msg="Should subtract from INT64_MIN", + ), + ExpressionTestCase( + "int64_min_plus_1_minuend", + doc={}, + expression={"$subtract": [INT64_MIN_PLUS_1, Int64(10)]}, + expected=-9.223372036854776e18, + msg="Should subtract from INT64_MIN+1", + ), + ExpressionTestCase( + "int64_max_subtrahend", + doc={}, + expression={"$subtract": [Int64(10), INT64_MAX]}, + expected=Int64(-9223372036854775797), + msg="Should subtract INT64_MAX", + ), + ExpressionTestCase( + "int64_min_subtrahend", + doc={}, + expression={"$subtract": [Int64(10), INT64_MIN]}, + expected=9.223372036854776e18, + msg="Should subtract INT64_MIN", + ), +] + +# Property [Double Boundaries]: $subtract handles double extremes. +SUBTRACT_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_overflow", + doc={}, + expression={"$subtract": [1e308, -1e308]}, + expected=float("inf"), + msg="Should overflow double subtraction to infinity", + ), + ExpressionTestCase( + "double_underflow", + doc={}, + expression={"$subtract": [-1e308, 1e308]}, + expected=float("-inf"), + msg="Should underflow double subtraction to negative infinity", + ), + ExpressionTestCase( + "double_min_subnormal_minuend", + doc={}, + expression={"$subtract": [DOUBLE_MIN_SUBNORMAL, 0]}, + expected=5e-324, + msg="Should subtract from smallest subnormal double", + ), + ExpressionTestCase( + "double_near_min_minuend", + doc={}, + expression={"$subtract": [DOUBLE_NEAR_MIN, 0]}, + expected=1e-308, + msg="Should subtract from near-min double", + ), + ExpressionTestCase( + "double_near_max_minuend", + doc={}, + expression={"$subtract": [DOUBLE_NEAR_MAX, 1e307]}, + expected=9e307, + msg="Should subtract near-max double", + ), + ExpressionTestCase( + "double_max_safe_integer_minuend", + doc={}, + expression={"$subtract": [float(DOUBLE_MAX_SAFE_INTEGER), 1]}, + expected=9007199254740991.0, + msg="Should subtract from max safe integer double", + ), + ExpressionTestCase( + "double_max_safe_integer_subtrahend", + doc={}, + expression={"$subtract": [1, float(DOUBLE_MAX_SAFE_INTEGER)]}, + expected=-9007199254740991.0, + msg="Should subtract max safe integer double", + ), + ExpressionTestCase( + "double_half_minuend", + doc={}, + expression={"$subtract": [0.5, 0.25]}, + expected=0.25, + msg="Should subtract double half boundary", + ), + ExpressionTestCase( + "double_one_and_half_minuend", + doc={}, + expression={"$subtract": [1.5, 0.5]}, + expected=1.0, + msg="Should subtract double one-and-half boundary", + ), + ExpressionTestCase( + "double_two_and_half_minuend", + doc={}, + expression={"$subtract": [2.5, 1.5]}, + expected=1.0, + msg="Should subtract double two-and-half boundary", + ), + ExpressionTestCase( + "double_negative_half_minuend", + doc={}, + expression={"$subtract": [-0.5, 0.5]}, + expected=-1.0, + msg="Should subtract double negative half boundary", + ), + ExpressionTestCase( + "double_negative_one_and_half_minuend", + doc={}, + expression={"$subtract": [-1.5, 0.5]}, + expected=-2.0, + msg="Should subtract double negative one-and-half boundary", + ), + ExpressionTestCase( + "double_just_below_half_minuend", + doc={}, + expression={"$subtract": [DOUBLE_JUST_BELOW_HALF, 0.25]}, + expected=pytest.approx(0.2499999999999994), + msg="Should subtract double just below half boundary", + ), + ExpressionTestCase( + "double_just_above_half_minuend", + doc={}, + expression={"$subtract": [DOUBLE_JUST_ABOVE_HALF, 0.25]}, + expected=pytest.approx(0.250000001), + msg="Should subtract double just above half boundary", + ), +] + +# Property [Decimal128 Boundaries]: $subtract preserves decimal128 precision and extremes. +SUBTRACT_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_MAX, Decimal128("1")]}, + expected=DECIMAL128_MAX, + msg="Should subtract from decimal128 max", + ), + ExpressionTestCase( + "decimal128_min_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_MIN, Decimal128("1")]}, + expected=DECIMAL128_MIN, + msg="Should subtract from decimal128 min", + ), + ExpressionTestCase( + "decimal128_small_exponent_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_SMALL_EXPONENT, Decimal128("0")]}, + expected=DECIMAL128_SMALL_EXPONENT, + msg="Should subtract from decimal128 with small exponent", + ), + ExpressionTestCase( + "decimal128_large_exponent_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_LARGE_EXPONENT, Decimal128("1")]}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="Should subtract from decimal128 with large exponent", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + doc={}, + expression={"$subtract": [DECIMAL128_TRAILING_ZERO, Decimal128("0.5")]}, + expected=Decimal128("0.5"), + msg="Should preserve decimal128 trailing zero", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + doc={}, + expression={"$subtract": [DECIMAL128_MANY_TRAILING_ZEROS, Decimal128("0.5")]}, + expected=Decimal128("0.50000000000000000000000000000000"), + msg="Should preserve decimal128 many trailing zeros", + ), + ExpressionTestCase( + "decimal_half_minuend", + doc={}, + expression={"$subtract": [Decimal128("0.5"), Decimal128("0.25")]}, + expected=Decimal128("0.25"), + msg="Should subtract decimal half boundary", + ), + ExpressionTestCase( + "decimal_one_and_half_minuend", + doc={}, + expression={"$subtract": [Decimal128("1.5"), Decimal128("0.5")]}, + expected=Decimal128("1.0"), + msg="Should subtract decimal one-and-half boundary", + ), + ExpressionTestCase( + "decimal_two_and_half_minuend", + doc={}, + expression={"$subtract": [Decimal128("2.5"), Decimal128("1.5")]}, + expected=Decimal128("1.0"), + msg="Should subtract decimal two-and-half boundary", + ), + ExpressionTestCase( + "decimal_negative_half_minuend", + doc={}, + expression={"$subtract": [Decimal128("-0.5"), Decimal128("0.5")]}, + expected=Decimal128("-1.0"), + msg="Should subtract decimal negative half boundary", + ), + ExpressionTestCase( + "decimal_negative_one_and_half_minuend", + doc={}, + expression={"$subtract": [Decimal128("-1.5"), Decimal128("0.5")]}, + expected=Decimal128("-2.0"), + msg="Should subtract decimal negative one-and-half boundary", + ), + ExpressionTestCase( + "decimal_just_below_half_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_JUST_BELOW_HALF, Decimal128("0.25")]}, + expected=Decimal128("0.2499999999999999999999999999999999"), + msg="Should subtract decimal just below half boundary", + ), + ExpressionTestCase( + "decimal_just_above_half_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_JUST_ABOVE_HALF, Decimal128("0.25")]}, + expected=Decimal128("0.2500000000000000000000000000000001"), + msg="Should subtract decimal just above half boundary", + ), +] + +SUBTRACT_BOUNDARY_TESTS = ( + SUBTRACT_INT32_BOUNDARY_TESTS + + SUBTRACT_INT64_BOUNDARY_TESTS + + SUBTRACT_DOUBLE_BOUNDARY_TESTS + + SUBTRACT_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_BOUNDARY_TESTS)) +def test_subtract_boundaries(collection, test_case: ExpressionTestCase): + """Test $subtract boundary and overflow 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/subtract/test_subtract_dates.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py new file mode 100644 index 000000000..f897a0f16 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py @@ -0,0 +1,147 @@ +"""Date arithmetic tests for the $subtract operator.""" + +from __future__ import annotations + +from datetime import datetime, timezone + +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.error_codes import TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Date minus numeric]: $subtract subtracts a numeric duration in milliseconds from a date. +SUBTRACT_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int32", + doc={"a": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), "b": 86400000}, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="Should subtract int32 milliseconds from a date", + ), + ExpressionTestCase( + "date_int64", + doc={"a": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), "b": Int64(86400000)}, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="Should subtract int64 milliseconds from a date", + ), + ExpressionTestCase( + "date_decimal", + doc={"a": datetime(2026, 1, 1, 0, 0, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 998000, tzinfo=timezone.utc), + msg="Should subtract decimal128 milliseconds from a date", + ), + ExpressionTestCase( + "date_double_round_up", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), 2.5]}, + expected=datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc), + msg="Should round up when subtracting fractional double milliseconds", + ), + ExpressionTestCase( + "date_double_truncates", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, 5000, tzinfo=timezone.utc), 4.4]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="Should truncate when subtracting fractional double milliseconds", + ), + ExpressionTestCase( + "date_negative", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), -86400000]}, + expected=datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + msg="Should subtract negative milliseconds from a date", + ), + ExpressionTestCase( + "date_zero", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), 0]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="Should subtract zero milliseconds from a date", + ), + ExpressionTestCase( + "date_negative_zero", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), -0.0]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="Should subtract negative zero from a date", + ), +] + +# Property [Date minus date]: $subtract returns the duration in milliseconds between two dates. +SUBTRACT_DATE_DATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "two_dates", + doc={ + "a": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "b": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(86400000), + msg="Should subtract an earlier date from a later date", + ), + ExpressionTestCase( + "two_dates_negative", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(-86400000), + msg="Should subtract a later date from an earlier date", + ), +] + +# Property [Date errors]: $subtract rejects invalid date arithmetic. +SUBTRACT_DATE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "number_minus_date", + doc={}, + expression={"$subtract": [1, datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc)]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject subtracting a date from a number", + ), + ExpressionTestCase( + "date_minus_string", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), "string"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject subtracting a string from a date", + ), + ExpressionTestCase( + "date_minus_bool", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), True]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject subtracting a boolean from a date", + ), + ExpressionTestCase( + "date_minus_array", + doc={}, + expression={"$subtract": [datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), [1, 2]]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject subtracting an array from a date", + ), +] + +SUBTRACT_DATE_TESTS = ( + SUBTRACT_DATE_NUMERIC_TESTS + SUBTRACT_DATE_DATE_TESTS + SUBTRACT_DATE_ERROR_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_DATE_TESTS)) +def test_subtract_dates(collection, test_case: ExpressionTestCase): + """Test $subtract date arithmetic 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/subtract/test_subtract_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py new file mode 100644 index 000000000..49ed62883 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py @@ -0,0 +1,171 @@ +"""Error tests for the $subtract operator.""" + +from __future__ import annotations + +import pytest +from bson import Binary, MaxKey, MinKey, 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, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Type Strictness]: $subtract rejects non-numeric operands. +SUBTRACT_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "string_subtrahend", + doc={}, + expression={"$subtract": [10, "string"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject string subtrahend", + ), + ExpressionTestCase( + "string_minuend", + doc={}, + expression={"$subtract": ["string", 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject string minuend", + ), + ExpressionTestCase( + "boolean_subtrahend", + doc={}, + expression={"$subtract": [10, True]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject boolean subtrahend", + ), + ExpressionTestCase( + "boolean_minuend", + doc={}, + expression={"$subtract": [True, 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject boolean minuend", + ), + ExpressionTestCase( + "array_subtrahend", + doc={}, + expression={"$subtract": [10, [2, 3]]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject array subtrahend", + ), + ExpressionTestCase( + "array_minuend", + doc={}, + expression={"$subtract": [[2, 3], 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject array minuend", + ), + ExpressionTestCase( + "object_subtrahend", + doc={}, + expression={"$subtract": [10, {"a": 2}]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject object subtrahend", + ), + ExpressionTestCase( + "object_minuend", + doc={}, + expression={"$subtract": [{"a": 2}, 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject object minuend", + ), + ExpressionTestCase( + "binary_minuend", + doc={"a": Binary(b"test", 0)}, + expression={"$subtract": ["$a", 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject binary minuend", + ), + ExpressionTestCase( + "binary_subtrahend", + doc={"a": Binary(b"test", 0)}, + expression={"$subtract": [10, "$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject binary subtrahend", + ), + ExpressionTestCase( + "timestamp_minuend", + doc={"a": Timestamp(1234567890, 1)}, + expression={"$subtract": ["$a", 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject timestamp minuend", + ), + ExpressionTestCase( + "timestamp_subtrahend", + doc={"a": Timestamp(1234567890, 1)}, + expression={"$subtract": [10, "$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject timestamp subtrahend", + ), + ExpressionTestCase( + "maxkey_minuend", + doc={"a": MaxKey()}, + expression={"$subtract": ["$a", 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject maxkey minuend", + ), + ExpressionTestCase( + "maxkey_subtrahend", + doc={"a": MaxKey()}, + expression={"$subtract": [10, "$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject maxkey subtrahend", + ), + ExpressionTestCase( + "minkey_minuend", + doc={"a": MinKey()}, + expression={"$subtract": ["$a", 5]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject minkey minuend", + ), + ExpressionTestCase( + "minkey_subtrahend", + doc={"a": MinKey()}, + expression={"$subtract": [10, "$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject minkey subtrahend", + ), +] + +# Property [Arity]: $subtract requires exactly two arguments. +SUBTRACT_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_args", + doc={}, + expression={"$subtract": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="Should reject zero arguments", + ), + ExpressionTestCase( + "one_arg", + doc={}, + expression={"$subtract": [1]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="Should reject one argument", + ), + ExpressionTestCase( + "three_args", + doc={}, + expression={"$subtract": [1, 2, 3]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="Should reject three arguments", + ), +] + +SUBTRACT_ERROR_ALL_TESTS = SUBTRACT_TYPE_ERROR_TESTS + SUBTRACT_ARITY_ERROR_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_ERROR_ALL_TESTS)) +def test_subtract_errors(collection, test_case: ExpressionTestCase): + """Test $subtract 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/subtract/test_subtract_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py new file mode 100644 index 000000000..b5fd50c13 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py @@ -0,0 +1,91 @@ +"""Input form and field path tests for the $subtract operator.""" + +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.error_codes import EXPRESSION_TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Literal Input]: $subtract evaluates inline literal arguments. +# Property [Expression Input]: $subtract evaluates nested expressions. +# Property [Field Path]: $subtract resolves field paths from documents. +SUBTRACT_INPUT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_operands", + doc={}, + expression={"$subtract": [10, 3]}, + expected=7, + msg="Should subtract literal values", + ), + ExpressionTestCase( + "nested_subtract_2", + doc={}, + expression={"$subtract": [{"$subtract": [10, 3]}, 2]}, + expected=5, + msg="Should subtract nested expression results", + ), + ExpressionTestCase( + "nested_subtract_3", + doc={}, + expression={"$subtract": [{"$subtract": [{"$subtract": [100, 10]}, 20]}, 30]}, + expected=40, + msg="Should subtract deeply nested expression results", + ), + ExpressionTestCase( + "nested_field", + doc={"a": {"b": 10}, "c": 3}, + expression={"$subtract": ["$a.b", "$c"]}, + expected=7, + msg="Should subtract resolved nested field values", + ), + ExpressionTestCase( + "nonexistent_field", + doc={"a": {"missing": 1}, "b": 5}, + expression={"$subtract": ["$a.nonexistent", "$b"]}, + expected=None, + msg="Should return null when a field path does not exist", + ), + ExpressionTestCase( + "array_index", + doc={"arr": [10, 5, 2]}, + expression={ + "$subtract": [ + {"$arrayElemAt": ["$arr", 0]}, + {"$arrayElemAt": ["$arr", 1]}, + ] + }, + expected=5, + msg="Should subtract values accessed via array index expressions", + ), + ExpressionTestCase( + "deeply_nested_field", + doc={"a": {"b": {"c": {"d": 20}}}}, + expression={"$subtract": ["$a.b.c.d", 8]}, + expected=12, + msg="Should subtract a deeply nested field value", + ), + ExpressionTestCase( + "composite_array_field", + doc={"x": [{"y": 10}, {"y": 3}]}, + expression={"$subtract": "$x.y"}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="Should reject composite array from field path", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_INPUT_FORM_TESTS)) +def test_subtract_input_forms(collection, test_case: ExpressionTestCase): + """Test $subtract input form and field path 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/subtract/test_subtract_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py new file mode 100644 index 000000000..b27f7cc95 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py @@ -0,0 +1,183 @@ +"""Non-finite (NaN/Infinity) tests for the $subtract operator.""" + +from __future__ import annotations + +import math + +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]: $subtract propagates infinity values. +SUBTRACT_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "infinity", + doc={}, + expression={"$subtract": [FLOAT_INFINITY, 1]}, + expected=FLOAT_INFINITY, + msg="Should subtract from positive infinity", + ), + ExpressionTestCase( + "negative_infinity", + doc={}, + expression={"$subtract": [FLOAT_NEGATIVE_INFINITY, 1]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should subtract from negative infinity", + ), + ExpressionTestCase( + "subtract_infinity", + doc={}, + expression={"$subtract": [1, FLOAT_INFINITY]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should subtract positive infinity", + ), + ExpressionTestCase( + "subtract_neg_infinity", + doc={}, + expression={"$subtract": [1, FLOAT_NEGATIVE_INFINITY]}, + expected=FLOAT_INFINITY, + msg="Should subtract negative infinity", + ), + ExpressionTestCase( + "inf_minus_zero", + doc={}, + expression={"$subtract": [FLOAT_INFINITY, 0]}, + expected=FLOAT_INFINITY, + msg="Should subtract zero from positive infinity", + ), + ExpressionTestCase( + "neg_inf_minus_zero", + doc={}, + expression={"$subtract": [FLOAT_NEGATIVE_INFINITY, 0]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should subtract zero from negative infinity", + ), + ExpressionTestCase( + "inf_minus_inf", + doc={}, + expression={"$subtract": [FLOAT_INFINITY, FLOAT_INFINITY]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for infinity minus infinity", + ), + ExpressionTestCase( + "neg_inf_minus_neg_inf", + doc={}, + expression={"$subtract": [FLOAT_NEGATIVE_INFINITY, FLOAT_NEGATIVE_INFINITY]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for negative infinity minus negative infinity", + ), + ExpressionTestCase( + "decimal_infinity", + doc={}, + expression={"$subtract": [DECIMAL128_INFINITY, 1]}, + expected=DECIMAL128_INFINITY, + msg="Should subtract from decimal infinity", + ), + ExpressionTestCase( + "decimal_negative_infinity", + doc={}, + expression={"$subtract": [DECIMAL128_NEGATIVE_INFINITY, 1]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="Should subtract from decimal negative infinity", + ), + ExpressionTestCase( + "decimal_inf_minus_inf", + doc={}, + expression={"$subtract": [DECIMAL128_INFINITY, DECIMAL128_INFINITY]}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN for decimal infinity minus decimal infinity", + ), + ExpressionTestCase( + "decimal_inf_minus_int", + doc={}, + expression={"$subtract": [DECIMAL128_INFINITY, 1]}, + expected=DECIMAL128_INFINITY, + msg="Should subtract int from decimal infinity", + ), + ExpressionTestCase( + "int_minus_decimal_inf", + doc={}, + expression={"$subtract": [1, DECIMAL128_INFINITY]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="Should subtract decimal infinity from int", + ), +] + +# Property [NaN]: $subtract propagates NaN values. +SUBTRACT_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_minuend", + doc={}, + expression={"$subtract": [FLOAT_NAN, 1]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN when minuend is NaN", + ), + ExpressionTestCase( + "nan_subtrahend", + doc={}, + expression={"$subtract": [10, FLOAT_NAN]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN when subtrahend is NaN", + ), + ExpressionTestCase( + "both_nan", + doc={}, + expression={"$subtract": [FLOAT_NAN, FLOAT_NAN]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN when both operands are NaN", + ), + ExpressionTestCase( + "decimal_nan_minuend", + doc={}, + expression={"$subtract": [DECIMAL128_NAN, 1]}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN when minuend is decimal NaN", + ), + ExpressionTestCase( + "decimal_nan_subtrahend", + doc={}, + expression={"$subtract": [10, DECIMAL128_NAN]}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN when subtrahend is decimal NaN", + ), + ExpressionTestCase( + "decimal_nan_minus_double", + doc={}, + expression={"$subtract": [DECIMAL128_NAN, 1.5]}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN for decimal NaN minus double", + ), + ExpressionTestCase( + "double_minus_decimal_nan", + doc={}, + expression={"$subtract": [1.5, DECIMAL128_NAN]}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN for double minus decimal NaN", + ), +] + +SUBTRACT_NON_FINITE_TESTS = SUBTRACT_INFINITY_TESTS + SUBTRACT_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_NON_FINITE_TESTS)) +def test_subtract_non_finite(collection, test_case: ExpressionTestCase): + """Test $subtract non-finite value 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/subtract/test_subtract_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py new file mode 100644 index 000000000..0b2652a65 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py @@ -0,0 +1,69 @@ +"""Null and missing field tests for the $subtract operator.""" + +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 Propagation]: $subtract returns null when either operand is null or missing. +SUBTRACT_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_subtrahend", + doc={"a": 10}, + expression={"$subtract": ["$a", None]}, + expected=None, + msg="Should return null when subtrahend is null", + ), + ExpressionTestCase( + "null_minuend", + doc={"a": 5}, + expression={"$subtract": [None, "$a"]}, + expected=None, + msg="Should return null when minuend is null", + ), + ExpressionTestCase( + "missing_subtrahend", + doc={"a": 10}, + expression={"$subtract": ["$a", "$missing"]}, + expected=None, + msg="Should return null when subtrahend is missing", + ), + ExpressionTestCase( + "missing_minuend", + doc={"a": 5}, + expression={"$subtract": ["$missing", "$a"]}, + expected=None, + msg="Should return null when minuend is missing", + ), + ExpressionTestCase( + "both_null", + doc={}, + expression={"$subtract": [None, None]}, + expected=None, + msg="Should return null when both operands are null", + ), + ExpressionTestCase( + "missing_minuend_invalid_subtrahend", + doc={"a": True}, + expression={"$subtract": ["$missing", "$a"]}, + expected=None, + msg="Should return null when minuend is missing (null propagates before type check)", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_NULL_TESTS)) +def test_subtract_null(collection, test_case: ExpressionTestCase): + """Test $subtract null and missing field 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 a49322d8734615004c7c5d0d671638dc50754ac7 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 2 Jul 2026 13:18:41 -0700 Subject: [PATCH 7/8] Fix to enable CI passing rather than skipping Signed-off-by: PatersonProjects --- documentdb_tests/conftest.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/documentdb_tests/conftest.py b/documentdb_tests/conftest.py index 1afe6dd5c..96f58100e 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -375,10 +375,12 @@ def pytest_collection_modifyitems(session, config, items): config.hook.pytest_deselected(items=requires_deselected) items[:] = kept - # Skip skip_localhost tests whose target connects to a localhost server. - # Resolved per target (mirroring the requires(...) gate above) so a run - # against multiple targets skips only the localhost-bound parametrizations - # while still exercising a remote --connection-string override. + # Skip skip_localhost tests whose target's server sees the connection as + # originating from localhost. This uses the server-side ``whatsmyuri`` + # command rather than inspecting the connection string, because network + # layers (e.g. Docker port mapping) can make a client-side "localhost" URI + # arrive from a non-local IP at the server — in which case the test is safe + # to run (the server will not treat it as a privileged local connection). localhost_hosts = {"localhost", "127.0.0.1", "::1"} localhost_by_target: dict[str, bool] = {} for item in items: @@ -390,10 +392,22 @@ def pytest_collection_modifyitems(session, config, items): is_localhost = localhost_by_target.get(target.connection_string) if is_localhost is None: try: - nodes = parse_uri(target.connection_string)["nodelist"] + from pymongo import MongoClient + + client = MongoClient(target.connection_string, serverSelectionTimeoutMS=5000) + result = client.admin.command("whatsmyuri") + client.close() + # result["you"] is "host:port" as seen by the server + server_sees_host = result["you"].rsplit(":", 1)[0] + is_localhost = server_sees_host in localhost_hosts except Exception: - nodes = [] - is_localhost = any(host in localhost_hosts for host, _ in nodes) + # Fall back to connection-string check if we cannot reach the + # server (e.g. during --collect-only without a live target). + try: + nodes = parse_uri(target.connection_string)["nodelist"] + except Exception: + nodes = [] + is_localhost = any(host in localhost_hosts for host, _ in nodes) localhost_by_target[target.connection_string] = is_localhost if is_localhost: item.add_marker( From 89e065764957632ee2d684068ec136d7f87c8268 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 3 Jul 2026 11:04:39 -0700 Subject: [PATCH 8/8] Changed fix to implement the new preconditions feature Signed-off-by: PatersonProjects --- docs/testing/TEST_FORMAT.md | 12 +++++ .../commands/shutdown/test_smoke_shutdown.py | 2 +- documentdb_tests/conftest.py | 48 +------------------ documentdb_tests/framework/preconditions.py | 20 +++++++- documentdb_tests/pytest.ini | 1 - 5 files changed, 34 insertions(+), 49 deletions(-) diff --git a/docs/testing/TEST_FORMAT.md b/docs/testing/TEST_FORMAT.md index 5d6b3cc40..596182a47 100644 --- a/docs/testing/TEST_FORMAT.md +++ b/docs/testing/TEST_FORMAT.md @@ -126,3 +126,15 @@ A pytest hook auto-validates during collection: - Must use assertion helpers, not plain `assert` - One assertion per test function - Must use `execute_command()` or helpers from utils + +## Remote-Target Tests + +Some tests are only meaningful when the client is connecting to a remote server — for example, commands whose behavior differs depending on whether the connection is local. Gate these with the `requires` marker and the `remote_target` capability: + +```python +@pytest.mark.requires(remote_target=True) +def test_shutdown_remote_only(collection): + ... +``` + +The harness detects the connection source from the server-side `whatsmyuri` command. If the server reports a localhost address (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) or the target cannot be reached, the test is deselected rather than run. diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py b/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py index c29b6b8f3..3cff00dcb 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/shutdown/test_smoke_shutdown.py @@ -9,7 +9,7 @@ from documentdb_tests.framework.assertions import assertFailure from documentdb_tests.framework.executor import execute_admin_command -pytestmark = [pytest.mark.smoke, pytest.mark.skip_localhost] +pytestmark = [pytest.mark.smoke, pytest.mark.requires(remote_target=True)] def test_smoke_shutdown(collection): diff --git a/documentdb_tests/conftest.py b/documentdb_tests/conftest.py index 96f58100e..ba8d11d41 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -16,8 +16,6 @@ from pathlib import Path # noqa: E402 -from pymongo.uri_parser import parse_uri # noqa: E402 - from documentdb_tests.framework import fixtures # noqa: E402 from documentdb_tests.framework.engine_registry import ( # noqa: E402 Target, @@ -341,12 +339,9 @@ def pytest_collection_modifyitems(session, config, items): Tests carrying a ``requires`` marker are deselected when their target's capabilities do not match what the test requires, so they do not run against a target they do not apply to (rather than appearing as skips). A target's - capabilities are determined by its engine and topology, resolved per target - at runtime (see ``framework.preconditions``). + capabilities are determined by its engine, topology, and connection source, + resolved per target at runtime (see ``framework.preconditions``). - Tests marked 'skip_localhost' are skipped when their target connects to a - localhost server -- a safety guard for destructive commands (e.g. shutdown) - that behave differently from a local client. """ # Deselect a capability-gated test when its target's capabilities do not # match its requires(...) marker. Each item is parametrized over a target; @@ -375,45 +370,6 @@ def pytest_collection_modifyitems(session, config, items): config.hook.pytest_deselected(items=requires_deselected) items[:] = kept - # Skip skip_localhost tests whose target's server sees the connection as - # originating from localhost. This uses the server-side ``whatsmyuri`` - # command rather than inspecting the connection string, because network - # layers (e.g. Docker port mapping) can make a client-side "localhost" URI - # arrive from a non-local IP at the server — in which case the test is safe - # to run (the server will not treat it as a privileged local connection). - localhost_hosts = {"localhost", "127.0.0.1", "::1"} - localhost_by_target: dict[str, bool] = {} - for item in items: - if not item.get_closest_marker("skip_localhost"): - continue - target = _item_target(item) - if target is None: - continue - is_localhost = localhost_by_target.get(target.connection_string) - if is_localhost is None: - try: - from pymongo import MongoClient - - client = MongoClient(target.connection_string, serverSelectionTimeoutMS=5000) - result = client.admin.command("whatsmyuri") - client.close() - # result["you"] is "host:port" as seen by the server - server_sees_host = result["you"].rsplit(":", 1)[0] - is_localhost = server_sees_host in localhost_hosts - except Exception: - # Fall back to connection-string check if we cannot reach the - # server (e.g. during --collect-only without a live target). - try: - nodes = parse_uri(target.connection_string)["nodelist"] - except Exception: - nodes = [] - is_localhost = any(host in localhost_hosts for host, _ in nodes) - localhost_by_target[target.connection_string] = is_localhost - if is_localhost: - item.add_marker( - pytest.mark.skip(reason="skipped on localhost (server connection is local)") - ) - # Deselect no_parallel tests when running under xdist is_xdist = bool(getattr(config.option, "numprocesses", None)) or hasattr(config, "workerinput") if is_xdist: diff --git a/documentdb_tests/framework/preconditions.py b/documentdb_tests/framework/preconditions.py index 8c3bf2534..416266b55 100644 --- a/documentdb_tests/framework/preconditions.py +++ b/documentdb_tests/framework/preconditions.py @@ -57,6 +57,7 @@ "reindex": "reIndex is permitted", "local_rename": "renaming into the unreplicated local database is permitted", "replication": "replication commands are available (applyOps, oplog access)", + "remote_target": "the server sees the connection as originating from a non-localhost address", } # The capabilities each (engine, topology) target has. To add an engine or @@ -72,6 +73,7 @@ "cluster_read_concern", "quorum_write_concern", "replication", + "remote_target", } ), ("mongodb", "standalone"): frozenset( @@ -79,6 +81,7 @@ "unforced_compact", "reindex", "local_rename", + "remote_target", } ), ("documentdb", "standalone"): frozenset( @@ -93,6 +96,7 @@ "unforced_compact", "reindex", "replication", + "remote_target", } ), } @@ -148,6 +152,16 @@ def _detect_topology(engine: str, client: MongoClient) -> str: raise ValueError(f"unknown engine {engine!r}; cannot classify topology") +_LOCALHOST_HOSTS = frozenset({"localhost", "127.0.0.1", "::1", "0.0.0.0"}) + + +def _is_remote_target(client: MongoClient) -> bool: + """Return whether the server sees this connection as non-localhost.""" + result = client.admin.command("whatsmyuri") + server_sees_host = result["you"].rsplit(":", 1)[0] + return server_sees_host not in _LOCALHOST_HOSTS + + def marker_spec() -> str: """Return the pytest marker definition line for the ``requires`` marker.""" return ( @@ -175,6 +189,7 @@ def detect_capabilities(engine: str, connection_string: str) -> frozenset[str]: try: topology = _detect_topology(engine, client) + is_remote = _is_remote_target(client) except Exception: return frozenset() finally: @@ -186,7 +201,10 @@ def detect_capabilities(engine: str, connection_string: str) -> frozenset[str]: f"no capability mapping for target profile {profile}; " "add it to _CAPABILITIES_BY_PROFILE" ) - return _CAPABILITIES_BY_PROFILE[profile] + capabilities = set(_CAPABILITIES_BY_PROFILE[profile]) + if not is_remote: + capabilities.discard("remote_target") + return frozenset(capabilities) def unmet_requirements(required: dict[str, bool], capabilities: frozenset[str]) -> dict[str, bool]: diff --git a/documentdb_tests/pytest.ini b/documentdb_tests/pytest.ini index bcf9c3387..b2cd6c4a1 100644 --- a/documentdb_tests/pytest.ini +++ b/documentdb_tests/pytest.ini @@ -44,7 +44,6 @@ markers = engine_xfail(engine, reason, raises): expected failure for a specific engine engine_xcrash(engine, reason): test crashes the server on a specific engine no_parallel: Tests that must run sequentially (not in parallel) - skip_localhost: Tests skipped when connected to a localhost server # The requires(...) marker is registered programmatically from # documentdb_tests/framework/preconditions.py — its single source of truth.