From f8f0b687be8cfdc8d6dfcc8513d102cbf5deb1a0 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Mon, 22 Jun 2026 08:59:58 -0700 Subject: [PATCH] Add fsync command tests Signed-off-by: Daniel Frankcom --- .../administration/commands/fsync/conftest.py | 11 + .../fsync/test_fsync_command_envelope.py | 164 +++++++++++++ .../fsync/test_fsync_execution_context.py | 67 ++++++ .../commands/fsync/test_fsync_lock.py | 227 ++++++++++++++++++ .../test_fsync_lock_timeout_type_errors.py | 113 +++++++++ .../fsync/test_fsync_no_lock_flush.py | 158 ++++++++++++ .../commands/fsync/test_fsync_read_concern.py | 184 ++++++++++++++ .../fsync/test_fsync_write_concern.py | 116 +++++++++ .../system/administration/utils/__init__.py | 0 .../system/administration/utils/fsync_lock.py | 40 +++ .../framework/test_structure_validator.py | 2 +- 11 files changed, 1081 insertions(+), 1 deletion(-) create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/conftest.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_command_envelope.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_execution_context.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock_timeout_type_errors.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_no_lock_flush.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_read_concern.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_write_concern.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/administration/utils/fsync_lock.py diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/conftest.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/conftest.py new file mode 100644 index 000000000..370722fbf --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/conftest.py @@ -0,0 +1,11 @@ +"""Shared fixtures for fsync tests. + +Re-exports the autouse fsync-lock baseline fixture so it applies to every test +in this directory without each test file importing it. +""" + +from documentdb_tests.compatibility.tests.system.administration.utils.fsync_lock import ( + unlocked_baseline, +) + +__all__ = ["unlocked_baseline"] diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_command_envelope.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_command_envelope.py new file mode 100644 index 000000000..b68f59349 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_command_envelope.py @@ -0,0 +1,164 @@ +"""Tests for fsync command-envelope field handling. + +Covers comment acceptance, generic envelope options, unknown-field rejection, +and apiStrict rejection. +""" + +from __future__ import annotations + +from datetime import ( + datetime, + timezone, +) + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import CommandTestCase +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + API_STRICT_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import ( + Eq, + NotExists, +) +from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF + +pytestmark = pytest.mark.no_parallel + + +# Property [Comment Acceptance]: comment accepts any BSON value and is not echoed +# in the response. +FSYNC_COMMENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"comment_{tid}", + command={"fsync": 1, "comment": val}, + expected={ + "ok": Eq(1.0), + "numFiles": Eq(1), + "lockCount": NotExists(), + "comment": NotExists(), + }, + msg=f"fsync should accept a {tid} comment value and not echo it in the response", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(7)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "audit note"), + ("object", {"a": 1}), + ("array", [1, 2, 3]), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +# Property [Generic Command Options Accepted]: generic command-envelope options +# ($readPreference, maxTimeMS, apiVersion) are accepted and ignored, leaving the +# no-lock flush response unchanged. Their semantics are owned elsewhere; this +# only confirms fsync accepts the syntax. +FSYNC_GENERIC_OPTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "generic_read_preference", + command={"fsync": 1, "$readPreference": {"mode": "secondary"}}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept a $readPreference option and perform a no-lock flush", + ), + CommandTestCase( + "generic_max_time_ms", + command={"fsync": 1, "maxTimeMS": 5_000}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept an int maxTimeMS option and perform a no-lock flush", + ), + CommandTestCase( + "generic_max_time_ms_float", + command={"fsync": 1, "maxTimeMS": 5_000.0}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept a float maxTimeMS option and perform a no-lock flush", + ), + CommandTestCase( + "generic_api_version", + command={"fsync": 1, "apiVersion": "1"}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept apiVersion 1 alone and perform a no-lock flush", + ), +] + +# Property [Unknown Field and Field-Name Case Sensitivity]: an unrecognized +# top-level field, including case variants of known field names, produces an +# unrecognized-field error. +FSYNC_UNKNOWN_FIELD_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "unknown_field", + command={"fsync": 1, "bogus": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="fsync should reject an unknown top-level command field with an " + "unrecognized-field error", + ), + CommandTestCase( + "case_variant_lock", + command={"fsync": 1, "Lock": True}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="fsync should reject a case variant of a known command field name " + "with an unrecognized-field error", + ), + CommandTestCase( + "timeout_missing_millis_suffix", + command={"fsync": 1, "fsyncLockAcquisitionTimeout": 90_000}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="fsync should reject fsyncLockAcquisitionTimeout, which is missing the " + "Millis suffix, as an unknown field", + ), +] + +# Property [Unsupported Stable API]: apiStrict under API Version 1 is rejected +# rather than silently ignored. +FSYNC_API_STRICT_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "envelope_api_strict", + command={"fsync": 1, "apiVersion": "1", "apiStrict": True}, + error_code=API_STRICT_ERROR, + msg="fsync should reject apiStrict under API Version 1 with an APIStrictError", + ), +] + +FSYNC_COMMAND_ENVELOPE_TESTS: list[CommandTestCase] = ( + FSYNC_COMMENT_TESTS + + FSYNC_GENERIC_OPTION_TESTS + + FSYNC_UNKNOWN_FIELD_ERROR_TESTS + + FSYNC_API_STRICT_ERROR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FSYNC_COMMAND_ENVELOPE_TESTS)) +def test_fsync_command_envelope_cases(collection, test): + """Test fsync command-envelope option acceptance and rejection cases.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_execution_context.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_execution_context.py new file mode 100644 index 000000000..887a08278 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_execution_context.py @@ -0,0 +1,67 @@ +"""Tests for fsync invocation context: explicit session, authorization scope, and transactions.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.assertions import ( + assertFailureCode, + assertSuccessPartial, +) +from documentdb_tests.framework.error_codes import ( + OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + UNAUTHORIZED_ERROR, +) +from documentdb_tests.framework.executor import ( + execute_admin_command, + execute_command, +) + +pytestmark = pytest.mark.no_parallel + + +# Property [Explicit Session Accepted]: a no-lock flush issued under an explicit +# client session behaves identically to a sessionless invocation. +def test_fsync_accepts_explicit_session(collection): + """Test fsync runs a no-lock flush under an explicit client session.""" + session = collection.database.client.start_session() + try: + result = execute_admin_command(collection, {"fsync": 1}, session=session) + assertSuccessPartial( + result, + {"ok": 1.0, "numFiles": 1}, + msg="fsync should run a no-lock flush under an explicit session", + ) + finally: + session.end_session() + + +# Property [Authorization Scope]: fsync is admin-only, so running it against a +# non-admin database produces an Unauthorized error. +def test_fsync_rejects_non_admin_database(collection): + """Test fsync rejects execution against a non-admin database.""" + result = execute_command(collection, {"fsync": 1}) + assertFailureCode( + result, + UNAUTHORIZED_ERROR, + msg="fsync should reject a flush against a non-admin database", + ) + + +# Property [Multi-Document Transaction]: fsync is not supported inside a +# multi-document transaction and errors with OperationNotSupportedInTransaction. +@pytest.mark.requires(transactions=True) +def test_fsync_rejects_multi_document_transaction(collection): + """Test fsync errors when issued inside a multi-document transaction.""" + client = collection.database.client + with client.start_session() as session: + session.start_transaction() + try: + result = execute_admin_command(collection, {"fsync": 1}, session=session) + finally: + session.abort_transaction() + assertFailureCode( + result, + OPERATION_NOT_SUPPORTED_IN_TRANSACTION_ERROR, + msg="fsync should error when issued inside a multi-document transaction", + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock.py new file mode 100644 index 000000000..5aa114828 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock.py @@ -0,0 +1,227 @@ +"""Tests for fsync lock acquisition, coercion, nesting, and persistence.""" + +from __future__ import annotations + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import CommandTestCase +from documentdb_tests.framework import fixtures +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import ( + Eq, + Exists, + IsType, + NotExists, +) +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_ZERO, + DOUBLE_HALF, + DOUBLE_NEGATIVE_ONE_AND_HALF, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_ZERO, + INT64_ZERO, +) + +pytestmark = pytest.mark.no_parallel + + +# Property [Lock Coercion - Falsy]: boolean false and any zero-magnitude numeric +# (including negative zero) coerce to false, acquiring no lock. +FSYNC_LOCK_FALSY_TESTS: list[CommandTestCase] = [ + *[ + CommandTestCase( + f"lock_falsy_{tid}", + command={"fsync": 1, "lock": val}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg=f"fsync should treat a {tid} lock value as falsy and acquire no lock", + ) + for tid, val in [ + ("int_zero", INT32_ZERO), + ("int64_zero", INT64_ZERO), + ("double_zero", DOUBLE_ZERO), + ("double_negative_zero", DOUBLE_NEGATIVE_ZERO), + ("decimal_zero", DECIMAL128_ZERO), + ("decimal_negative_zero", DECIMAL128_NEGATIVE_ZERO), + ] + ], + CommandTestCase( + "lock_falsy_bool_false", + command={"fsync": 1, "lock": False}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should treat a boolean false lock value as falsy and acquire no lock", + ), + CommandTestCase( + "lock_falsy_with_timeout", + command={"fsync": 1, "lock": False, "fsyncLockAcquisitionTimeoutMillis": 5_000}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when lock is false even with a " + "timeout present", + ), +] + +# Property [Lock Acquisition and Response Shape]: a truthy lock acquires an +# fsync lock and returns info, an Int64 lockCount, and seeAlso with no numFiles; +# options supplied alongside lock do not alter the response. +FSYNC_LOCK_ACQUISITION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "lock_true", + command={"fsync": 1, "lock": True}, + expected={ + "info": Eq("now locked against writes, use db.fsyncUnlock() to unlock"), + "lockCount": [IsType("long"), Eq(Int64(1))], + "seeAlso": [Exists(), IsType("string")], + "ok": Eq(1.0), + "numFiles": NotExists(), + }, + msg="fsync should acquire a lock and return the lock response shape", + ), + CommandTestCase( + "lock_true_with_options", + command={ + "fsync": 1, + "lock": True, + "comment": "lock with options", + "fsyncLockAcquisitionTimeoutMillis": 90_000, + }, + expected={ + "info": [Exists(), IsType("string")], + "lockCount": [IsType("long"), Eq(Int64(1))], + "seeAlso": [Exists(), IsType("string")], + "ok": Eq(1.0), + "numFiles": NotExists(), + "comment": NotExists(), + }, + msg="fsync should acquire a lock when comment and timeout are also present", + ), +] + +# Property [Lock Coercion - Truthy]: any non-zero-magnitude numeric (including +# NaN and signed infinity) coerces to true and acquires a lock. +FSYNC_LOCK_TRUTHY_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"lock_truthy_{tid}", + command={"fsync": 1, "lock": val}, + expected={ + "ok": Eq(1.0), + "lockCount": [IsType("long"), Eq(Int64(1))], + "numFiles": NotExists(), + }, + msg=f"fsync should treat a {tid} lock value as truthy and acquire a lock", + ) + for tid, val in [ + ("int_positive", 1), + ("int64_positive", Int64(2)), + ("double_positive", DOUBLE_HALF), + ("double_negative", DOUBLE_NEGATIVE_ONE_AND_HALF), + ("decimal_positive", DECIMAL128_ONE_AND_HALF), + ("decimal_negative", DECIMAL128_NEGATIVE_ONE_AND_HALF), + ("double_nan", FLOAT_NAN), + ("decimal_nan", DECIMAL128_NAN), + ("double_infinity", FLOAT_INFINITY), + ("double_negative_infinity", FLOAT_NEGATIVE_INFINITY), + ("decimal_infinity", DECIMAL128_INFINITY), + ("decimal_negative_infinity", DECIMAL128_NEGATIVE_INFINITY), + ] +] + +# Property [Timeout Value Accepted With Lock]: any int32 +# fsyncLockAcquisitionTimeoutMillis, including zero, negatives, and the int32 +# extremes, is accepted with no value-range validation. +FSYNC_TIMEOUT_WITH_LOCK_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"timeout_with_lock_{tid}", + command={ + "fsync": 1, + "lock": True, + "fsyncLockAcquisitionTimeoutMillis": val, + }, + expected={ + "ok": Eq(1.0), + "lockCount": [IsType("long"), Eq(Int64(1))], + "numFiles": NotExists(), + }, + msg=f"fsync should accept a {tid} timeout value and still acquire the lock", + ) + for tid, val in [ + ("zero", INT32_ZERO), + ("one", 1), + ("negative_one", -1), + ("int32_max", INT32_MAX), + ("int32_min", INT32_MIN), + ] +] + +FSYNC_LOCK_TESTS: list[CommandTestCase] = ( + FSYNC_LOCK_FALSY_TESTS + + FSYNC_LOCK_ACQUISITION_TESTS + + FSYNC_LOCK_TRUTHY_TESTS + + FSYNC_TIMEOUT_WITH_LOCK_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FSYNC_LOCK_TESTS)) +def test_fsync_lock_cases(collection, test): + """Test fsync lock coercion, acquisition, and response-shape cases.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) + + +# Property [Lock Counting and Nesting]: a lock:true issued while the lock is +# already held nests rather than conflicting, returning a lockCount one higher +# than the existing depth. +def test_fsync_lock_nesting_increments_count(collection): + """Test fsync lock:true returns an incremented lockCount when already locked.""" + # Precondition: hold the lock twice so the command under test is the third. + execute_admin_command(collection, {"fsync": 1, "lock": True}) + execute_admin_command(collection, {"fsync": 1, "lock": True}) + result = execute_admin_command(collection, {"fsync": 1, "lock": True}) + assertResult( + result, + expected={"ok": Eq(1.0), "lockCount": [IsType("long"), Eq(Int64(3))]}, + msg="fsync should nest a lock while already held, returning lockCount 3", + raw_res=True, + ) + + +# Property [Lock Persistence Across Connection Close]: the server-global lock +# persists when the acquiring connection closes without unlocking, so a fresh +# lock:true returns lockCount 2 rather than 1. +def test_fsync_lock_persists_after_connection_close(connection_string, collection): + """Test fsync lock persists after the acquiring connection closes.""" + # Precondition: acquire the lock on a separate connection, then close that + # connection without unlocking. + second_client = fixtures.create_engine_client(connection_string, "second") + second_coll = second_client[collection.database.name][collection.name] + execute_admin_command(second_coll, {"fsync": 1, "lock": True}) + second_client.close() + # If the lock had been released on disconnect this would return lockCount 1; + # lockCount 2 proves the lock survived the close. + result = execute_admin_command(collection, {"fsync": 1, "lock": True}) + assertResult( + result, + expected={"ok": Eq(1.0), "lockCount": [IsType("long"), Eq(Int64(2))]}, + msg="fsync should still see the lock held after the acquiring connection " + "closes, returning lockCount 2", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock_timeout_type_errors.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock_timeout_type_errors.py new file mode 100644 index 000000000..9dbb63da2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_lock_timeout_type_errors.py @@ -0,0 +1,113 @@ +"""Tests for fsync lock and timeout field type strictness.""" + +from __future__ import annotations + +from datetime import ( + datetime, + timezone, +) + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import CommandTestCase +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF + +pytestmark = pytest.mark.no_parallel + + +# Property [lock Type Strictness]: a non-numeric lock value produces a +# TypeMismatch error, and a literal array is not unwrapped. +FSYNC_LOCK_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + *[ + CommandTestCase( + f"lock_type_{tid}", + command={"fsync": 1, "lock": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"fsync should reject a {tid} lock value with a TypeMismatch error", + ) + for tid, val in [ + ("string", "x"), + ("object", {"a": 1}), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] + ], + CommandTestCase( + "lock_array_true", + command={"fsync": 1, "lock": [True]}, + error_code=TYPE_MISMATCH_ERROR, + msg="fsync should reject a single-element truthy array lock value without unwrapping it", + ), + CommandTestCase( + "lock_array_false", + command={"fsync": 1, "lock": [False]}, + error_code=TYPE_MISMATCH_ERROR, + msg="fsync should reject a single-element falsy array lock value without unwrapping it", + ), +] + +# Property [fsyncLockAcquisitionTimeoutMillis Type Strictness]: a non-int32 +# timeout produces a TypeMismatch error with no coercion, and a literal array is +# not unwrapped. +FSYNC_TIMEOUT_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"timeout_type_{tid}", + command={"fsync": 1, "fsyncLockAcquisitionTimeoutMillis": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"fsync should reject a {tid} timeout value with a TypeMismatch error", + ) + for tid, val in [ + ("int64", Int64(100)), + ("double_whole", 90_000.0), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "90000"), + ("object", {"a": 1}), + ("array", [42]), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +FSYNC_LOCK_TIMEOUT_TYPE_ERROR_TESTS: list[CommandTestCase] = ( + FSYNC_LOCK_TYPE_ERROR_TESTS + FSYNC_TIMEOUT_TYPE_ERROR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FSYNC_LOCK_TIMEOUT_TYPE_ERROR_TESTS)) +def test_fsync_lock_timeout_type_error_cases(collection, test): + """Test fsync lock and timeout type-strictness error cases.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_no_lock_flush.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_no_lock_flush.py new file mode 100644 index 000000000..0b8d98e61 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_no_lock_flush.py @@ -0,0 +1,158 @@ +"""Tests for fsync no-lock flush behavior and response shape.""" + +from __future__ import annotations + +from datetime import ( + datetime, + timezone, +) + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import CommandTestCase +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import ( + Eq, + IsType, + NotExists, +) +from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF + +pytestmark = pytest.mark.no_parallel + + +# Property [Null and Missing Behavior]: a null or missing value in any field is +# treated as absent, performing a no-lock flush. +FSYNC_NULL_MISSING_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "fsync_key_null", + command={"fsync": None}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when the command-key value is null", + ), + CommandTestCase( + "lock_null", + command={"fsync": 1, "lock": None}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when lock is null", + ), + CommandTestCase( + "timeout_null", + command={"fsync": 1, "fsyncLockAcquisitionTimeoutMillis": None}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when " + "fsyncLockAcquisitionTimeoutMillis is null", + ), + CommandTestCase( + "comment_null", + command={"fsync": 1, "comment": None}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when comment is null", + ), + CommandTestCase( + "all_fields_null", + command={ + "fsync": None, + "lock": None, + "fsyncLockAcquisitionTimeoutMillis": None, + "comment": None, + }, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when every field is null", + ), + CommandTestCase( + "optional_fields_missing", + command={"fsync": 1}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should perform a no-lock flush when all optional fields are omitted", + ), +] + +# Property [Flush Response Shape]: a no-lock flush returns an integer numFiles +# and none of the lock-only fields. +FSYNC_FLUSH_SHAPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "flush_shape", + command={"fsync": 1}, + expected={ + "ok": Eq(1.0), + "numFiles": IsType("int"), + "lockCount": NotExists(), + "info": NotExists(), + "seeAlso": NotExists(), + }, + msg="fsync should return an integer numFiles and no lock-only fields on a no-lock flush", + ), +] + +# Property [Command-Key Value Ignored]: the fsync command-key value is never +# type-validated. +FSYNC_KEY_IGNORED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"key_{tid}", + command={"fsync": val}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg=f"fsync should ignore a {tid} command-key value and perform a no-lock flush", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(7)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "flush"), + ("object", {"a": 1}), + ("array", [1, 2, 3]), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +# Property [Timeout Value Without Lock]: fsyncLockAcquisitionTimeoutMillis is +# accepted and has no effect when no lock is requested. +FSYNC_TIMEOUT_NO_LOCK_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "timeout_no_lock", + command={"fsync": 1, "fsyncLockAcquisitionTimeoutMillis": 5_000}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept a timeout value without lock and perform a no-lock flush", + ), +] + +FSYNC_NO_LOCK_FLUSH_TESTS: list[CommandTestCase] = ( + FSYNC_NULL_MISSING_TESTS + + FSYNC_FLUSH_SHAPE_TESTS + + FSYNC_KEY_IGNORED_TESTS + + FSYNC_TIMEOUT_NO_LOCK_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FSYNC_NO_LOCK_FLUSH_TESTS)) +def test_fsync_no_lock_flush_cases(collection, test): + """Test fsync no-lock flush success and response-shape cases.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_read_concern.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_read_concern.py new file mode 100644 index 000000000..c92ea5562 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_read_concern.py @@ -0,0 +1,184 @@ +"""Tests for fsync readConcern acceptance and rejection behavior.""" + +from __future__ import annotations + +from datetime import ( + datetime, + timezone, +) + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import CommandTestCase +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + INVALID_OPTIONS_ERROR, + TYPE_MISMATCH_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import ( + Eq, + NotExists, +) +from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF + +pytestmark = pytest.mark.no_parallel + + +# Property [readConcern Level Acceptance]: fsync supports the local read concern +# level; an empty document, a null readConcern, and a null level are all treated +# as absent. +FSYNC_READ_CONCERN_ACCEPTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_level_local", + command={"fsync": 1, "readConcern": {"level": "local"}}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept readConcern level local and perform a no-lock flush", + ), + CommandTestCase( + "read_concern_empty", + command={"fsync": 1, "readConcern": {}}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should accept an empty readConcern document and perform a no-lock flush", + ), + CommandTestCase( + "read_concern_null", + command={"fsync": 1, "readConcern": None}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should treat a null readConcern as absent and perform a no-lock flush", + ), + CommandTestCase( + "read_concern_level_null", + command={"fsync": 1, "readConcern": {"level": None}}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should treat a null readConcern level as absent and perform a no-lock flush", + ), +] + +# Property [readConcern Level Not Supported]: every recognized read concern +# level other than local is rejected with an InvalidOptions error. +FSYNC_READ_CONCERN_LEVEL_REJECTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_level_{level}", + command={"fsync": 1, "readConcern": {"level": level}}, + error_code=INVALID_OPTIONS_ERROR, + msg=f"fsync should reject readConcern level {level} with an InvalidOptions error", + ) + for level in ["majority", "available", "linearizable", "snapshot"] +] + +# Property [readConcern Invalid Level Value]: a readConcern level string that is +# not a recognized enum value is rejected with a BadValue error. +FSYNC_READ_CONCERN_INVALID_LEVEL_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_level_unknown", + command={"fsync": 1, "readConcern": {"level": "bogus"}}, + error_code=BAD_VALUE_ERROR, + msg="fsync should reject an unrecognized readConcern level value with a BadValue error", + ), +] + +# Property [readConcern Level Type Strictness]: a readConcern level whose BSON +# type is not a string produces a TypeMismatch error. +FSYNC_READ_CONCERN_LEVEL_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_level_type_{tid}", + command={"fsync": 1, "readConcern": {"level": val}}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"fsync should reject a {tid} readConcern level with a TypeMismatch error", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(7)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("object", {"a": 1}), + ("array", ["local"]), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +# Property [readConcern Type Strictness]: a non-document readConcern produces a +# TypeMismatch error (null is treated as absent, covered by the acceptance +# property). +FSYNC_READ_CONCERN_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"read_concern_type_{tid}", + command={"fsync": 1, "readConcern": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"fsync should reject a {tid} readConcern value as a non-document " + "with a TypeMismatch error", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(7)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "local"), + ("array", [{"level": "local"}]), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +# Property [readConcern Unknown Sub-Field]: an unrecognized field inside the +# readConcern document produces an unrecognized-field error. +FSYNC_READ_CONCERN_UNKNOWN_SUBFIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "read_concern_unknown_subfield", + command={"fsync": 1, "readConcern": {"level": "local", "bogus": 1}}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="fsync should reject an unknown sub-field inside readConcern with an " + "unrecognized-field error", + ), +] + +FSYNC_READ_CONCERN_TESTS: list[CommandTestCase] = ( + FSYNC_READ_CONCERN_ACCEPTED_TESTS + + FSYNC_READ_CONCERN_LEVEL_REJECTED_TESTS + + FSYNC_READ_CONCERN_INVALID_LEVEL_TESTS + + FSYNC_READ_CONCERN_LEVEL_TYPE_ERROR_TESTS + + FSYNC_READ_CONCERN_TYPE_ERROR_TESTS + + FSYNC_READ_CONCERN_UNKNOWN_SUBFIELD_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FSYNC_READ_CONCERN_TESTS)) +def test_fsync_read_concern_cases(collection, test): + """Test fsync readConcern acceptance and rejection cases.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_write_concern.py b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_write_concern.py new file mode 100644 index 000000000..875fbb3ce --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/commands/fsync/test_fsync_write_concern.py @@ -0,0 +1,116 @@ +"""Tests for fsync writeConcern rejection and type strictness.""" + +from __future__ import annotations + +from datetime import ( + datetime, + timezone, +) + +import pytest +from bson import ( + Binary, + Code, + Int64, + MaxKey, + MinKey, + ObjectId, + Regex, + Timestamp, +) + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import CommandTestCase +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + INVALID_OPTIONS_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import ( + Eq, + NotExists, +) +from documentdb_tests.framework.test_constants import DECIMAL128_ONE_AND_HALF + +pytestmark = pytest.mark.no_parallel + + +# Property [writeConcern Not Supported]: fsync does not support writeConcern, so +# a writeConcern document is rejected with an InvalidOptions error regardless of +# its sub-fields (a null writeConcern is treated as absent, covered separately). +FSYNC_WRITE_CONCERN_REJECTED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"write_concern_{tid}", + command={"fsync": 1, "writeConcern": val}, + error_code=INVALID_OPTIONS_ERROR, + msg=f"fsync should reject a writeConcern document ({tid}) with an InvalidOptions error", + ) + for tid, val in [ + ("empty", {}), + ("w_one", {"w": 1}), + ("w_zero", {"w": 0}), + ("w_majority", {"w": "majority"}), + ("j_true", {"j": True}), + ("wtimeout", {"w": 1, "wtimeout": 1_000}), + ] +] + +# Property [writeConcern Type Strictness]: a non-document writeConcern produces a +# TypeMismatch error. +FSYNC_WRITE_CONCERN_TYPE_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"write_concern_type_{tid}", + command={"fsync": 1, "writeConcern": val}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"fsync should reject a {tid} writeConcern value as a non-document " + "with a TypeMismatch error", + ) + for tid, val in [ + ("int32", 42), + ("int64", Int64(7)), + ("double", 3.14), + ("decimal128", DECIMAL128_ONE_AND_HALF), + ("bool", True), + ("string", "majority"), + ("array", [{"w": 1}]), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(1, 1)), + ("binary", Binary(b"\x01\x02\x03")), + ("regex", Regex(".*", "i")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +# Property [writeConcern Null Treated As Absent]: a null writeConcern is ignored +# rather than triggering the unsupported-option rejection. +FSYNC_WRITE_CONCERN_NULL_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "write_concern_null", + command={"fsync": 1, "writeConcern": None}, + expected={"ok": Eq(1.0), "numFiles": Eq(1), "lockCount": NotExists()}, + msg="fsync should treat a null writeConcern as absent and perform a no-lock flush", + ), +] + +FSYNC_WRITE_CONCERN_TESTS: list[CommandTestCase] = ( + FSYNC_WRITE_CONCERN_REJECTED_TESTS + + FSYNC_WRITE_CONCERN_TYPE_ERROR_TESTS + + FSYNC_WRITE_CONCERN_NULL_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(FSYNC_WRITE_CONCERN_TESTS)) +def test_fsync_write_concern_cases(collection, test): + """Test fsync writeConcern rejection, type-strictness, and null-as-absent cases.""" + result = execute_admin_command(collection, test.command) + assertResult( + result, + expected=test.expected, + error_code=test.error_code, + msg=test.msg, + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/__init__.py b/documentdb_tests/compatibility/tests/system/administration/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/administration/utils/fsync_lock.py b/documentdb_tests/compatibility/tests/system/administration/utils/fsync_lock.py new file mode 100644 index 000000000..219c3ed22 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/administration/utils/fsync_lock.py @@ -0,0 +1,40 @@ +"""Shared fsync-lock state management for fsync and fsyncUnlock tests. + +The fsync lock is a server-global count that neither command's collection +fixture manages, so both test directories need a way to return the server to a +known unlocked baseline. The drain helper and the autouse baseline fixture here +are the single source of truth; each command directory's conftest re-exports the +fixture so it applies to that directory's tests. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.framework.error_codes import ILLEGAL_OPERATION_ERROR +from documentdb_tests.framework.executor import execute_admin_command + + +def drain_fsync_lock(collection) -> None: + """Release outstanding fsync locks until the server-global count reaches 0. + + Loops fsyncUnlock until the reported count is 0, treating the not-locked + error as "already 0" and re-raising any other error so an unexpected failure + cannot silently leave the server locked. + """ + while True: + result = execute_admin_command(collection, {"fsyncUnlock": 1}) + if isinstance(result, Exception): + if getattr(result, "code", None) == ILLEGAL_OPERATION_ERROR: + return + raise result + if result.get("lockCount", 0) == 0: + return + + +@pytest.fixture(autouse=True) +def unlocked_baseline(collection): + """Drain the server-global fsync lock before and after every test.""" + drain_fsync_lock(collection) + yield + drain_fsync_lock(collection) diff --git a/documentdb_tests/framework/test_structure_validator.py b/documentdb_tests/framework/test_structure_validator.py index 57f811f08..dd40a3f75 100644 --- a/documentdb_tests/framework/test_structure_validator.py +++ b/documentdb_tests/framework/test_structure_validator.py @@ -15,7 +15,7 @@ def validate_python_files_in_tests(tests_dir: Path) -> list[str]: allowed_folders = {"utils", "fixtures", "__pycache__"} for py_file in tests_dir.rglob("*.py"): - if py_file.name == "__init__.py": + if py_file.name in ("__init__.py", "conftest.py"): continue if any(folder in py_file.parts for folder in allowed_folders): continue