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 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 ad11014b2..d15690406 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -343,8 +343,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 561b9dec5..f0cf8b408 100644 --- a/documentdb_tests/framework/preconditions.py +++ b/documentdb_tests/framework/preconditions.py @@ -59,6 +59,7 @@ "local_rename": "renaming into the unreplicated local database is permitted", "search": "search and vector search surfaces are available", "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)" @@ -84,6 +85,7 @@ "search", "oplog", "replication", + "remote_target", } ), ("mongodb", "standalone"): frozenset( @@ -91,6 +93,7 @@ "unforced_compact", "reindex", "local_rename", + "remote_target", "validate_repair", } ), @@ -106,6 +109,7 @@ "unforced_compact", "reindex", "replication", + "remote_target", "validate_repair", } ), @@ -162,6 +166,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 ( @@ -189,6 +203,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: @@ -200,7 +215,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]: