From de76c378141ea2668064977c0e2159e44e7d3d31 Mon Sep 17 00:00:00 2001 From: Samar Farooqi Date: Thu, 15 Oct 2026 23:33:20 +0000 Subject: [PATCH] Report setup and intermediate command failures through the assertion Several compatibility tests crash with a Python TypeError/AttributeError/ KeyError before reaching framework/assertions.py when a setup or intermediate execute_command result is an engine exception (or a successful response missing a field): they index into the exception, iterate it, or build an expectation from a missing field. Apply the PR #718 pattern across these tests: extract cursor ids and response fields defensively, and forward a failed setup/intermediate result to the test's single assertion so the engine's real error or value is visible through a framework assertion helper instead of a raw crash. Where a first response is missing a field, read it with .get(...) so the assertion reports the mismatch. MongoDB behavior and the checked property are unchanged. Covers listIndexes cursor batching, bulkWrite getMore drain, changeStream events/event-read-error/resume, setQuerySettings response/remove/update and hash tests, getParameter *, allParameters+named and setAt variants, dbStats scale/avgObjSize, getCmdLineOpts, serverStatus, and hello. Signed-off-by: Samar Farooqi --- .../test_listIndexes_cursor_batching.py | 88 +++++++++---- .../test_changeStream_event_read_error.py | 7 +- .../changeStream/test_changeStream_events.py | 14 ++- .../changeStream/test_changeStream_resume.py | 74 ++++++++--- .../test_bulkWrite_response_structure.py | 10 +- .../test_setQuerySettings_behavior.py | 22 +++- .../test_setQuerySettings_settings.py | 9 +- .../test_setQuerySettings_verification.py | 24 ++-- .../test_getParameter_core_behavior.py | 119 +++++++++++------- .../commands/dbStats/test_dbStats_accuracy.py | 15 ++- .../test_dbStats_response_structure.py | 9 +- .../test_getCmdLineOpts_consistency.py | 10 +- .../test_serverStatus_core_behavior.py | 10 +- .../commands/hello/test_hello_consistency.py | 19 +-- .../hello/test_hello_response_structure.py | 3 +- 15 files changed, 298 insertions(+), 135 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/indexes/commands/listIndexes/test_listIndexes_cursor_batching.py b/documentdb_tests/compatibility/tests/core/indexes/commands/listIndexes/test_listIndexes_cursor_batching.py index edd8f402a..e5909708e 100644 --- a/documentdb_tests/compatibility/tests/core/indexes/commands/listIndexes/test_listIndexes_cursor_batching.py +++ b/documentdb_tests/compatibility/tests/core/indexes/commands/listIndexes/test_listIndexes_cursor_batching.py @@ -96,15 +96,21 @@ def test_listIndexes_getMore_retrieves_remaining(collection_with_3_indexes): collection_with_3_indexes, {"listIndexes": collection_with_3_indexes.name, "cursor": {"batchSize": 1}}, ) - cursor_id = result["cursor"]["id"] - - get_more_result = execute_command( - collection_with_3_indexes, - {"getMore": cursor_id, "collection": collection_with_3_indexes.name}, - ) + if isinstance(result, Exception): + outcome = result + else: + get_more_result = execute_command( + collection_with_3_indexes, + {"getMore": result["cursor"]["id"], "collection": collection_with_3_indexes.name}, + ) + outcome = ( + get_more_result + if isinstance(get_more_result, Exception) + else {"cursor": {"firstBatch": get_more_result["cursor"]["nextBatch"]}, "ok": 1.0} + ) assertSuccess( - {"cursor": {"firstBatch": get_more_result["cursor"]["nextBatch"]}, "ok": 1.0}, + outcome, [{"v": 2, "key": {"a": 1}, "name": "a_1"}, {"v": 2, "key": {"b": 1}, "name": "b_1"}], ) @@ -141,19 +147,33 @@ def test_listIndexes_batchSize_1_iterate_all(collection_with_3_indexes): collection_with_3_indexes, {"listIndexes": collection_with_3_indexes.name, "cursor": {"batchSize": 1}}, ) - all_indexes = list(result["cursor"]["firstBatch"]) - cursor_id = result["cursor"]["id"] - - while cursor_id != 0: - get_more_result = execute_command( - collection_with_3_indexes, - {"getMore": cursor_id, "collection": collection_with_3_indexes.name, "batchSize": 1}, + if isinstance(result, Exception): + outcome = result + else: + all_indexes = list(result["cursor"]["firstBatch"]) + cursor_id = result["cursor"]["id"] + + failure = None + while cursor_id != 0: + get_more_result = execute_command( + collection_with_3_indexes, + { + "getMore": cursor_id, + "collection": collection_with_3_indexes.name, + "batchSize": 1, + }, + ) + if isinstance(get_more_result, Exception): + failure = get_more_result + break + all_indexes.extend(get_more_result["cursor"]["nextBatch"]) + cursor_id = get_more_result["cursor"]["id"] + outcome = ( + failure if failure is not None else {"cursor": {"firstBatch": all_indexes}, "ok": 1.0} ) - all_indexes.extend(get_more_result["cursor"]["nextBatch"]) - cursor_id = get_more_result["cursor"]["id"] assertSuccess( - {"cursor": {"firstBatch": all_indexes}, "ok": 1.0}, + outcome, [ {"v": 2, "key": {"_id": 1}, "name": "_id_"}, {"v": 2, "key": {"a": 1}, "name": "a_1"}, @@ -180,22 +200,36 @@ def test_listIndexes_batchSize_10_with_64_indexes(collection_with_64_indexes): collection_with_64_indexes, {"listIndexes": collection_with_64_indexes.name, "cursor": {"batchSize": 10}}, ) - all_indexes = list(result["cursor"]["firstBatch"]) - cursor_id = result["cursor"]["id"] - - while cursor_id != 0: - get_more_result = execute_command( - collection_with_64_indexes, - {"getMore": cursor_id, "collection": collection_with_64_indexes.name, "batchSize": 10}, + if isinstance(result, Exception): + outcome = result + else: + all_indexes = list(result["cursor"]["firstBatch"]) + cursor_id = result["cursor"]["id"] + + failure = None + while cursor_id != 0: + get_more_result = execute_command( + collection_with_64_indexes, + { + "getMore": cursor_id, + "collection": collection_with_64_indexes.name, + "batchSize": 10, + }, + ) + if isinstance(get_more_result, Exception): + failure = get_more_result + break + all_indexes.extend(get_more_result["cursor"]["nextBatch"]) + cursor_id = get_more_result["cursor"]["id"] + outcome = ( + failure if failure is not None else {"cursor": {"firstBatch": all_indexes}, "ok": 1.0} ) - all_indexes.extend(get_more_result["cursor"]["nextBatch"]) - cursor_id = get_more_result["cursor"]["id"] expected = [{"v": 2, "key": {"_id": 1}, "name": "_id_"}] for i in range(63): expected.append({"v": 2, "key": {f"f{i}": 1}, "name": f"f{i}_1"}) assertSuccess( - {"cursor": {"firstBatch": all_indexes}, "ok": 1.0}, + outcome, expected, msg="Total indexes across all batches should equal 64", ) diff --git a/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_event_read_error.py b/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_event_read_error.py index 870cd7014..abbd97184 100644 --- a/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_event_read_error.py +++ b/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_event_read_error.py @@ -44,6 +44,9 @@ def test_changeStream_required_image_event_read_error(collection, test_case): opened = execute_command( collection, change_stream_command(collection, pipeline=test_case.pipeline) ) - collection.update_one({"_id": 1}, {"$set": {"a": 2}}) - result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) + if isinstance(opened, Exception): + result = opened + else: + collection.update_one({"_id": 1}, {"$set": {"a": 2}}) + result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) assertResult(result, error_code=test_case.error_code, msg=test_case.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_events.py b/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_events.py index bdb9eba66..c9ee830e4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_events.py +++ b/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_events.py @@ -425,11 +425,12 @@ def __post_init__(self): ) -def _emit_event(collection, test_case: ChangeStreamEventTestCase) -> dict[str, Any]: +def _emit_event(collection, test_case: ChangeStreamEventTestCase) -> dict[str, Any] | Exception: """Open a stream, perform the test case mutation, and return the matching event. The mutation happens after the stream is open, so its event is delivered by - the first getMore. + the first getMore. If opening the stream or reading the batch fails, the + engine's exception is returned so the test's single assertion surfaces it. """ mutation = test_case.mutation assert mutation is not None # guaranteed by __post_init__ @@ -443,12 +444,15 @@ def _emit_event(collection, test_case: ChangeStreamEventTestCase) -> dict[str, A opened = execute_command( collection, change_stream_command(collection, pipeline=test_case.pipeline) ) + if isinstance(opened, Exception): + return opened mutation.apply(collection) if test_case.post_mutation is not None: test_case.post_mutation(collection) - batch = execute_command(collection, get_more_command(collection, opened["cursor"]["id"]))[ - "cursor" - ]["nextBatch"] + batch_result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) + if isinstance(batch_result, Exception): + return batch_result + batch = batch_result["cursor"]["nextBatch"] matching = [e for e in batch if e["operationType"] == mutation.operation_type] assert matching, f"no {mutation.operation_type} event in batch: {batch!r}" event: dict[str, Any] = matching[0] diff --git a/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_resume.py b/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_resume.py index dfb4bc6f5..e6c55b388 100644 --- a/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_resume.py +++ b/documentdb_tests/compatibility/tests/core/operator/system-stages/changeStream/test_changeStream_resume.py @@ -58,6 +58,36 @@ def _event_token_of_type(collection, operation_type, seed, spec=None): return next(e["_id"] for e in batch if e["operationType"] == operation_type) +def _first_event_token_or_error(collection, seed, spec=None): + """Like ``_first_event_token`` but return the engine error instead of crashing. + + When opening or draining the token-capture stream fails, the exception is + returned so a caller can forward it to the test's single assertion rather + than indexing into it. + """ + opened = _open_stream(collection, spec) + if isinstance(opened, Exception): + return opened + seed(collection) + result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) + if isinstance(result, Exception): + return result + return result["cursor"]["nextBatch"][0]["_id"] + + +def _event_token_of_type_or_error(collection, operation_type, seed, spec=None): + """Like ``_event_token_of_type`` but return the engine error instead of crashing.""" + opened = _open_stream(collection, spec) + if isinstance(opened, Exception): + return opened + seed(collection) + result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) + if isinstance(result, Exception): + return result + batch = result["cursor"]["nextBatch"] + return next((e["_id"] for e in batch if e["operationType"] == operation_type), None) + + def _high_water_mark(collection): """Return the postBatchResumeToken of a freshly opened, idle stream.""" return _open_stream(collection)["cursor"]["postBatchResumeToken"] @@ -310,14 +340,22 @@ def seed(c): c.insert_one({"_id": 1}) c.drop() - token = _event_token_of_type(collection, "invalidate", seed) + token = _event_token_of_type_or_error(collection, "invalidate", seed) - opened = execute_command( - collection, - change_stream_command(collection, pipeline=[{"$changeStream": {"startAfter": token}}]), - ) - collection.insert_one({"_id": 2}) - result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) + if isinstance(token, Exception): + result = token + else: + opened = execute_command( + collection, + change_stream_command(collection, pipeline=[{"$changeStream": {"startAfter": token}}]), + ) + if isinstance(opened, Exception): + result = opened + else: + collection.insert_one({"_id": 2}) + result = execute_command( + collection, get_more_command(collection, opened["cursor"]["id"]) + ) assertResult( result, @@ -388,14 +426,22 @@ def test_start_after_foreign_collection_token(collection, database_client): """Test $changeStream startAfter from a foreign-collection token.""" other = database_client[f"{collection.name}_other"] database_client.create_collection(other.name) - token = _first_event_token(other, lambda c: c.insert_one({"_id": 1})) + token = _first_event_token_or_error(other, lambda c: c.insert_one({"_id": 1})) - opened = execute_command( - collection, - change_stream_command(collection, pipeline=[{"$changeStream": {"startAfter": token}}]), - ) - collection.insert_one({"_id": 99}) - result = execute_command(collection, get_more_command(collection, opened["cursor"]["id"])) + if isinstance(token, Exception): + result = token + else: + opened = execute_command( + collection, + change_stream_command(collection, pipeline=[{"$changeStream": {"startAfter": token}}]), + ) + if isinstance(opened, Exception): + result = opened + else: + collection.insert_one({"_id": 99}) + result = execute_command( + collection, get_more_command(collection, opened["cursor"]["id"]) + ) assertResult( result, diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/bulkWrite/test_bulkWrite_response_structure.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/bulkWrite/test_bulkWrite_response_structure.py index 819a91b04..980ae44d6 100644 --- a/documentdb_tests/compatibility/tests/core/query_and_write/commands/bulkWrite/test_bulkWrite_response_structure.py +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/bulkWrite/test_bulkWrite_response_structure.py @@ -185,8 +185,14 @@ def test_bulkWrite_getMore_drains_remaining_results(collection): "cursor": {"batchSize": 1}, }, ) - cursor_id = result["cursor"]["id"] - more = execute_admin_command(collection, {"getMore": cursor_id, "collection": "$cmd.bulkWrite"}) + cursor_id = None if isinstance(result, Exception) else result["cursor"]["id"] + more = ( + result + if isinstance(result, Exception) + else execute_admin_command( + collection, {"getMore": cursor_id, "collection": "$cmd.bulkWrite"} + ) + ) assertResult( more, expected={ diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index e714973c3..e32635ff4 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -133,11 +133,14 @@ def test_setQuerySettings_response(collection, test): try: result = execute_admin_command(collection, test.build_command(ctx)) expected = test.build_expected(ctx) - # Also verify the dynamic fields are present - if test.id == "response_contains_hash": - expected["queryShapeHash"] = result.get("queryShapeHash") - elif test.id == "response_contains_representative_query": - expected["representativeQuery"] = result.get("representativeQuery") + # Also verify the dynamic fields are present. When the command failed, + # pass the exception through so the single assertion reports the engine + # error instead of crashing while reading fields off the exception. + if not isinstance(result, Exception): + if test.id == "response_contains_hash": + expected["queryShapeHash"] = result.get("queryShapeHash") + elif test.id == "response_contains_representative_query": + expected["representativeQuery"] = result.get("representativeQuery") assertSuccessPartial(result, expected, msg=test.msg) finally: for cmd in test.build_cleanup(ctx): @@ -231,7 +234,14 @@ def test_setQuerySettings_remove(collection, test): for cmd in test.build_setup(ctx): r = execute_admin_command(collection, cmd) ctx.setup_results.append(r) - result = execute_admin_command(collection, test.build_command(ctx)) + # If a setup command failed, its result is needed to build the command + # (e.g. removeQuerySettings by hash); forward the failure to the single + # assertion instead of indexing into the exception. + setup_failure = next((r for r in ctx.setup_results if isinstance(r, Exception)), None) + if setup_failure is not None: + result = setup_failure + else: + result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: for cmd in test.build_cleanup(ctx): diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py index 80265cbb1..1b6b32f2e 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -775,7 +775,14 @@ def test_setQuerySettings_update(collection, test): for cmd in test.build_setup(ctx): r = execute_admin_command(collection, cmd) ctx.setup_results.append(r) - result = execute_admin_command(collection, test.build_command(ctx)) + # If a setup command failed, its result is needed to build the command + # (update via hash); forward the failure to the single assertion instead + # of indexing into the exception. + setup_failure = next((r for r in ctx.setup_results if isinstance(r, Exception)), None) + if setup_failure is not None: + result = setup_failure + else: + result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: for cmd in test.build_cleanup(ctx): diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py index fe671ed68..386faf404 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -326,7 +326,7 @@ def test_setQuerySettings_hash_same(collection, test): for cmd in test.build_setup(ctx): r = execute_admin_command(collection, cmd) ctx.setup_results.append(r) - if "queryShapeHash" in r: + if isinstance(r, dict) and "queryShapeHash" in r: setup_hash = r["queryShapeHash"] result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial( @@ -352,12 +352,15 @@ def test_setQuerySettings_hash_different(collection, test): for cmd in test.build_setup(ctx): r = execute_admin_command(collection, cmd) ctx.setup_results.append(r) - if "queryShapeHash" in r: + if isinstance(r, dict) and "queryShapeHash" in r: setup_hash = r["queryShapeHash"] result = execute_admin_command(collection, test.build_command(ctx)) - hashes_differ = result["queryShapeHash"] != setup_hash + if isinstance(result, Exception): + actual = result + else: + actual = {"differ": result["queryShapeHash"] != setup_hash} assertSuccessPartial( - {"differ": hashes_differ}, + actual, {"differ": True}, msg=test.msg, ) @@ -412,12 +415,17 @@ def test_setQuerySettings_hash_format(collection, test): ctx = CommandContext.from_collection(collection) try: result = execute_admin_command(collection, test.build_command(ctx)) - h = result.get("queryShapeHash", "") - is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) + if isinstance(result, Exception): + actual = result + msg = test.msg + else: + h = result.get("queryShapeHash", "") + actual = {"valid": bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h))} + msg = f"{test.msg}, got: {h!r}" assertSuccessPartial( - {"valid": is_valid}, + actual, {"valid": True}, - msg=f"{test.msg}, got: {h!r}", + msg=msg, ) finally: for cmd in test.build_cleanup(ctx): diff --git a/documentdb_tests/compatibility/tests/system/administration/commands/getParameter/test_getParameter_core_behavior.py b/documentdb_tests/compatibility/tests/system/administration/commands/getParameter/test_getParameter_core_behavior.py index 8cec9a924..f02416de4 100644 --- a/documentdb_tests/compatibility/tests/system/administration/commands/getParameter/test_getParameter_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/administration/commands/getParameter/test_getParameter_core_behavior.py @@ -149,12 +149,17 @@ def test_getParameter_star_and_allParameters_consistent(collection): """Test all parameters returned by '*' are also returned by {allParameters: true}.""" star_result = execute_admin_command(collection, {"getParameter": "*"}) all_result = execute_admin_command(collection, {"getParameter": {"allParameters": True}}) - star_keys = set(star_result.keys()) - {"ok"} - all_keys = set(all_result.keys()) - {"ok"} - missing = star_keys - all_keys + failure = next((r for r in (star_result, all_result) if isinstance(r, Exception)), None) + if failure is None: + missing = (set(star_result.keys()) - {"ok"}) - (set(all_result.keys()) - {"ok"}) + actual = {"ok": 1.0, "missing": []} + expected = {"ok": 1.0, "missing": list(missing)} + else: + actual = failure + expected = {"ok": 1.0, "missing": []} assertSuccess( - {"ok": 1.0, "missing": []}, - {"ok": 1.0, "missing": list(missing)}, + actual, + expected, raw_res=True, msg="Keys from '*' should all appear in allParameters", ) @@ -195,9 +200,13 @@ def test_getParameter_setAt_without_allParameters_succeeds(collection): def test_getParameter_star_response_has_multiple_params(collection): """Test '*' response contains multiple parameter fields.""" result = execute_admin_command(collection, {"getParameter": "*"}) - param_keys = set(result.keys()) - {"ok"} + param_keys = set() if isinstance(result, Exception) else set(result.keys()) - {"ok"} assertSuccess( - {"ok": 1.0, "has_logLevel": True, "has_multiple_params": True}, + ( + result + if isinstance(result, Exception) + else {"ok": 1.0, "has_logLevel": True, "has_multiple_params": True} + ), { "ok": 1.0, "has_logLevel": "logLevel" in param_keys, @@ -212,9 +221,10 @@ def test_getParameter_single_matches_star(collection): """Test parameter value from single retrieval matches value in '*' response.""" single = execute_admin_command(collection, {"getParameter": 1, "logLevel": 1}) star = execute_admin_command(collection, {"getParameter": "*"}) + failure = next((r for r in (single, star) if isinstance(r, Exception)), None) assertSuccess( - {"ok": 1.0, "logLevel": single["logLevel"]}, - {"ok": 1.0, "logLevel": star["logLevel"]}, + failure if failure is not None else {"ok": 1.0, "logLevel": single["logLevel"]}, + {"ok": 1.0, "logLevel": None if failure is not None else star["logLevel"]}, raw_res=True, msg="Single retrieval should match star value", ) @@ -267,25 +277,31 @@ def test_getParameter_fully_immutable_param(collection): def test_getParameter_setAt_filters_result_count(collection): """Test setAt actually narrows the result set.""" - all_keys = set(execute_admin_command(collection, {"getParameter": {"allParameters": True}})) - runtime_keys = set( - execute_admin_command( - collection, {"getParameter": {"allParameters": True, "setAt": "runtime"}} - ) + all_result = execute_admin_command(collection, {"getParameter": {"allParameters": True}}) + runtime_result = execute_admin_command( + collection, {"getParameter": {"allParameters": True, "setAt": "runtime"}} ) - startup_keys = set( - execute_admin_command( - collection, {"getParameter": {"allParameters": True, "setAt": "startup"}} - ) + startup_result = execute_admin_command( + collection, {"getParameter": {"allParameters": True, "setAt": "startup"}} ) - n_all = len(all_keys - {"ok"}) - assertSuccess( - {"ok": 1.0, "runtime_filtered": True, "startup_filtered": True}, - { + failure = next( + (r for r in (all_result, runtime_result, startup_result) if isinstance(r, Exception)), + None, + ) + if failure is None: + n_all = len(set(all_result) - {"ok"}) + actual = {"ok": 1.0, "runtime_filtered": True, "startup_filtered": True} + expected = { "ok": 1.0, - "runtime_filtered": len(runtime_keys - {"ok"}) < n_all, - "startup_filtered": len(startup_keys - {"ok"}) < n_all, - }, + "runtime_filtered": len(set(runtime_result) - {"ok"}) < n_all, + "startup_filtered": len(set(startup_result) - {"ok"}) < n_all, + } + else: + actual = failure + expected = {"ok": 1.0, "runtime_filtered": True, "startup_filtered": True} + assertSuccess( + actual, + expected, raw_res=True, msg="setAt:'runtime' and setAt:'startup' should each return fewer params than unfiltered", ) @@ -293,23 +309,26 @@ def test_getParameter_setAt_filters_result_count(collection): def test_getParameter_setAt_buckets_startup_only_param(collection): """Test setAt routes a startup-only param to the correct bucket.""" - runtime_keys = set( - execute_admin_command( - collection, {"getParameter": {"allParameters": True, "setAt": "runtime"}} - ) + runtime_result = execute_admin_command( + collection, {"getParameter": {"allParameters": True, "setAt": "runtime"}} ) - startup_keys = set( - execute_admin_command( - collection, {"getParameter": {"allParameters": True, "setAt": "startup"}} - ) + startup_result = execute_admin_command( + collection, {"getParameter": {"allParameters": True, "setAt": "startup"}} ) - assertSuccess( - {"ok": 1.0, "in_runtime": False, "in_startup": True}, - { + failure = next((r for r in (runtime_result, startup_result) if isinstance(r, Exception)), None) + if failure is None: + actual = {"ok": 1.0, "in_runtime": False, "in_startup": True} + expected = { "ok": 1.0, - "in_runtime": "KeysRotationIntervalSec" in runtime_keys, - "in_startup": "KeysRotationIntervalSec" in startup_keys, - }, + "in_runtime": "KeysRotationIntervalSec" in set(runtime_result), + "in_startup": "KeysRotationIntervalSec" in set(startup_result), + } + else: + actual = failure + expected = {"ok": 1.0, "in_runtime": False, "in_startup": True} + assertSuccess( + actual, + expected, raw_res=True, msg="Startup-only param should appear only under setAt:'startup'", ) @@ -321,11 +340,17 @@ def test_getParameter_allParameters_with_named_param_returns_all(collection): with_named = execute_admin_command( collection, {"getParameter": {"allParameters": True}, "logLevel": 1} ) - all_only_keys = set(all_only.keys()) - {"ok"} - with_named_keys = set(with_named.keys()) - {"ok"} + failure = next((r for r in (all_only, with_named) if isinstance(r, Exception)), None) + if failure is None: + key_diff = sorted((set(all_only.keys()) - {"ok"}) ^ (set(with_named.keys()) - {"ok"})) + actual = {"key_diff": []} + expected = {"key_diff": key_diff} + else: + actual = failure + expected = {"key_diff": []} assertSuccess( - {"key_diff": []}, - {"key_diff": sorted(all_only_keys ^ with_named_keys)}, + actual, + expected, raw_res=True, msg="allParameters with a named param should return the same keys as allParameters alone", ) @@ -336,9 +361,13 @@ def test_getParameter_allParameters_with_named_param_not_filtered(collection): result = execute_admin_command( collection, {"getParameter": {"allParameters": True}, "logLevel": 1} ) - param_keys = set(result.keys()) - {"ok"} + param_keys = set() if isinstance(result, Exception) else set(result.keys()) - {"ok"} assertSuccess( - {"has_logLevel": True, "has_other_params": True}, + ( + result + if isinstance(result, Exception) + else {"has_logLevel": True, "has_other_params": True} + ), { "has_logLevel": "logLevel" in param_keys, "has_other_params": len(param_keys) > 1, diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py index b8be07716..8c0fadc6a 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py @@ -79,15 +79,20 @@ def test_dbStats_scale_divides_data_size(collection): collection.insert_many([{"_id": i, "data": "x" * 1024} for i in range(50)]) unscaled = execute_command(collection, {"dbStats": 1, "scale": 1}) scaled = execute_command(collection, {"dbStats": 1, "scale": 1024}) - expected_scaled = unscaled.get("dataSize") / 1024 - actual_scaled = scaled.get("dataSize") + unscaled_data = unscaled.get("dataSize") if isinstance(unscaled, dict) else None + scaled_data = scaled.get("dataSize") if isinstance(scaled, dict) else None + matches = ( + isinstance(unscaled_data, (int, float)) + and isinstance(scaled_data, (int, float)) + and scaled_data == pytest.approx(unscaled_data / 1024, abs=1.0) + ) assertSuccess( - actual_scaled == pytest.approx(expected_scaled, abs=1.0), + matches, expected=True, raw_res=True, msg=( - f"scale=1024 dataSize ({actual_scaled}) should approximate " - f"unscaled dataSize / 1024 ({expected_scaled})" + f"scale=1024 dataSize ({scaled_data}) should approximate " + f"unscaled dataSize / 1024 (unscaled dataSize={unscaled_data})" ), ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py index 25272aa74..0da128313 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py @@ -119,9 +119,16 @@ def test_dbStats_avg_obj_size_equals_data_size_over_objects(collection): """Test avgObjSize equals dataSize divided by objects.""" collection.insert_many([{"_id": i, "data": "x" * (i + 1)} for i in range(10)]) result = execute_command(collection, {"dbStats": 1}) + data_size = result.get("dataSize") if isinstance(result, dict) else None + objects = result.get("objects") if isinstance(result, dict) else None + expected_avg = ( + data_size / objects + if isinstance(data_size, (int, float)) and isinstance(objects, (int, float)) and objects + else None + ) assertProperties( result, - {"avgObjSize": Eq(result.get("dataSize") / result.get("objects"))}, + {"avgObjSize": Eq(expected_avg)}, raw_res=True, msg="avgObjSize should equal dataSize / objects", ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/getCmdLineOpts/test_getCmdLineOpts_consistency.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/getCmdLineOpts/test_getCmdLineOpts_consistency.py index 7da27970c..fc60bccb4 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/getCmdLineOpts/test_getCmdLineOpts_consistency.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/getCmdLineOpts/test_getCmdLineOpts_consistency.py @@ -15,9 +15,10 @@ def test_getCmdLineOpts_parsed_stable(collection): """Test the 'parsed' content is identical across consecutive calls.""" result1 = execute_admin_command(collection, {"getCmdLineOpts": 1}) result2 = execute_admin_command(collection, {"getCmdLineOpts": 1}) + failure = next((r for r in (result1, result2) if isinstance(r, Exception)), None) assertSuccess( - result2["parsed"], - expected=result1["parsed"], + failure if failure is not None else result2["parsed"], + expected=None if failure is not None else result1["parsed"], msg="'parsed' content should be identical across calls", raw_res=True, ) @@ -27,9 +28,10 @@ def test_getCmdLineOpts_argv_stable(collection): """Test the 'argv' content is identical across consecutive calls.""" result1 = execute_admin_command(collection, {"getCmdLineOpts": 1}) result2 = execute_admin_command(collection, {"getCmdLineOpts": 1}) + failure = next((r for r in (result1, result2) if isinstance(r, Exception)), None) assertSuccess( - result2["argv"], - expected=result1["argv"], + failure if failure is not None else result2["argv"], + expected=None if failure is not None else result1["argv"], msg="'argv' content should be identical across calls", raw_res=True, ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/serverStatus/test_serverStatus_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/serverStatus/test_serverStatus_core_behavior.py index 659d79235..e6391cce5 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/serverStatus/test_serverStatus_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/serverStatus/test_serverStatus_core_behavior.py @@ -210,7 +210,7 @@ def test_serverStatus_pid_consistent_across_calls(collection): result2 = execute_admin_command(collection, {"serverStatus": 1}) assertProperties( result2, - {"pid": Eq(result1["pid"])}, + {"pid": Eq(result1.get("pid") if isinstance(result1, dict) else None)}, raw_res=True, msg="serverStatus should return the same pid across calls", ) @@ -247,10 +247,10 @@ def test_serverStatus_cross_database_same_core_fields(collection): assertProperties( db_result, { - "host": Eq(admin_result["host"]), - "version": Eq(admin_result["version"]), - "process": Eq(admin_result["process"]), - "pid": Eq(admin_result["pid"]), + "host": Eq(admin_result.get("host")), + "version": Eq(admin_result.get("version")), + "process": Eq(admin_result.get("process")), + "pid": Eq(admin_result.get("pid")), }, raw_res=True, msg="serverStatus should return the same core fields regardless of database", diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_consistency.py b/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_consistency.py index bf5d86c30..f0978e8d0 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_consistency.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_consistency.py @@ -158,7 +158,7 @@ def test_hello_connectionId_stable(collection): r2 = execute_command(collection, {"hello": 1}) assertResult( r2, - expected={"connectionId": Eq(r1["connectionId"])}, + expected={"connectionId": Eq(r1.get("connectionId") if isinstance(r1, dict) else None)}, msg="hello should return stable connectionId on same connection", raw_res=True, ) @@ -170,17 +170,18 @@ def test_hello_legacy_isMaster_fields_match(collection): """Test hello and isMaster return same values for common fields.""" r_hello = execute_command(collection, {"hello": 1}) r_ismaster = execute_command(collection, {"isMaster": 1}) + h = r_hello if isinstance(r_hello, dict) else {} assertResult( r_ismaster, expected={ - "maxBsonObjectSize": Eq(r_hello["maxBsonObjectSize"]), - "maxMessageSizeBytes": Eq(r_hello["maxMessageSizeBytes"]), - "maxWriteBatchSize": Eq(r_hello["maxWriteBatchSize"]), - "minWireVersion": Eq(r_hello["minWireVersion"]), - "maxWireVersion": Eq(r_hello["maxWireVersion"]), - "readOnly": Eq(r_hello["readOnly"]), - "connectionId": Eq(r_hello["connectionId"]), - "logicalSessionTimeoutMinutes": Eq(r_hello["logicalSessionTimeoutMinutes"]), + "maxBsonObjectSize": Eq(h.get("maxBsonObjectSize")), + "maxMessageSizeBytes": Eq(h.get("maxMessageSizeBytes")), + "maxWriteBatchSize": Eq(h.get("maxWriteBatchSize")), + "minWireVersion": Eq(h.get("minWireVersion")), + "maxWireVersion": Eq(h.get("maxWireVersion")), + "readOnly": Eq(h.get("readOnly")), + "connectionId": Eq(h.get("connectionId")), + "logicalSessionTimeoutMinutes": Eq(h.get("logicalSessionTimeoutMinutes")), }, msg="hello should return matching fields with isMaster alias", raw_res=True, diff --git a/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_response_structure.py b/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_response_structure.py index a3d4eec53..674eb9ad5 100644 --- a/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_response_structure.py +++ b/documentdb_tests/compatibility/tests/system/replication/commands/hello/test_hello_response_structure.py @@ -157,9 +157,10 @@ def test_hello_response_topologyVersion_processId_stable(collection): """Test hello topologyVersion.processId is stable across calls.""" r1 = execute_command(collection, {"hello": 1}) r2 = execute_command(collection, {"hello": 1}) + tv = r1.get("topologyVersion", {}) if isinstance(r1, dict) else {} assertResult( r2, - expected={"topologyVersion": {"processId": Eq(r1["topologyVersion"]["processId"])}}, + expected={"topologyVersion": {"processId": Eq(tv.get("processId"))}}, msg="hello should return stable topologyVersion.processId across calls", raw_res=True, )