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/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 + ) 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..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 +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 90bf88938..ba8d11d41 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -339,8 +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``). + """ # Deselect a capability-gated test when its target's capabilities do not # match its requires(...) marker. Each item is parametrized over a target; diff --git a/documentdb_tests/framework/preconditions.py b/documentdb_tests/framework/preconditions.py index a42e71716..18c33ebef 100644 --- a/documentdb_tests/framework/preconditions.py +++ b/documentdb_tests/framework/preconditions.py @@ -58,6 +58,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", "validate_repair": ( "validate with repair/fixMultikey is permitted and background validation " "is rejected (standalone-only behavior)" @@ -78,6 +79,7 @@ "quorum_write_concern", "oplog", "replication", + "remote_target", } ), ("mongodb", "standalone"): frozenset( @@ -85,6 +87,7 @@ "unforced_compact", "reindex", "local_rename", + "remote_target", "validate_repair", } ), @@ -100,6 +103,7 @@ "unforced_compact", "reindex", "replication", + "remote_target", "validate_repair", } ), @@ -156,6 +160,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 ( @@ -183,6 +197,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: @@ -194,7 +209,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]: