diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/__init__.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_argument_handling.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_argument_handling.py new file mode 100644 index 000000000..e40863497 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_argument_handling.py @@ -0,0 +1,157 @@ +"""Tests for top command argument handling. + +Validates that top accepts any BSON type as its argument value and +accepts unrecognized fields. +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +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 + +pytestmark = pytest.mark.admin + +# Property [BSON Type Acceptance]: top accepts any non-deprecated BSON type as command value. +ARGUMENT_TYPE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "int_1", command={"top": 1}, checks={"ok": Eq(1.0)}, msg="Should accept int 1" + ), + DiagnosticTestCase( + "int_0", command={"top": 0}, checks={"ok": Eq(1.0)}, msg="Should accept int 0" + ), + DiagnosticTestCase( + "int_neg1", command={"top": -1}, checks={"ok": Eq(1.0)}, msg="Should accept int -1" + ), + DiagnosticTestCase( + "bool_true", command={"top": True}, checks={"ok": Eq(1.0)}, msg="Should accept true" + ), + DiagnosticTestCase( + "bool_false", + command={"top": False}, + checks={"ok": Eq(1.0)}, + msg="Should accept false", + ), + DiagnosticTestCase( + "string", command={"top": "hello"}, checks={"ok": Eq(1.0)}, msg="Should accept string" + ), + DiagnosticTestCase( + "null", command={"top": None}, checks={"ok": Eq(1.0)}, msg="Should accept null" + ), + DiagnosticTestCase( + "empty_object", + command={"top": {}}, + checks={"ok": Eq(1.0)}, + msg="Should accept empty object", + ), + DiagnosticTestCase( + "empty_array", + command={"top": []}, + checks={"ok": Eq(1.0)}, + msg="Should accept empty array", + ), + DiagnosticTestCase( + "double", command={"top": 1.5}, checks={"ok": Eq(1.0)}, msg="Should accept double" + ), + DiagnosticTestCase( + "int64", command={"top": Int64(1)}, checks={"ok": Eq(1.0)}, msg="Should accept int64" + ), + DiagnosticTestCase( + "decimal128", + command={"top": Decimal128("1")}, + checks={"ok": Eq(1.0)}, + msg="Should accept decimal128", + ), + DiagnosticTestCase( + "decimal128_nan", + command={"top": Decimal128("NaN")}, + checks={"ok": Eq(1.0)}, + msg="Should accept decimal128 NaN", + ), + DiagnosticTestCase( + "infinity", + command={"top": float("inf")}, + checks={"ok": Eq(1.0)}, + msg="Should accept infinity", + ), + DiagnosticTestCase( + "date", + command={"top": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + checks={"ok": Eq(1.0)}, + msg="Should accept date", + ), + DiagnosticTestCase( + "binData", + command={"top": Binary(b"")}, + checks={"ok": Eq(1.0)}, + msg="Should accept binData", + ), + DiagnosticTestCase( + "objectId", + command={"top": ObjectId()}, + checks={"ok": Eq(1.0)}, + msg="Should accept objectId", + ), + DiagnosticTestCase( + "regex", + command={"top": Regex("test")}, + checks={"ok": Eq(1.0)}, + msg="Should accept regex", + ), + DiagnosticTestCase( + "timestamp", + command={"top": Timestamp(0, 0)}, + checks={"ok": Eq(1.0)}, + msg="Should accept timestamp", + ), + DiagnosticTestCase( + "minKey", + command={"top": MinKey()}, + checks={"ok": Eq(1.0)}, + msg="Should accept minKey", + ), + DiagnosticTestCase( + "maxKey", + command={"top": MaxKey()}, + checks={"ok": Eq(1.0)}, + msg="Should accept maxKey", + ), + DiagnosticTestCase( + "code", + command={"top": Code("function(){}")}, + checks={"ok": Eq(1.0)}, + msg="Should accept JavaScript code", + ), +] + +# Property [Unrecognized Fields]: top accepts and ignores unrecognized fields. +UNRECOGNIZED_FIELD_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "single_unrecognized_field", + command={"top": 1, "unknownField": 1}, + checks={"ok": Eq(1.0)}, + msg="top should accept a single unrecognized field", + ), + DiagnosticTestCase( + "multiple_unrecognized_fields", + command={"top": 1, "foo": 1, "bar": "baz", "qux": []}, + checks={"ok": Eq(1.0)}, + msg="top should accept multiple unrecognized fields", + ), +] + +ARGUMENT_HANDLING_TESTS = ARGUMENT_TYPE_TESTS + UNRECOGNIZED_FIELD_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ARGUMENT_HANDLING_TESTS)) +def test_top_argument_handling(collection, test): + """Test that top accepts various BSON types and unrecognized fields.""" + result = execute_admin_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_consistency.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_consistency.py new file mode 100644 index 000000000..f8c037cfa --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_consistency.py @@ -0,0 +1,156 @@ +"""Tests for top command consistency, visibility, and special collection types. + +Validates idempotency, namespace visibility, system namespace structure, +and behavior with capped collections and views. + +Standalone functions are used throughout because every test requires runtime +logic that DiagnosticTestCase cannot express: multi-call comparisons with +dynamic thresholds, namespace key extraction from the response, conditional +skips, or ad-hoc collection creation (capped, views). +""" + +import pytest + +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.property_checks import Exists, Gte, IsType + +pytestmark = pytest.mark.admin + + +# Property [Idempotency]: repeated top calls succeed and counters are non-decreasing. + + +def test_top_repeated_calls_return_ok(collection): + """Test that calling top returns ok after multiple calls.""" + for _ in range(5): + execute_admin_command(collection, {"top": 1}) + result = execute_admin_command(collection, {"top": 1}) + assertSuccessPartial(result, {"ok": 1.0}, msg="top should succeed after repeated calls") + + +def test_top_counters_non_decreasing_count(collection): + """Test that total.count is non-decreasing across two consecutive calls.""" + collection.insert_one({"_id": 1}) + ns = f"{collection.database.name}.{collection.name}" + result1 = execute_admin_command(collection, {"top": 1}) + count1 = result1["totals"][ns]["total"]["count"] + result2 = execute_admin_command(collection, {"top": 1}) + ns_data2 = result2["totals"][ns] + assertProperties( + ns_data2, + {"total.count": Gte(count1)}, + msg="total.count should be non-decreasing", + raw_res=True, + ) + + +def test_top_counters_non_decreasing_time(collection): + """Test that total.time is non-decreasing across two consecutive calls.""" + collection.insert_one({"_id": 1}) + ns = f"{collection.database.name}.{collection.name}" + result1 = execute_admin_command(collection, {"top": 1}) + time1 = result1["totals"][ns]["total"]["time"] + result2 = execute_admin_command(collection, {"top": 1}) + ns_data2 = result2["totals"][ns] + assertProperties( + ns_data2, + {"total.time": Gte(time1)}, + msg="total.time should be non-decreasing", + raw_res=True, + ) + + +# Property [Collection Visibility]: active collections appear in totals as db.collection keys. + + +def test_top_newly_created_collection_appears(collection): + """Test that a newly created collection appears in top totals.""" + collection.insert_one({"_id": 1}) + result = execute_admin_command(collection, {"top": 1}) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"].get(ns) + assertProperties( + {"ns_entry": ns_data}, + {"ns_entry": Exists()}, + msg=f"Namespace {ns} should appear in top totals", + raw_res=True, + ) + + +def test_top_multiple_collections_appear(collection): + """Test that multiple collections appear in top totals.""" + db = collection.database + coll1 = db.create_collection(f"{collection.name}_multi1") + coll2 = db.create_collection(f"{collection.name}_multi2") + coll1.insert_one({"_id": 1}) + coll2.insert_one({"_id": 1}) + result = execute_admin_command(coll1, {"top": 1}) + ns1 = f"{db.name}.{coll1.name}" + ns2 = f"{db.name}.{coll2.name}" + assertProperties( + {"ns1": result["totals"].get(ns1), "ns2": result["totals"].get(ns2)}, + {"ns1": Exists(), "ns2": Exists()}, + msg="Both namespaces should appear in top totals", + raw_res=True, + ) + + +# Property [System Collections]: system namespaces have the standard event field structure. + + +def test_top_system_collections_have_event_structure(collection): + """Test that a system namespace in totals has the expected event field structure.""" + collection.insert_one({"_id": 1}) + result = execute_admin_command(collection, {"top": 1}) + system_ns = None + for ns_key in result["totals"]: + if ".system." in ns_key or ns_key.startswith("admin.") or ns_key.startswith("local."): + system_ns = ns_key + break + if system_ns is None: + pytest.skip("No system namespace found in top totals") + ns_data = result["totals"][system_ns] + assertProperties( + ns_data, + {"total": IsType("object"), "total.time": Gte(0), "total.count": Gte(0)}, + msg=f"System namespace {system_ns} should have event fields with time/count", + raw_res=True, + ) + + +# Property [Special Collection Types]: capped collections and views are handled by top. + + +def test_top_tracks_capped_collection(collection): + """Test that a capped collection appears in top totals with expected structure.""" + db = collection.database + coll = db.create_collection(f"{collection.name}_capped", capped=True, size=4096) + coll.insert_one({"_id": 1}) + result = execute_admin_command(coll, {"top": 1}) + ns = f"{db.name}.{coll.name}" + ns_data = result["totals"][ns] + assertProperties( + ns_data, + {"total": IsType("object"), "total.time": Gte(0), "total.count": Gte(0)}, + msg="Capped collection should appear in top totals with expected structure", + raw_res=True, + ) + + +def test_top_tracks_view(collection): + """Test that a view namespace appears in top totals.""" + db = collection.database + source_coll = db.create_collection(f"{collection.name}_view_src") + source_coll.insert_one({"_id": 1}) + view_name = f"{collection.name}_view" + db.command("create", view_name, viewOn=source_coll.name, pipeline=[]) + result = execute_admin_command(source_coll, {"top": 1}) + view_ns = f"{db.name}.{view_name}" + view_data = result["totals"].get(view_ns) + assertProperties( + {"ns_entry": view_data}, + {"ns_entry": Exists()}, + msg=f"View namespace {view_ns} should appear in top totals", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_core_behavior.py new file mode 100644 index 000000000..ad3b7e01d --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_core_behavior.py @@ -0,0 +1,200 @@ +"""Tests for top command core behavior. + +Validates that counters reflect operations and cross-lock consistency invariants hold. +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Gt, Gte + +pytestmark = pytest.mark.admin + + +# Property [Counter Behavior - Insert]: insert operations populate insert, writeLock, and time. +INSERT_COUNTER_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "insert_count_gte_1", + command={"top": 1}, + checks={"insert.count": Gte(1)}, + msg="insert.count should be >= 1 after inserts", + ), + DiagnosticTestCase( + "insert_writeLock_count_gte_1", + command={"top": 1}, + checks={"writeLock.count": Gte(1)}, + msg="writeLock.count should be >= 1 after inserts", + ), + DiagnosticTestCase( + "insert_time_gt_0", + command={"top": 1}, + checks={"insert.time": Gt(0)}, + msg="insert.time should be > 0 after inserts", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INSERT_COUNTER_TESTS)) +def test_top_after_inserts(collection, test): + """Test counters after insert operations.""" + collection.insert_many([{"_id": i} for i in range(10)]) + result = execute_admin_command(collection, test.command) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties(ns_data, test.checks, msg=test.msg, raw_res=True) + + +# Property [Counter Behavior - Query]: find operations populate queries, readLock, and time. +QUERY_COUNTER_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "queries_count_gte_1", + command={"top": 1}, + checks={"queries.count": Gte(1)}, + msg="queries.count should be >= 1 after query", + ), + DiagnosticTestCase( + "query_readLock_count_gte_1", + command={"top": 1}, + checks={"readLock.count": Gte(1)}, + msg="readLock.count should be >= 1 after query", + ), + DiagnosticTestCase( + "queries_time_gt_0", + command={"top": 1}, + checks={"queries.time": Gt(0)}, + msg="queries.time should be > 0 after query", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(QUERY_COUNTER_TESTS)) +def test_top_after_query(collection, test): + """Test counters after find operations.""" + collection.insert_one({"_id": 1}) + list(collection.find()) + result = execute_admin_command(collection, test.command) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties(ns_data, test.checks, msg=test.msg, raw_res=True) + + +# Property [Counter Behavior - Update]: update operations populate the update counter. +UPDATE_COUNTER_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "update_count_gte_1", + command={"top": 1}, + checks={"update.count": Gte(1)}, + msg="update.count should be >= 1 after update", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(UPDATE_COUNTER_TESTS)) +def test_top_after_update(collection, test): + """Test counters after update operations.""" + collection.insert_one({"_id": 1, "a": 1}) + collection.update_one({"_id": 1}, {"$set": {"a": 2}}) + result = execute_admin_command(collection, test.command) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties(ns_data, test.checks, msg=test.msg, raw_res=True) + + +# Property [Counter Behavior - Remove]: delete operations populate the remove counter. +REMOVE_COUNTER_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "remove_count_gte_1", + command={"top": 1}, + checks={"remove.count": Gte(1)}, + msg="remove.count should be >= 1 after delete", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(REMOVE_COUNTER_TESTS)) +def test_top_after_remove(collection, test): + """Test counters after delete operations.""" + collection.insert_one({"_id": 1}) + collection.delete_one({"_id": 1}) + result = execute_admin_command(collection, test.command) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties(ns_data, test.checks, msg=test.msg, raw_res=True) + + +# Property [Cross-Lock Invariants]: aggregate lock counters are >= the sum of their components. + + +def test_top_readLock_count_gte_queries_count(collection): + """Test that readLock.count >= queries.count.""" + collection.insert_many([{"_id": i, "a": i} for i in range(5)]) + list(collection.find()) + collection.update_one({"_id": 0}, {"$set": {"a": 99}}) + collection.delete_one({"_id": 4}) + result = execute_admin_command(collection, {"top": 1}) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties( + ns_data, + {"readLock.count": Gte(ns_data["queries"]["count"])}, + msg="readLock.count should be >= queries.count", + raw_res=True, + ) + + +def test_top_readLock_time_gte_queries_time(collection): + """Test that readLock.time >= queries.time.""" + collection.insert_many([{"_id": i, "a": i} for i in range(5)]) + list(collection.find()) + collection.update_one({"_id": 0}, {"$set": {"a": 99}}) + collection.delete_one({"_id": 4}) + result = execute_admin_command(collection, {"top": 1}) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties( + ns_data, + {"readLock.time": Gte(ns_data["queries"]["time"])}, + msg="readLock.time should be >= queries.time", + raw_res=True, + ) + + +def test_top_writeLock_count_gte_insert_update_remove(collection): + """Test that writeLock.count >= insert.count + update.count + remove.count.""" + collection.insert_many([{"_id": i, "a": i} for i in range(5)]) + list(collection.find()) + collection.update_one({"_id": 0}, {"$set": {"a": 99}}) + collection.delete_one({"_id": 4}) + result = execute_admin_command(collection, {"top": 1}) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + write_sum = ns_data["insert"]["count"] + ns_data["update"]["count"] + ns_data["remove"]["count"] + assertProperties( + ns_data, + {"writeLock.count": Gte(write_sum)}, + msg="writeLock.count should be >= insert+update+remove count", + raw_res=True, + ) + + +def test_top_writeLock_time_gte_insert_update_remove(collection): + """Test that writeLock.time >= insert.time + update.time + remove.time.""" + collection.insert_many([{"_id": i, "a": i} for i in range(5)]) + list(collection.find()) + collection.update_one({"_id": 0}, {"$set": {"a": 99}}) + collection.delete_one({"_id": 4}) + result = execute_admin_command(collection, {"top": 1}) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + write_sum = ns_data["insert"]["time"] + ns_data["update"]["time"] + ns_data["remove"]["time"] + assertProperties( + ns_data, + {"writeLock.time": Gte(write_sum)}, + msg="writeLock.time should be >= insert+update+remove time", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_errors.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_errors.py new file mode 100644 index 000000000..f7a2e50ba --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_errors.py @@ -0,0 +1,64 @@ +"""Tests for top command error conditions. + +Validates that invalid usages of top produce appropriate errors. +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import COMMAND_NOT_FOUND_ERROR, UNAUTHORIZED_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.admin + +# Property [Case Sensitivity]: command names are case-sensitive. +CASE_SENSITIVITY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="case_sensitive_Top", + command={"Top": 1}, + use_admin=True, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="'Top' (capitalized) should not be recognized", + ), + DiagnosticTestCase( + id="case_sensitive_TOP", + command={"TOP": 1}, + use_admin=True, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="'TOP' (all caps) should not be recognized", + ), + DiagnosticTestCase( + id="case_sensitive_tOP", + command={"tOP": 1}, + use_admin=True, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="'tOP' (mixed case) should not be recognized", + ), +] + +# Property [Admin Database Required]: top fails on non-admin database. +ADMIN_DB_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="non_admin_db", + command={"top": 1}, + use_admin=False, + error_code=UNAUTHORIZED_ERROR, + msg="top should fail on non-admin db", + ), +] + +ERROR_TESTS = CASE_SENSITIVITY_TESTS + ADMIN_DB_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ERROR_TESTS)) +def test_top_error_conditions(collection, test): + """Test that invalid top command usages produce appropriate errors.""" + if test.use_admin: + result = execute_admin_command(collection, test.command) + else: + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_response_structure.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_response_structure.py new file mode 100644 index 000000000..c6e1c8baa --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/top/test_top_response_structure.py @@ -0,0 +1,114 @@ +"""Tests for top command response structure. + +Validates top-level response fields and per-collection event field structure. +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +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, Gte, IsType + +pytestmark = pytest.mark.admin + + +# Property [Top-Level Fields]: top response contains totals, totals.note, and ok fields. +TOP_LEVEL_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="response_has_totals", + checks={"totals": IsType("object")}, + msg="'totals' field should be an object", + ), + DiagnosticTestCase( + id="response_has_note", + checks={"totals.note": Eq("all times in microseconds")}, + msg="'totals.note' should describe time units", + ), + DiagnosticTestCase( + id="response_has_ok", + checks={"ok": Eq(1.0)}, + msg="'ok' field should be 1.0", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TOP_LEVEL_TESTS)) +def test_top_response_top_level(collection, test): + """Test that top response contains expected top-level fields.""" + result = execute_admin_command(collection, {"top": 1}) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + + +# Property [Per-Collection Event Fields]: each namespace entry has 9 event fields with time/count. +EVENT_EXISTS_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id=f"event_{name}_exists", + checks={name: IsType("object")}, + msg=f"'{name}' event field should be an object", + ) + for name in [ + "total", + "readLock", + "writeLock", + "queries", + "getmore", + "insert", + "update", + "remove", + "commands", + ] +] + +EVENT_TIME_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id=f"event_{name}_time_gte_0", + checks={f"{name}.time": Gte(0)}, + msg=f"'{name}.time' should be >= 0", + ) + for name in [ + "total", + "readLock", + "writeLock", + "queries", + "getmore", + "insert", + "update", + "remove", + "commands", + ] +] + +EVENT_COUNT_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id=f"event_{name}_count_gte_0", + checks={f"{name}.count": Gte(0)}, + msg=f"'{name}.count' should be >= 0", + ) + for name in [ + "total", + "readLock", + "writeLock", + "queries", + "getmore", + "insert", + "update", + "remove", + "commands", + ] +] + +EVENT_FIELD_TESTS = EVENT_EXISTS_TESTS + EVENT_TIME_TESTS + EVENT_COUNT_TESTS + + +@pytest.mark.parametrize("test", pytest_params(EVENT_FIELD_TESTS)) +def test_top_event_field_structure(collection, test): + """Test that per-collection event fields have expected structure.""" + collection.insert_one({"_id": "event_structure_probe"}) + result = execute_admin_command(collection, {"top": 1}) + ns = f"{collection.database.name}.{collection.name}" + ns_data = result["totals"][ns] + assertProperties(ns_data, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/utils/diagnostic_test_case.py b/documentdb_tests/compatibility/tests/system/diagnostic/utils/diagnostic_test_case.py index 39adb13d7..3d08d60c0 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/utils/diagnostic_test_case.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/utils/diagnostic_test_case.py @@ -1,7 +1,7 @@ """Shared test case for diagnostic command tests.""" from dataclasses import dataclass, field -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from documentdb_tests.framework.test_case import BaseTestCase @@ -11,11 +11,13 @@ class DiagnosticTestCase(BaseTestCase): """Test case for diagnostic command tests. Attributes: + setup: Commands to run before the test command to establish state. command: The command document to execute. use_admin: If True, execute against the admin database. checks: Mapping of dotted field paths to property check objects. """ + setup: List[Dict[str, Any]] = field(default_factory=list) command: Optional[Dict[str, Any]] = None use_admin: bool = True checks: Dict[str, Any] = field(default_factory=dict)