From ffcf26f763d656eae09055857444ad632d4dfaf2 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 17 Jun 2026 13:48:43 -0700 Subject: [PATCH 1/5] Test Cases, post parametrization and edits to use more frameworks (bson type validation and target collections primarily) Signed-off-by: PatersonProjects --- .../diagnostic/commands/dbStats/__init__.py | 0 .../commands/dbStats/test_dbStats_accuracy.py | 94 ++++++++ .../dbStats/test_dbStats_argument_handling.py | 77 +++++++ .../test_dbStats_collection_scenarios.py | 147 +++++++++++++ .../dbStats/test_dbStats_core_behavior.py | 79 +++++++ .../dbStats/test_dbStats_free_storage.py | 84 +++++++ .../test_dbStats_response_structure.py | 182 ++++++++++++++++ .../dbStats/test_dbStats_scale_parameter.py | 206 ++++++++++++++++++ 8 files changed, 869 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/__init__.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..b0565a7c9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py @@ -0,0 +1,94 @@ +"""Tests for dbStats accuracy, state changes, and field relationships. + +Covers that counts reflect inserted documents, created collections, and +created indexes; that storageSize does not shrink while dataSize shrinks +after deletes; that objects equals the sum across collections; and the +fsTotalSize >= fsUsedSize relationship. +""" + +import pytest +from bson import Int64 + +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.property_checks import Gt, Gte +from documentdb_tests.framework.target_collection import ExtraCollections + +pytestmark = pytest.mark.admin + + +def test_dbStats_collections_count_reflects_created_collections(collection): + """Test collections reflects the number of created collections.""" + ExtraCollections(count=3).resolve(collection.database, collection) + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"collections": Int64(3)}, + msg="collections should equal the number of created collections", + ) + + +def test_dbStats_objects_count_equals_sum_across_collections(collection): + """Test objects equals the sum of document counts across collections.""" + collection.insert_many([{"_id": i} for i in range(4)]) + c2 = collection.database[f"{collection.name}_c2"] + c2.insert_many([{"_id": i} for i in range(6)]) + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"objects": Int64(10)}, + msg="objects should equal the total documents across all collections", + ) + + +def test_dbStats_indexes_count_reflects_created_indexes(collection): + """Test indexes reflects the default _id index plus created indexes.""" + collection.insert_many([{"_id": i, "a": i, "b": i} for i in range(5)]) + collection.create_index("a") + collection.create_index("b") + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"indexes": Int64(3)}, + msg="indexes should count the default _id index plus created indexes", + ) + + +def test_dbStats_storage_size_does_not_decrease_after_delete(collection): + """Test storageSize does not decrease after documents are removed.""" + collection.insert_many([{"_id": i, "data": "x" * 100} for i in range(100)]) + before = execute_command(collection, {"dbStats": 1}) + collection.delete_many({}) + after = execute_command(collection, {"dbStats": 1}) + assertProperties( + after, + {"storageSize": Gte(before["storageSize"])}, + raw_res=True, + msg="storageSize should not decrease after deletes", + ) + + +def test_dbStats_data_size_decreases_after_delete(collection): + """Test dataSize decreases after documents are removed.""" + collection.insert_many([{"_id": i, "data": "x" * 100} for i in range(100)]) + before = execute_command(collection, {"dbStats": 1}) + collection.delete_many({}) + after = execute_command(collection, {"dbStats": 1}) + assertProperties( + before, + {"dataSize": Gt(after["dataSize"])}, + raw_res=True, + msg="dataSize should decrease after deletes", + ) + + +def test_dbStats_fs_total_size_gte_used_size(collection): + """Test fsTotalSize is greater than or equal to fsUsedSize.""" + collection.insert_one({"_id": 1}) + result = execute_command(collection, {"dbStats": 1}) + assertProperties( + result, + {"fsTotalSize": Gte(result["fsUsedSize"])}, + raw_res=True, + msg="fsTotalSize should be >= fsUsedSize", + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py new file mode 100644 index 000000000..8e2ae9fa8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py @@ -0,0 +1,77 @@ +"""Tests for dbStats command argument handling. + +The value of the ``dbStats`` field is ignored by the server: any value +selects the current database, so every BSON type should be accepted, +including numeric edge cases such as 0, -1, and Infinity. +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.bson_type_validator import ( + BsonTypeTestCase, + generate_bson_acceptance_test_cases, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.test_constants import FLOAT_INFINITY, BsonType + +pytestmark = pytest.mark.admin + + +# dbStats ignores the command field value — all BSON types should succeed. +DBSTATS_VALUE_PARAMS: list[BsonTypeTestCase] = [ + BsonTypeTestCase( + id="dbStats_value", + msg="dbStats should accept all BSON types for the command field value", + keyword="dbStats", + valid_types=list(BsonType), + ), +] + +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(DBSTATS_VALUE_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_dbStats_accepts_any_value_type(collection, bson_type, sample_value, spec): + """Test dbStats accepts all BSON types for the command field value.""" + result = execute_command(collection, {"dbStats": sample_value}) + assertSuccessPartial( + result, + {"ok": 1.0, "db": collection.database.name}, + msg=f"dbStats should accept {bson_type.value} for the command field value", + ) + + +# Specific numeric edge-case values for the command field. +EDGE_CASE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="value_zero", + command={"dbStats": 0}, + checks={"ok": Eq(1.0)}, + msg="dbStats:0 should succeed", + ), + DiagnosticTestCase( + id="value_negative_one", + command={"dbStats": -1}, + checks={"ok": Eq(1.0)}, + msg="dbStats:-1 should succeed", + ), + DiagnosticTestCase( + id="value_infinity", + command={"dbStats": FLOAT_INFINITY}, + checks={"ok": Eq(1.0)}, + msg="dbStats:Infinity should succeed", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EDGE_CASE_TESTS)) +def test_dbStats_accepts_value_edge_cases(collection, test): + """Test dbStats succeeds for specific numeric edge-case command values.""" + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py new file mode 100644 index 000000000..60f92b64f --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py @@ -0,0 +1,147 @@ +"""Tests for dbStats across collection variants and data scenarios. + +Covers empty collections (with and without a secondary index), avgObjSize +when there are no objects, positive storage/index sizes when data and +indexes exist, total index counts across multiple collections, capped +collections, and object counts across a range of collection sizes and +document shapes. +""" + +from dataclasses import dataclass, field + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Gt +from documentdb_tests.framework.target_collection import CappedCollection +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = pytest.mark.admin + + +def test_dbStats_empty_collection_index_count(collection): + """Test an empty collection with a secondary index reports two indexes.""" + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"indexes": Int64(2)}, + msg="Empty collection with one secondary index should report indexes:2", + ) + + +def test_dbStats_empty_collection_sizes_positive(collection): + """Test an empty collection with an index reports positive storage and index sizes.""" + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1}) + assertProperties( + result, + {"storageSize": Gt(0.0), "indexSize": Gt(0.0)}, + raw_res=True, + msg="Empty collection should still allocate storage and index space", + ) + + +def test_dbStats_avg_obj_size_zero_when_no_objects(collection): + """Test an empty collection reports zero objects and zero avgObjSize.""" + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"objects": Int64(0), "avgObjSize": 0.0}, + msg="Empty collection should report objects:0 and avgObjSize:0", + ) + + +def test_dbStats_index_size_positive_with_indexes(collection): + """Test indexSize is positive when secondary indexes exist.""" + collection.insert_many([{"_id": i, "a": i} for i in range(10)]) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1}) + assertProperties( + result, + {"indexSize": Gt(0.0)}, + raw_res=True, + msg="indexSize should be positive with indexes", + ) + + +def test_dbStats_total_index_count_across_collections(collection): + """Test indexes counts the default and secondary indexes across all collections.""" + collection.insert_many([{"_id": i, "a": i} for i in range(5)]) + collection.create_index("a") + c2 = collection.database[f"{collection.name}_c2"] + c2.insert_many([{"_id": i, "b": i} for i in range(5)]) + c2.create_index("b") + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"indexes": Int64(4)}, + msg="indexes should total default plus secondary indexes across collections", + ) + + +def test_dbStats_capped_collection_counted(collection): + """Test dbStats counts a capped collection and its documents.""" + capped = CappedCollection(size=4096).resolve(collection.database, collection) + capped.insert_many([{"_id": i} for i in range(3)]) + result = execute_command(capped, {"dbStats": 1}) + assertSuccessPartial( + result, + {"collections": Int64(1), "objects": Int64(3)}, + msg="Capped collection should be counted with its documents", + ) + + +@dataclass(frozen=True) +class ScenarioTestCase(BaseTestCase): + """A dbStats data scenario: documents to insert plus response checks.""" + + docs: list = field(default_factory=list) + checks: dict = field(default_factory=dict) + + +# Document scenarios covering varied collection sizes and shapes +# (stats_unified scenarios #32-#39). +SCENARIO_TESTS: list[ScenarioTestCase] = [ + ScenarioTestCase( + id="small", + docs=[{"_id": i, "a": i} for i in range(5)], + checks={"objects": Eq(Int64(5)), "storageSize": Gt(0.0)}, + msg="small collection should report objects:5 and positive storageSize", + ), + ScenarioTestCase( + id="medium_mixed_types", + docs=[{"_id": i, "a": Int64(i), "b": float(i), "c": Decimal128(str(i))} for i in range(50)], + checks={"objects": Eq(Int64(50)), "storageSize": Gt(0.0)}, + msg="mixed-type collection should report objects:50 and positive storageSize", + ), + ScenarioTestCase( + id="large", + docs=[{"_id": i, "a": i} for i in range(500)], + checks={"objects": Eq(Int64(500)), "storageSize": Gt(0.0)}, + msg="large collection should report objects:500 and positive storageSize", + ), + ScenarioTestCase( + id="toast_small", + docs=[{"_id": i, "blob": "x" * 4096} for i in range(5)], + checks={"objects": Eq(Int64(5)), "storageSize": Gt(0.0)}, + msg="toast small collection should report objects:5 and positive storageSize", + ), + ScenarioTestCase( + id="toast_large", + docs=[{"_id": i, "blob": "x" * 4096} for i in range(50)], + checks={"objects": Eq(Int64(50)), "storageSize": Gt(0.0)}, + msg="toast large collection should report objects:50 and positive storageSize", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SCENARIO_TESTS)) +def test_dbStats_object_count_and_storage_across_scenarios(collection, test): + """Test objects and storageSize for varied collection sizes and document shapes.""" + collection.insert_many(test.docs) + result = execute_command(collection, {"dbStats": 1}) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py new file mode 100644 index 000000000..a3cc9016d --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py @@ -0,0 +1,79 @@ +"""Tests for dbStats command core behavior. + +Covers success on populated and empty databases, the all-zero response for +a non-existent database, execution against the admin database, and rejection +of unrecognized command fields. +""" + +import pytest +from bson import Int64 + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial +from documentdb_tests.framework.error_codes import UNRECOGNIZED_COMMAND_FIELD_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.target_collection import TargetDatabase + +pytestmark = pytest.mark.admin + + +def test_dbStats_populated_database_returns_ok(collection): + """Test dbStats returns ok:1 on a database that has collections.""" + collection.insert_many([{"_id": 0, "a": 1}, {"_id": 1, "a": 2}]) + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"ok": 1.0, "db": collection.database.name}, + msg="Populated database should return ok:1", + ) + + +def test_dbStats_empty_database_returns_ok(collection): + """Test dbStats returns ok:1 with zero collections on an empty database.""" + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + {"ok": 1.0, "db": collection.database.name, "collections": Int64(0)}, + msg="Empty database should return ok:1 with zero collections", + ) + + +def test_dbStats_nonexistent_database_returns_zeros(collection, register_db_cleanup): + """Test dbStats on a non-existent database returns zeroed size and count fields.""" + missing_coll = TargetDatabase(suffix="missing").resolve(collection.database, collection) + register_db_cleanup(missing_coll.database.name) + result = execute_command(missing_coll, {"dbStats": 1}) + assertSuccessPartial( + result, + { + "ok": 1.0, + "db": missing_coll.database.name, + "collections": Int64(0), + "objects": Int64(0), + "storageSize": 0.0, + "indexes": Int64(0), + "indexSize": 0.0, + }, + msg="Non-existent database should report all counts and sizes as zero", + ) + + +def test_dbStats_admin_database_reports_admin_name(collection): + """Test dbStats executed against the admin database reports db:admin.""" + admin_coll = collection.database.client["admin"]["unused"] + result = execute_command(admin_coll, {"dbStats": 1}) + assertSuccessPartial( + result, + {"ok": 1.0, "db": "admin"}, + msg="dbStats on admin database should report db:admin", + ) + + +def test_dbStats_unrecognized_field_errors(collection): + """Test dbStats rejects an unrecognized command field.""" + collection.insert_one({"_id": 1}) + result = execute_command(collection, {"dbStats": 1, "bogusField": 1}) + assertFailureCode( + result, + UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="Unrecognized command field should error with code 40415", + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py new file mode 100644 index 000000000..c420d489d --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py @@ -0,0 +1,84 @@ +"""Tests for the dbStats ``freeStorage`` parameter. + +Covers acceptance of freeStorage 0/1, presence of the free-storage fields +when freeStorage:1 is set, the totalFreeStorageSize relationship, presence +of filesystem size fields, and absence of the free-storage fields when +freeStorage is omitted or 0. +""" + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import ( + assertProperties, + assertSuccess, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Exists, NotExists + +pytestmark = pytest.mark.admin + + +def test_dbStats_free_storage_one_includes_expected_fields(collection): + """Test dbStats with freeStorage:1 includes the three free-storage fields.""" + collection.insert_one({"_id": 1}) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) + assertProperties( + result, + { + "freeStorageSize": Exists(), + "indexFreeStorageSize": Exists(), + "totalFreeStorageSize": Exists(), + }, + raw_res=True, + msg="freeStorage:1 should include free-storage fields", + ) + + +def test_dbStats_total_free_storage_size_relationship(collection): + """Test totalFreeStorageSize equals freeStorageSize plus indexFreeStorageSize.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) + assertSuccess( + result["totalFreeStorageSize"], + result["freeStorageSize"] + result["indexFreeStorageSize"], + raw_res=True, + msg="totalFreeStorageSize should equal freeStorageSize + indexFreeStorageSize", + ) + + +OMITS_FREE_STORAGE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "no_free_storage_param", + command={"dbStats": 1}, + checks={ + "freeStorageSize": NotExists(), + "indexFreeStorageSize": NotExists(), + "totalFreeStorageSize": NotExists(), + }, + msg="Omitting freeStorage should omit free-storage fields", + ), + DiagnosticTestCase( + "free_storage_zero", + command={"dbStats": 1, "freeStorage": 0}, + checks={ + "freeStorageSize": NotExists(), + "indexFreeStorageSize": NotExists(), + "totalFreeStorageSize": NotExists(), + }, + msg="freeStorage:0 should omit free-storage fields", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OMITS_FREE_STORAGE_TESTS)) +def test_dbStats_omits_free_storage_fields(collection, test): + """Test dbStats omits free-storage fields when freeStorage is not set or 0.""" + collection.insert_one({"_id": 1}) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, raw_res=True, msg=test.msg) 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 new file mode 100644 index 000000000..2cd239783 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py @@ -0,0 +1,182 @@ +"""Tests for the dbStats response structure. + +Covers the presence and BSON type of every documented response field, the +totalSize relationship, the db-name field, the avgObjSize relationship, and +collection/view counts. +""" + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import ( + assertProperties, + assertSuccess, + assertSuccessPartial, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Gt, IsType +from documentdb_tests.framework.target_collection import ViewCollection + +pytestmark = pytest.mark.admin + + +# One DiagnosticTestCase per documented response field, following the buildInfo pattern. +RESPONSE_PROPERTY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="db_is_string", + checks={"db": IsType("string")}, + msg="'db' field should be a string", + ), + DiagnosticTestCase( + id="collections_is_long", + checks={"collections": IsType("long")}, + msg="'collections' field should be a long", + ), + DiagnosticTestCase( + id="views_is_long", + checks={"views": IsType("long")}, + msg="'views' field should be a long", + ), + DiagnosticTestCase( + id="objects_is_long", + checks={"objects": IsType("long")}, + msg="'objects' field should be a long", + ), + DiagnosticTestCase( + id="avgObjSize_is_double", + checks={"avgObjSize": IsType("double")}, + msg="'avgObjSize' field should be a double", + ), + DiagnosticTestCase( + id="dataSize_is_double", + checks={"dataSize": IsType("double")}, + msg="'dataSize' field should be a double", + ), + DiagnosticTestCase( + id="storageSize_is_double", + checks={"storageSize": IsType("double")}, + msg="'storageSize' field should be a double", + ), + DiagnosticTestCase( + id="indexes_is_long", + checks={"indexes": IsType("long")}, + msg="'indexes' field should be a long", + ), + DiagnosticTestCase( + id="indexSize_is_double", + checks={"indexSize": IsType("double")}, + msg="'indexSize' field should be a double", + ), + DiagnosticTestCase( + id="totalSize_is_double", + checks={"totalSize": IsType("double")}, + msg="'totalSize' field should be a double", + ), + DiagnosticTestCase( + id="scaleFactor_is_long", + checks={"scaleFactor": IsType("long")}, + msg="'scaleFactor' field should be a long", + ), + DiagnosticTestCase( + id="fsUsedSize_is_double", + checks={"fsUsedSize": IsType("double")}, + msg="'fsUsedSize' field should be a double", + ), + DiagnosticTestCase( + id="fsTotalSize_is_double", + checks={"fsTotalSize": IsType("double")}, + msg="'fsTotalSize' field should be a double", + ), + DiagnosticTestCase( + id="ok_is_double", + checks={"ok": IsType("double")}, + msg="'ok' field should be a double", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(RESPONSE_PROPERTY_TESTS)) +def test_dbStats_response_properties(collection, test): + """Verifies each documented dbStats response field has the expected BSON type.""" + collection.insert_many([{"_id": i, "a": i} for i in range(5)]) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1}) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + + +def test_dbStats_total_size_relationship(collection): + """Test totalSize equals storageSize plus indexSize.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1}) + assertSuccess( + result["totalSize"], + expected=result["storageSize"] + result["indexSize"], + raw_res=True, + msg="totalSize should equal storageSize + indexSize", + ) + + +def test_dbStats_db_field_matches_database_name(collection): + """Test the db field matches the queried database name.""" + collection.insert_one({"_id": 1}) + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + expected={"db": collection.database.name}, + msg="db field should match the database name", + ) + + +def test_dbStats_data_size_positive_after_insert(collection): + """Test dataSize is greater than zero after inserting documents.""" + collection.insert_many([{"_id": i, "data": "x" * 50} for i in range(10)]) + result = execute_command(collection, {"dbStats": 1}) + assertProperties( + result, + {"dataSize": Gt(0.0)}, + raw_res=True, + msg="dataSize should be positive after inserts", + ) + + +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}) + assertSuccess( + result["avgObjSize"], + expected=result["dataSize"] / result["objects"], + raw_res=True, + msg="avgObjSize should equal dataSize / objects", + ) + + +def test_dbStats_collections_count_includes_system_views(collection): + """Test collections count includes system.views after a view is created.""" + collection.insert_many([{"_id": i} for i in range(3)]) + collection.database.command( + "create", f"{collection.name}_view", viewOn=collection.name, pipeline=[] + ) + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + expected={"collections": Int64(2)}, + msg="collections should include the base collection and system.views", + ) + + +def test_dbStats_views_count(collection): + """Test the views field counts created views.""" + collection.insert_many([{"_id": i} for i in range(3)]) + ViewCollection().resolve(collection.database, collection) + result = execute_command(collection, {"dbStats": 1}) + assertSuccessPartial( + result, + expected={"views": Int64(1)}, + msg="views should count the created view", + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py new file mode 100644 index 000000000..6411718c5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py @@ -0,0 +1,206 @@ +"""Tests for the dbStats ``scale`` parameter. + +Covers valid scale values and their reported scaleFactor (including type +coverage and non-integer truncation), invalid scale values, invalid scale +types, duplicate scale keys, and the scaling applied to size fields. +""" + +import pytest +from bson import SON, Decimal128, Int64 + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import ( + assertFailureCode, + assertProperties, + assertSuccessPartial, +) +from documentdb_tests.framework.bson_type_validator import ( + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.test_constants import BsonType + +pytestmark = pytest.mark.admin + + +# Type-level acceptance and rejection for the scale parameter. +# Valid numeric types: double, int, long, decimal, null. +# The default decimal sample (0.5) is overridden to 1024 since 0.5 would fail +# with BadValue rather than TypeMismatch. +SCALE_TYPE_PARAMS: list[BsonTypeTestCase] = [ + BsonTypeTestCase( + id="scale", + msg="scale should reject non-numeric types with TypeMismatch", + keyword="scale", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL, BsonType.NULL], + default_error_code=TYPE_MISMATCH_ERROR, + valid_inputs={BsonType.DECIMAL: Decimal128("1024")}, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(SCALE_TYPE_PARAMS) +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(SCALE_TYPE_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_dbStats_scale_accepts_valid_type(collection, bson_type, sample_value, spec): + """Test dbStats accepts valid BSON types for the scale parameter.""" + result = execute_command(collection, {"dbStats": 1, "scale": sample_value}) + assertSuccessPartial(result, {"ok": 1.0}, msg=spec.msg) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_dbStats_scale_rejects_invalid_type(collection, bson_type, sample_value, spec): + """Test dbStats rejects non-numeric BSON types for the scale parameter with TypeMismatch.""" + result = execute_command(collection, {"dbStats": 1, "scale": sample_value}) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +# Truncation and default behaviour edge cases not covered by type-level tests. +SCALE_EDGE_CASES: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "double_truncates", + command={"dbStats": 1, "scale": 2.5}, + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(2))}, + msg="Double scale should truncate toward zero", + ), + DiagnosticTestCase( + "double_1023_999_truncates", + command={"dbStats": 1, "scale": 1023.999}, + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1023))}, + msg="Double scale 1023.999 should truncate to 1023", + ), + DiagnosticTestCase( + "default_no_scale", + command={"dbStats": 1}, + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1))}, + msg="Omitting scale should default scaleFactor to 1", + ), + DiagnosticTestCase( + "duplicate_keys_last_valid", + command=SON([("dbStats", 1), ("scale", 1), ("scale", 1024)]), + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1024))}, + msg="Last duplicate scale value should win", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SCALE_EDGE_CASES)) +def test_dbStats_scale_edge_cases(collection, test): + """Test dbStats scale truncation and default behaviour.""" + result = execute_command(collection, test.command) + assertProperties(result, test.checks, raw_res=True, msg=test.msg) + + +# Invalid scale values (BadValue). Non-positive or truncate-to-zero values of +# otherwise-valid numeric types are rejected with code 2. Type-level rejections +# (TypeMismatch) are covered by REJECTION_CASES above. +INVALID_SCALE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "zero", + command={"dbStats": 1, "scale": 0}, + error_code=BAD_VALUE_ERROR, + msg="scale=0 should error with BadValue", + ), + DiagnosticTestCase( + "negative_int", + command={"dbStats": 1, "scale": -1}, + error_code=BAD_VALUE_ERROR, + msg="Negative int scale should error with BadValue", + ), + DiagnosticTestCase( + "fractional_lt_1", + command={"dbStats": 1, "scale": 0.5}, + error_code=BAD_VALUE_ERROR, + msg="Fractional scale < 1 should error with BadValue", + ), + DiagnosticTestCase( + "approaching_1_from_below", + command={"dbStats": 1, "scale": 0.9999999}, + error_code=BAD_VALUE_ERROR, + msg="Scale truncating to 0 should error with BadValue", + ), + DiagnosticTestCase( + "negative_double", + command={"dbStats": 1, "scale": -1.5}, + error_code=BAD_VALUE_ERROR, + msg="Negative double scale should error with BadValue", + ), + DiagnosticTestCase( + "negative_int64", + command={"dbStats": 1, "scale": Int64(-5)}, + error_code=BAD_VALUE_ERROR, + msg="Negative int64 scale should error with BadValue", + ), + DiagnosticTestCase( + "negative_decimal", + command={"dbStats": 1, "scale": Decimal128("-5")}, + error_code=BAD_VALUE_ERROR, + msg="Negative decimal scale should error with BadValue", + ), + DiagnosticTestCase( + "decimal_lt_1", + command={"dbStats": 1, "scale": Decimal128("0.5")}, + error_code=BAD_VALUE_ERROR, + msg="Decimal scale < 1 should error with BadValue", + ), + DiagnosticTestCase( + "duplicate_keys_last_invalid", + command=SON([("dbStats", 1), ("scale", 1024), ("scale", -1)]), + error_code=BAD_VALUE_ERROR, + msg="Invalid last duplicate scale value should error", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_SCALE_TESTS)) +def test_dbStats_invalid_scale_errors(collection, test): + """Test dbStats rejects invalid scale values and types with the expected error.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) + + +def test_dbStats_scale_divides_storage_size(collection): + """Test the scale factor divides storageSize in the response.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + scale = 1024 + unscaled = execute_command(collection, {"dbStats": 1}) + scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) + assertSuccessPartial( + scaled, + expected={"storageSize": unscaled.get("storageSize") / scale}, + msg="storageSize should be divided by the scale factor", + ) + + +def test_dbStats_scale_divides_index_size(collection): + """Test the scale factor divides indexSize in the response.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + collection.create_index("a") + scale = 1024 + unscaled = execute_command(collection, {"dbStats": 1}) + scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) + assertSuccessPartial( + scaled, + expected={"indexSize": unscaled.get("indexSize") / scale}, + msg="indexSize should be divided by the scale factor", + ) + + +def test_dbStats_scale_does_not_affect_avg_obj_size(collection): + """Test avgObjSize is not affected by the scale factor.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + unscaled = execute_command(collection, {"dbStats": 1}) + scaled = execute_command(collection, {"dbStats": 1, "scale": 1024}) + assertSuccessPartial( + scaled, + expected={"avgObjSize": unscaled.get("avgObjSize")}, + msg="avgObjSize should be unaffected by scale", + ) From a4394520968c7cb799606904dc53201025ef934e Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Wed, 17 Jun 2026 14:55:58 -0700 Subject: [PATCH 2/5] Reorganized tests and reworked docstrings for clarity Signed-off-by: PatersonProjects --- .../commands/dbStats/test_dbStats_accuracy.py | 6 +- .../dbStats/test_dbStats_argument_handling.py | 229 +++++++++++++++++- .../test_dbStats_collection_scenarios.py | 19 +- .../dbStats/test_dbStats_free_storage.py | 84 ------- .../test_dbStats_response_structure.py | 23 +- .../dbStats/test_dbStats_scale_parameter.py | 206 ---------------- 6 files changed, 237 insertions(+), 330 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py delete mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py 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 b0565a7c9..8871a1a2b 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 @@ -62,7 +62,7 @@ def test_dbStats_storage_size_does_not_decrease_after_delete(collection): after = execute_command(collection, {"dbStats": 1}) assertProperties( after, - {"storageSize": Gte(before["storageSize"])}, + {"storageSize": Gte(before.get("storageSize"))}, raw_res=True, msg="storageSize should not decrease after deletes", ) @@ -76,7 +76,7 @@ def test_dbStats_data_size_decreases_after_delete(collection): after = execute_command(collection, {"dbStats": 1}) assertProperties( before, - {"dataSize": Gt(after["dataSize"])}, + {"dataSize": Gt(after.get("dataSize"))}, raw_res=True, msg="dataSize should decrease after deletes", ) @@ -88,7 +88,7 @@ def test_dbStats_fs_total_size_gte_used_size(collection): result = execute_command(collection, {"dbStats": 1}) assertProperties( result, - {"fsTotalSize": Gte(result["fsUsedSize"])}, + {"fsTotalSize": Gte(result.get("fsUsedSize"))}, raw_res=True, msg="fsTotalSize should be >= fsUsedSize", ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py index 8e2ae9fa8..320ebc9bd 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py @@ -3,21 +3,34 @@ The value of the ``dbStats`` field is ignored by the server: any value selects the current database, so every BSON type should be accepted, including numeric edge cases such as 0, -1, and Infinity. + +Also covers the ``scale`` parameter (valid/invalid values and types, +truncation, duplicate keys, and the scaling applied to size fields) and the +``freeStorage`` parameter (acceptance, free-storage field presence and the +totalFreeStorageSize relationship, and omission when unset or 0). """ import pytest +from bson import SON, Decimal128, Int64 from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( DiagnosticTestCase, ) -from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.assertions import ( + assertFailureCode, + assertProperties, + assertSuccess, + assertSuccessPartial, +) from documentdb_tests.framework.bson_type_validator import ( BsonTypeTestCase, generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, ) +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, TYPE_MISMATCH_ERROR from documentdb_tests.framework.executor import execute_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.property_checks import Eq, Exists, NotExists from documentdb_tests.framework.test_constants import FLOAT_INFINITY, BsonType pytestmark = pytest.mark.admin @@ -33,10 +46,10 @@ ), ] -ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(DBSTATS_VALUE_PARAMS) +VALUE_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(DBSTATS_VALUE_PARAMS) -@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +@pytest.mark.parametrize("bson_type,sample_value,spec", VALUE_ACCEPTANCE_CASES) def test_dbStats_accepts_any_value_type(collection, bson_type, sample_value, spec): """Test dbStats accepts all BSON types for the command field value.""" result = execute_command(collection, {"dbStats": sample_value}) @@ -75,3 +88,211 @@ def test_dbStats_accepts_value_edge_cases(collection, test): """Test dbStats succeeds for specific numeric edge-case command values.""" result = execute_command(collection, test.command) assertProperties(result, test.checks, msg=test.msg, raw_res=True) + + +# Type-level acceptance and rejection for the scale parameter. +# Valid numeric types: double, int, long, decimal, null. +# The default decimal sample (0.5) is overridden to 1024 since 0.5 would fail +# with BadValue rather than TypeMismatch. +SCALE_TYPE_PARAMS: list[BsonTypeTestCase] = [ + BsonTypeTestCase( + id="scale", + msg="scale should reject non-numeric types with TypeMismatch", + keyword="scale", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL, BsonType.NULL], + default_error_code=TYPE_MISMATCH_ERROR, + valid_inputs={BsonType.DECIMAL: Decimal128("1024")}, + ), +] + +SCALE_REJECTION_CASES = generate_bson_rejection_test_cases(SCALE_TYPE_PARAMS) +SCALE_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(SCALE_TYPE_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", SCALE_ACCEPTANCE_CASES) +def test_dbStats_scale_accepts_valid_type(collection, bson_type, sample_value, spec): + """Test dbStats accepts valid BSON types for the scale parameter.""" + result = execute_command(collection, {"dbStats": 1, "scale": sample_value}) + assertSuccessPartial(result, {"ok": 1.0}, msg=spec.msg) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", SCALE_REJECTION_CASES) +def test_dbStats_scale_rejects_invalid_type(collection, bson_type, sample_value, spec): + """Test dbStats rejects non-numeric BSON types for the scale parameter with TypeMismatch.""" + result = execute_command(collection, {"dbStats": 1, "scale": sample_value}) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +# Truncation and default behaviour edge cases not covered by type-level tests. +SCALE_EDGE_CASES: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "double_truncates", + command={"dbStats": 1, "scale": 2.5}, + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(2))}, + msg="Double scale should truncate toward zero", + ), + DiagnosticTestCase( + "double_1023_999_truncates", + command={"dbStats": 1, "scale": 1023.999}, + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1023))}, + msg="Double scale 1023.999 should truncate to 1023", + ), + DiagnosticTestCase( + "default_no_scale", + command={"dbStats": 1}, + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1))}, + msg="Omitting scale should default scaleFactor to 1", + ), + DiagnosticTestCase( + "duplicate_keys_last_valid", + command=SON([("dbStats", 1), ("scale", 1), ("scale", 1024)]), + checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1024))}, + msg="Last duplicate scale value should win", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SCALE_EDGE_CASES)) +def test_dbStats_scale_edge_cases(collection, test): + """Test dbStats scale truncation and default behaviour.""" + result = execute_command(collection, test.command) + assertProperties(result, test.checks, raw_res=True, msg=test.msg) + + +# Invalid scale values (BadValue). Non-positive or truncate-to-zero values of +# otherwise-valid numeric types are rejected with code 2. Type-level rejections +# (TypeMismatch) are covered by SCALE_REJECTION_CASES above. +INVALID_SCALE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "zero", + command={"dbStats": 1, "scale": 0}, + error_code=BAD_VALUE_ERROR, + msg="scale=0 should error with BadValue", + ), + DiagnosticTestCase( + "negative_int", + command={"dbStats": 1, "scale": -1}, + error_code=BAD_VALUE_ERROR, + msg="Negative int scale should error with BadValue", + ), + DiagnosticTestCase( + "fractional_lt_1", + command={"dbStats": 1, "scale": 0.5}, + error_code=BAD_VALUE_ERROR, + msg="Fractional scale < 1 should error with BadValue", + ), + DiagnosticTestCase( + "duplicate_keys_last_invalid", + command=SON([("dbStats", 1), ("scale", 1024), ("scale", -1)]), + error_code=BAD_VALUE_ERROR, + msg="Invalid last duplicate scale value should error", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_SCALE_TESTS)) +def test_dbStats_invalid_scale_errors(collection, test): + """Test dbStats rejects invalid (non-positive/truncate-to-zero) scale values with BadValue.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) + + +def test_dbStats_scale_divides_storage_size(collection): + """Test the scale factor divides storageSize in the response.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + scale = 1024 + unscaled = execute_command(collection, {"dbStats": 1}) + scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) + assertSuccessPartial( + scaled, + expected={"storageSize": unscaled.get("storageSize") / scale}, + msg="storageSize should be divided by the scale factor", + ) + + +def test_dbStats_scale_divides_index_size(collection): + """Test the scale factor divides indexSize in the response.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + collection.create_index("a") + scale = 1024 + unscaled = execute_command(collection, {"dbStats": 1}) + scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) + assertSuccessPartial( + scaled, + expected={"indexSize": unscaled.get("indexSize") / scale}, + msg="indexSize should be divided by the scale factor", + ) + + +def test_dbStats_scale_does_not_affect_avg_obj_size(collection): + """Test avgObjSize is not affected by the scale factor.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + unscaled = execute_command(collection, {"dbStats": 1}) + scaled = execute_command(collection, {"dbStats": 1, "scale": 1024}) + assertSuccessPartial( + scaled, + expected={"avgObjSize": unscaled.get("avgObjSize")}, + msg="avgObjSize should be unaffected by scale", + ) + + +def test_dbStats_free_storage_one_includes_expected_fields(collection): + """Test dbStats with freeStorage:1 includes the three free-storage fields.""" + collection.insert_one({"_id": 1}) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) + assertProperties( + result, + { + "freeStorageSize": Exists(), + "indexFreeStorageSize": Exists(), + "totalFreeStorageSize": Exists(), + }, + raw_res=True, + msg="freeStorage:1 should include free-storage fields", + ) + + +def test_dbStats_total_free_storage_size_relationship(collection): + """Test totalFreeStorageSize equals freeStorageSize plus indexFreeStorageSize.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + collection.create_index("a") + result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) + assertSuccess( + result.get("totalFreeStorageSize"), + result.get("freeStorageSize") + result.get("indexFreeStorageSize"), + raw_res=True, + msg="totalFreeStorageSize should equal freeStorageSize + indexFreeStorageSize", + ) + + +OMITS_FREE_STORAGE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "no_free_storage_param", + command={"dbStats": 1}, + checks={ + "freeStorageSize": NotExists(), + "indexFreeStorageSize": NotExists(), + "totalFreeStorageSize": NotExists(), + }, + msg="Omitting freeStorage should omit free-storage fields", + ), + DiagnosticTestCase( + "free_storage_zero", + command={"dbStats": 1, "freeStorage": 0}, + checks={ + "freeStorageSize": NotExists(), + "indexFreeStorageSize": NotExists(), + "totalFreeStorageSize": NotExists(), + }, + msg="freeStorage:0 should omit free-storage fields", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OMITS_FREE_STORAGE_TESTS)) +def test_dbStats_omits_free_storage_fields(collection, test): + """Test dbStats omits free-storage fields when freeStorage is not set or 0.""" + collection.insert_one({"_id": 1}) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, raw_res=True, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py index 60f92b64f..077d8640e 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py @@ -1,8 +1,8 @@ """Tests for dbStats across collection variants and data scenarios. -Covers empty collections (with and without a secondary index), avgObjSize -when there are no objects, positive storage/index sizes when data and -indexes exist, total index counts across multiple collections, capped +Covers empty collections (with and without a secondary index), positive +storage and index sizes for an indexed collection, avgObjSize when there +are no objects, total index counts across multiple collections, capped collections, and object counts across a range of collection sizes and document shapes. """ @@ -55,19 +55,6 @@ def test_dbStats_avg_obj_size_zero_when_no_objects(collection): ) -def test_dbStats_index_size_positive_with_indexes(collection): - """Test indexSize is positive when secondary indexes exist.""" - collection.insert_many([{"_id": i, "a": i} for i in range(10)]) - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1}) - assertProperties( - result, - {"indexSize": Gt(0.0)}, - raw_res=True, - msg="indexSize should be positive with indexes", - ) - - def test_dbStats_total_index_count_across_collections(collection): """Test indexes counts the default and secondary indexes across all collections.""" collection.insert_many([{"_id": i, "a": i} for i in range(5)]) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py deleted file mode 100644 index c420d489d..000000000 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_free_storage.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Tests for the dbStats ``freeStorage`` parameter. - -Covers acceptance of freeStorage 0/1, presence of the free-storage fields -when freeStorage:1 is set, the totalFreeStorageSize relationship, presence -of filesystem size fields, and absence of the free-storage fields when -freeStorage is omitted or 0. -""" - -import pytest - -from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( - DiagnosticTestCase, -) -from documentdb_tests.framework.assertions import ( - assertProperties, - assertSuccess, -) -from documentdb_tests.framework.executor import execute_command -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Exists, NotExists - -pytestmark = pytest.mark.admin - - -def test_dbStats_free_storage_one_includes_expected_fields(collection): - """Test dbStats with freeStorage:1 includes the three free-storage fields.""" - collection.insert_one({"_id": 1}) - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) - assertProperties( - result, - { - "freeStorageSize": Exists(), - "indexFreeStorageSize": Exists(), - "totalFreeStorageSize": Exists(), - }, - raw_res=True, - msg="freeStorage:1 should include free-storage fields", - ) - - -def test_dbStats_total_free_storage_size_relationship(collection): - """Test totalFreeStorageSize equals freeStorageSize plus indexFreeStorageSize.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) - assertSuccess( - result["totalFreeStorageSize"], - result["freeStorageSize"] + result["indexFreeStorageSize"], - raw_res=True, - msg="totalFreeStorageSize should equal freeStorageSize + indexFreeStorageSize", - ) - - -OMITS_FREE_STORAGE_TESTS: list[DiagnosticTestCase] = [ - DiagnosticTestCase( - "no_free_storage_param", - command={"dbStats": 1}, - checks={ - "freeStorageSize": NotExists(), - "indexFreeStorageSize": NotExists(), - "totalFreeStorageSize": NotExists(), - }, - msg="Omitting freeStorage should omit free-storage fields", - ), - DiagnosticTestCase( - "free_storage_zero", - command={"dbStats": 1, "freeStorage": 0}, - checks={ - "freeStorageSize": NotExists(), - "indexFreeStorageSize": NotExists(), - "totalFreeStorageSize": NotExists(), - }, - msg="freeStorage:0 should omit free-storage fields", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(OMITS_FREE_STORAGE_TESTS)) -def test_dbStats_omits_free_storage_fields(collection, test): - """Test dbStats omits free-storage fields when freeStorage is not set or 0.""" - collection.insert_one({"_id": 1}) - result = execute_command(collection, test.command) - assertProperties(result, test.checks, raw_res=True, msg=test.msg) 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 2cd239783..546d73093 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 @@ -1,8 +1,8 @@ """Tests for the dbStats response structure. Covers the presence and BSON type of every documented response field, the -totalSize relationship, the db-name field, the avgObjSize relationship, and -collection/view counts. +totalSize relationship, dataSize positivity after inserts, the avgObjSize +relationship, and collection/view counts. """ import pytest @@ -114,24 +114,13 @@ def test_dbStats_total_size_relationship(collection): collection.create_index("a") result = execute_command(collection, {"dbStats": 1}) assertSuccess( - result["totalSize"], - expected=result["storageSize"] + result["indexSize"], + result.get("totalSize"), + expected=result.get("storageSize") + result.get("indexSize"), raw_res=True, msg="totalSize should equal storageSize + indexSize", ) -def test_dbStats_db_field_matches_database_name(collection): - """Test the db field matches the queried database name.""" - collection.insert_one({"_id": 1}) - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - expected={"db": collection.database.name}, - msg="db field should match the database name", - ) - - def test_dbStats_data_size_positive_after_insert(collection): """Test dataSize is greater than zero after inserting documents.""" collection.insert_many([{"_id": i, "data": "x" * 50} for i in range(10)]) @@ -149,8 +138,8 @@ def test_dbStats_avg_obj_size_equals_data_size_over_objects(collection): collection.insert_many([{"_id": i, "data": "x" * (i + 1)} for i in range(10)]) result = execute_command(collection, {"dbStats": 1}) assertSuccess( - result["avgObjSize"], - expected=result["dataSize"] / result["objects"], + result.get("avgObjSize"), + expected=result.get("dataSize") / result.get("objects"), raw_res=True, msg="avgObjSize should equal dataSize / objects", ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py deleted file mode 100644 index 6411718c5..000000000 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_scale_parameter.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Tests for the dbStats ``scale`` parameter. - -Covers valid scale values and their reported scaleFactor (including type -coverage and non-integer truncation), invalid scale values, invalid scale -types, duplicate scale keys, and the scaling applied to size fields. -""" - -import pytest -from bson import SON, Decimal128, Int64 - -from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( - DiagnosticTestCase, -) -from documentdb_tests.framework.assertions import ( - assertFailureCode, - assertProperties, - assertSuccessPartial, -) -from documentdb_tests.framework.bson_type_validator import ( - BsonTypeTestCase, - generate_bson_acceptance_test_cases, - generate_bson_rejection_test_cases, -) -from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, TYPE_MISMATCH_ERROR -from documentdb_tests.framework.executor import execute_command -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Eq -from documentdb_tests.framework.test_constants import BsonType - -pytestmark = pytest.mark.admin - - -# Type-level acceptance and rejection for the scale parameter. -# Valid numeric types: double, int, long, decimal, null. -# The default decimal sample (0.5) is overridden to 1024 since 0.5 would fail -# with BadValue rather than TypeMismatch. -SCALE_TYPE_PARAMS: list[BsonTypeTestCase] = [ - BsonTypeTestCase( - id="scale", - msg="scale should reject non-numeric types with TypeMismatch", - keyword="scale", - valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL, BsonType.NULL], - default_error_code=TYPE_MISMATCH_ERROR, - valid_inputs={BsonType.DECIMAL: Decimal128("1024")}, - ), -] - -REJECTION_CASES = generate_bson_rejection_test_cases(SCALE_TYPE_PARAMS) -ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(SCALE_TYPE_PARAMS) - - -@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) -def test_dbStats_scale_accepts_valid_type(collection, bson_type, sample_value, spec): - """Test dbStats accepts valid BSON types for the scale parameter.""" - result = execute_command(collection, {"dbStats": 1, "scale": sample_value}) - assertSuccessPartial(result, {"ok": 1.0}, msg=spec.msg) - - -@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) -def test_dbStats_scale_rejects_invalid_type(collection, bson_type, sample_value, spec): - """Test dbStats rejects non-numeric BSON types for the scale parameter with TypeMismatch.""" - result = execute_command(collection, {"dbStats": 1, "scale": sample_value}) - assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) - - -# Truncation and default behaviour edge cases not covered by type-level tests. -SCALE_EDGE_CASES: list[DiagnosticTestCase] = [ - DiagnosticTestCase( - "double_truncates", - command={"dbStats": 1, "scale": 2.5}, - checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(2))}, - msg="Double scale should truncate toward zero", - ), - DiagnosticTestCase( - "double_1023_999_truncates", - command={"dbStats": 1, "scale": 1023.999}, - checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1023))}, - msg="Double scale 1023.999 should truncate to 1023", - ), - DiagnosticTestCase( - "default_no_scale", - command={"dbStats": 1}, - checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1))}, - msg="Omitting scale should default scaleFactor to 1", - ), - DiagnosticTestCase( - "duplicate_keys_last_valid", - command=SON([("dbStats", 1), ("scale", 1), ("scale", 1024)]), - checks={"ok": Eq(1.0), "scaleFactor": Eq(Int64(1024))}, - msg="Last duplicate scale value should win", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(SCALE_EDGE_CASES)) -def test_dbStats_scale_edge_cases(collection, test): - """Test dbStats scale truncation and default behaviour.""" - result = execute_command(collection, test.command) - assertProperties(result, test.checks, raw_res=True, msg=test.msg) - - -# Invalid scale values (BadValue). Non-positive or truncate-to-zero values of -# otherwise-valid numeric types are rejected with code 2. Type-level rejections -# (TypeMismatch) are covered by REJECTION_CASES above. -INVALID_SCALE_TESTS: list[DiagnosticTestCase] = [ - DiagnosticTestCase( - "zero", - command={"dbStats": 1, "scale": 0}, - error_code=BAD_VALUE_ERROR, - msg="scale=0 should error with BadValue", - ), - DiagnosticTestCase( - "negative_int", - command={"dbStats": 1, "scale": -1}, - error_code=BAD_VALUE_ERROR, - msg="Negative int scale should error with BadValue", - ), - DiagnosticTestCase( - "fractional_lt_1", - command={"dbStats": 1, "scale": 0.5}, - error_code=BAD_VALUE_ERROR, - msg="Fractional scale < 1 should error with BadValue", - ), - DiagnosticTestCase( - "approaching_1_from_below", - command={"dbStats": 1, "scale": 0.9999999}, - error_code=BAD_VALUE_ERROR, - msg="Scale truncating to 0 should error with BadValue", - ), - DiagnosticTestCase( - "negative_double", - command={"dbStats": 1, "scale": -1.5}, - error_code=BAD_VALUE_ERROR, - msg="Negative double scale should error with BadValue", - ), - DiagnosticTestCase( - "negative_int64", - command={"dbStats": 1, "scale": Int64(-5)}, - error_code=BAD_VALUE_ERROR, - msg="Negative int64 scale should error with BadValue", - ), - DiagnosticTestCase( - "negative_decimal", - command={"dbStats": 1, "scale": Decimal128("-5")}, - error_code=BAD_VALUE_ERROR, - msg="Negative decimal scale should error with BadValue", - ), - DiagnosticTestCase( - "decimal_lt_1", - command={"dbStats": 1, "scale": Decimal128("0.5")}, - error_code=BAD_VALUE_ERROR, - msg="Decimal scale < 1 should error with BadValue", - ), - DiagnosticTestCase( - "duplicate_keys_last_invalid", - command=SON([("dbStats", 1), ("scale", 1024), ("scale", -1)]), - error_code=BAD_VALUE_ERROR, - msg="Invalid last duplicate scale value should error", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(INVALID_SCALE_TESTS)) -def test_dbStats_invalid_scale_errors(collection, test): - """Test dbStats rejects invalid scale values and types with the expected error.""" - result = execute_command(collection, test.command) - assertFailureCode(result, test.error_code, msg=test.msg) - - -def test_dbStats_scale_divides_storage_size(collection): - """Test the scale factor divides storageSize in the response.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - scale = 1024 - unscaled = execute_command(collection, {"dbStats": 1}) - scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) - assertSuccessPartial( - scaled, - expected={"storageSize": unscaled.get("storageSize") / scale}, - msg="storageSize should be divided by the scale factor", - ) - - -def test_dbStats_scale_divides_index_size(collection): - """Test the scale factor divides indexSize in the response.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - collection.create_index("a") - scale = 1024 - unscaled = execute_command(collection, {"dbStats": 1}) - scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) - assertSuccessPartial( - scaled, - expected={"indexSize": unscaled.get("indexSize") / scale}, - msg="indexSize should be divided by the scale factor", - ) - - -def test_dbStats_scale_does_not_affect_avg_obj_size(collection): - """Test avgObjSize is not affected by the scale factor.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - unscaled = execute_command(collection, {"dbStats": 1}) - scaled = execute_command(collection, {"dbStats": 1, "scale": 1024}) - assertSuccessPartial( - scaled, - expected={"avgObjSize": unscaled.get("avgObjSize")}, - msg="avgObjSize should be unaffected by scale", - ) From 4b65bec6949f585375546c9e8aa59ec5c95b3726 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 18 Jun 2026 09:42:43 -0700 Subject: [PATCH 3/5] Test fixes, removed out of scope tests, further parametrization by adding field to diagnostic_test_case Signed-off-by: PatersonProjects --- .../commands/dbStats/test_dbStats_accuracy.py | 135 ++++++-------- .../dbStats/test_dbStats_argument_handling.py | 162 ++++++----------- .../test_dbStats_collection_scenarios.py | 166 ++++++------------ .../dbStats/test_dbStats_core_behavior.py | 88 +++++----- .../commands/dbStats/test_dbStats_errors.py | 68 +++++++ .../test_dbStats_response_structure.py | 80 ++++----- .../diagnostic/utils/diagnostic_test_case.py | 4 +- 7 files changed, 315 insertions(+), 388 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py 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 8871a1a2b..52f7bbb6c 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 @@ -1,94 +1,69 @@ -"""Tests for dbStats accuracy, state changes, and field relationships. +"""Tests for dbStats accuracy and state changes. -Covers that counts reflect inserted documents, created collections, and -created indexes; that storageSize does not shrink while dataSize shrinks -after deletes; that objects equals the sum across collections; and the -fsTotalSize >= fsUsedSize relationship. +Covers count fields (collections, objects, indexes) reflecting database +state. """ import pytest from bson import Int64 -from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +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_command -from documentdb_tests.framework.property_checks import Gt, Gte -from documentdb_tests.framework.target_collection import ExtraCollections +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq pytestmark = pytest.mark.admin -def test_dbStats_collections_count_reflects_created_collections(collection): - """Test collections reflects the number of created collections.""" - ExtraCollections(count=3).resolve(collection.database, collection) - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"collections": Int64(3)}, +COUNT_ACCURACY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="collections_count_reflects_created", + setup=[{"create": "c1"}, {"create": "c2"}, {"create": "c3"}], + command={"dbStats": 1}, + use_admin=False, + checks={"collections": Eq(Int64(3))}, msg="collections should equal the number of created collections", - ) - - -def test_dbStats_objects_count_equals_sum_across_collections(collection): - """Test objects equals the sum of document counts across collections.""" - collection.insert_many([{"_id": i} for i in range(4)]) - c2 = collection.database[f"{collection.name}_c2"] - c2.insert_many([{"_id": i} for i in range(6)]) - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"objects": Int64(10)}, + ), + DiagnosticTestCase( + id="objects_sum_across_collections", + setup=[ + {"insert": "c1", "documents": [{"_id": i} for i in range(4)]}, + {"insert": "c2", "documents": [{"_id": i} for i in range(6)]}, + ], + command={"dbStats": 1}, + use_admin=False, + checks={"objects": Eq(Int64(10))}, msg="objects should equal the total documents across all collections", - ) - - -def test_dbStats_indexes_count_reflects_created_indexes(collection): - """Test indexes reflects the default _id index plus created indexes.""" - collection.insert_many([{"_id": i, "a": i, "b": i} for i in range(5)]) - collection.create_index("a") - collection.create_index("b") - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"indexes": Int64(3)}, + ), + DiagnosticTestCase( + id="indexes_default_plus_created", + setup=[ + {"insert": "c1", "documents": [{"_id": i, "a": i, "b": i} for i in range(5)]}, + { + "createIndexes": "c1", + "indexes": [ + {"key": {"a": 1}, "name": "a_1"}, + {"key": {"b": 1}, "name": "b_1"}, + ], + }, + ], + command={"dbStats": 1}, + use_admin=False, + checks={"indexes": Eq(Int64(3))}, msg="indexes should count the default _id index plus created indexes", - ) - - -def test_dbStats_storage_size_does_not_decrease_after_delete(collection): - """Test storageSize does not decrease after documents are removed.""" - collection.insert_many([{"_id": i, "data": "x" * 100} for i in range(100)]) - before = execute_command(collection, {"dbStats": 1}) - collection.delete_many({}) - after = execute_command(collection, {"dbStats": 1}) - assertProperties( - after, - {"storageSize": Gte(before.get("storageSize"))}, - raw_res=True, - msg="storageSize should not decrease after deletes", - ) - - -def test_dbStats_data_size_decreases_after_delete(collection): - """Test dataSize decreases after documents are removed.""" - collection.insert_many([{"_id": i, "data": "x" * 100} for i in range(100)]) - before = execute_command(collection, {"dbStats": 1}) - collection.delete_many({}) - after = execute_command(collection, {"dbStats": 1}) - assertProperties( - before, - {"dataSize": Gt(after.get("dataSize"))}, - raw_res=True, - msg="dataSize should decrease after deletes", - ) - - -def test_dbStats_fs_total_size_gte_used_size(collection): - """Test fsTotalSize is greater than or equal to fsUsedSize.""" - collection.insert_one({"_id": 1}) - result = execute_command(collection, {"dbStats": 1}) - assertProperties( - result, - {"fsTotalSize": Gte(result.get("fsUsedSize"))}, - raw_res=True, - msg="fsTotalSize should be >= fsUsedSize", - ) + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COUNT_ACCURACY_TESTS)) +def test_dbStats_count_accuracy(collection, test): + """Test dbStats count fields accurately reflect created collections, documents, and indexes.""" + for setup_command in test.setup: + setup_result = execute_command(collection, setup_command) + if isinstance(setup_result, Exception): + raise setup_result + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py index 320ebc9bd..876750ffb 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py @@ -4,10 +4,11 @@ selects the current database, so every BSON type should be accepted, including numeric edge cases such as 0, -1, and Infinity. -Also covers the ``scale`` parameter (valid/invalid values and types, -truncation, duplicate keys, and the scaling applied to size fields) and the -``freeStorage`` parameter (acceptance, free-storage field presence and the -totalFreeStorageSize relationship, and omission when unset or 0). +Also covers the ``scale`` parameter (type-level acceptance and rejection, +value truncation, and duplicate-key behavior) and the ``freeStorage`` +parameter (type-level acceptance and rejection, free-storage field +presence, and omission when unset or 0). Value-level errors (BadValue) +are in test_dbStats_errors.py. """ import pytest @@ -19,7 +20,6 @@ from documentdb_tests.framework.assertions import ( assertFailureCode, assertProperties, - assertSuccess, assertSuccessPartial, ) from documentdb_tests.framework.bson_type_validator import ( @@ -27,7 +27,7 @@ generate_bson_acceptance_test_cases, generate_bson_rejection_test_cases, ) -from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, TYPE_MISMATCH_ERROR +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR from documentdb_tests.framework.executor import execute_command from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq, Exists, NotExists @@ -36,7 +36,6 @@ pytestmark = pytest.mark.admin -# dbStats ignores the command field value — all BSON types should succeed. DBSTATS_VALUE_PARAMS: list[BsonTypeTestCase] = [ BsonTypeTestCase( id="dbStats_value", @@ -60,7 +59,6 @@ def test_dbStats_accepts_any_value_type(collection, bson_type, sample_value, spe ) -# Specific numeric edge-case values for the command field. EDGE_CASE_TESTS: list[DiagnosticTestCase] = [ DiagnosticTestCase( id="value_zero", @@ -90,10 +88,6 @@ def test_dbStats_accepts_value_edge_cases(collection, test): assertProperties(result, test.checks, msg=test.msg, raw_res=True) -# Type-level acceptance and rejection for the scale parameter. -# Valid numeric types: double, int, long, decimal, null. -# The default decimal sample (0.5) is overridden to 1024 since 0.5 would fail -# with BadValue rather than TypeMismatch. SCALE_TYPE_PARAMS: list[BsonTypeTestCase] = [ BsonTypeTestCase( id="scale", @@ -123,7 +117,6 @@ def test_dbStats_scale_rejects_invalid_type(collection, bson_type, sample_value, assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) -# Truncation and default behaviour edge cases not covered by type-level tests. SCALE_EDGE_CASES: list[DiagnosticTestCase] = [ DiagnosticTestCase( "double_truncates", @@ -159,116 +152,59 @@ def test_dbStats_scale_edge_cases(collection, test): assertProperties(result, test.checks, raw_res=True, msg=test.msg) -# Invalid scale values (BadValue). Non-positive or truncate-to-zero values of -# otherwise-valid numeric types are rejected with code 2. Type-level rejections -# (TypeMismatch) are covered by SCALE_REJECTION_CASES above. -INVALID_SCALE_TESTS: list[DiagnosticTestCase] = [ - DiagnosticTestCase( - "zero", - command={"dbStats": 1, "scale": 0}, - error_code=BAD_VALUE_ERROR, - msg="scale=0 should error with BadValue", - ), - DiagnosticTestCase( - "negative_int", - command={"dbStats": 1, "scale": -1}, - error_code=BAD_VALUE_ERROR, - msg="Negative int scale should error with BadValue", - ), - DiagnosticTestCase( - "fractional_lt_1", - command={"dbStats": 1, "scale": 0.5}, - error_code=BAD_VALUE_ERROR, - msg="Fractional scale < 1 should error with BadValue", - ), - DiagnosticTestCase( - "duplicate_keys_last_invalid", - command=SON([("dbStats", 1), ("scale", 1024), ("scale", -1)]), - error_code=BAD_VALUE_ERROR, - msg="Invalid last duplicate scale value should error", +FREE_STORAGE_TYPE_PARAMS: list[BsonTypeTestCase] = [ + BsonTypeTestCase( + id="freeStorage", + msg="freeStorage should reject non-numeric, non-bool types with TypeMismatch", + keyword="freeStorage", + valid_types=[ + BsonType.BOOL, + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=TYPE_MISMATCH_ERROR, ), ] +FREE_STORAGE_REJECTION_CASES = generate_bson_rejection_test_cases(FREE_STORAGE_TYPE_PARAMS) +FREE_STORAGE_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(FREE_STORAGE_TYPE_PARAMS) -@pytest.mark.parametrize("test", pytest_params(INVALID_SCALE_TESTS)) -def test_dbStats_invalid_scale_errors(collection, test): - """Test dbStats rejects invalid (non-positive/truncate-to-zero) scale values with BadValue.""" - result = execute_command(collection, test.command) - assertFailureCode(result, test.error_code, msg=test.msg) - - -def test_dbStats_scale_divides_storage_size(collection): - """Test the scale factor divides storageSize in the response.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - scale = 1024 - unscaled = execute_command(collection, {"dbStats": 1}) - scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) - assertSuccessPartial( - scaled, - expected={"storageSize": unscaled.get("storageSize") / scale}, - msg="storageSize should be divided by the scale factor", - ) - -def test_dbStats_scale_divides_index_size(collection): - """Test the scale factor divides indexSize in the response.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - collection.create_index("a") - scale = 1024 - unscaled = execute_command(collection, {"dbStats": 1}) - scaled = execute_command(collection, {"dbStats": 1, "scale": scale}) - assertSuccessPartial( - scaled, - expected={"indexSize": unscaled.get("indexSize") / scale}, - msg="indexSize should be divided by the scale factor", - ) +@pytest.mark.parametrize("bson_type,sample_value,spec", FREE_STORAGE_ACCEPTANCE_CASES) +def test_dbStats_free_storage_accepts_valid_type(collection, bson_type, sample_value, spec): + """Test dbStats accepts valid BSON types for the freeStorage parameter.""" + result = execute_command(collection, {"dbStats": 1, "freeStorage": sample_value}) + assertSuccessPartial(result, {"ok": 1.0}, msg=spec.msg) -def test_dbStats_scale_does_not_affect_avg_obj_size(collection): - """Test avgObjSize is not affected by the scale factor.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - unscaled = execute_command(collection, {"dbStats": 1}) - scaled = execute_command(collection, {"dbStats": 1, "scale": 1024}) - assertSuccessPartial( - scaled, - expected={"avgObjSize": unscaled.get("avgObjSize")}, - msg="avgObjSize should be unaffected by scale", - ) +@pytest.mark.parametrize("bson_type,sample_value,spec", FREE_STORAGE_REJECTION_CASES) +def test_dbStats_free_storage_rejects_invalid_type(collection, bson_type, sample_value, spec): + """Test dbStats rejects non-numeric, non-bool BSON types for freeStorage with TypeMismatch.""" + result = execute_command(collection, {"dbStats": 1, "freeStorage": sample_value}) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) -def test_dbStats_free_storage_one_includes_expected_fields(collection): - """Test dbStats with freeStorage:1 includes the three free-storage fields.""" - collection.insert_one({"_id": 1}) - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) - assertProperties( - result, - { +FREE_STORAGE_FIELD_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "free_storage_one_includes_fields", + setup=[ + {"insert": "c1", "documents": [{"_id": 1}]}, + {"createIndexes": "c1", "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ], + command={"dbStats": 1, "freeStorage": 1}, + checks={ "freeStorageSize": Exists(), "indexFreeStorageSize": Exists(), "totalFreeStorageSize": Exists(), }, - raw_res=True, msg="freeStorage:1 should include free-storage fields", - ) - - -def test_dbStats_total_free_storage_size_relationship(collection): - """Test totalFreeStorageSize equals freeStorageSize plus indexFreeStorageSize.""" - collection.insert_many([{"_id": i, "a": i} for i in range(20)]) - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1, "freeStorage": 1}) - assertSuccess( - result.get("totalFreeStorageSize"), - result.get("freeStorageSize") + result.get("indexFreeStorageSize"), - raw_res=True, - msg="totalFreeStorageSize should equal freeStorageSize + indexFreeStorageSize", - ) - - -OMITS_FREE_STORAGE_TESTS: list[DiagnosticTestCase] = [ + ), DiagnosticTestCase( "no_free_storage_param", + setup=[{"insert": "c1", "documents": [{"_id": 1}]}], command={"dbStats": 1}, checks={ "freeStorageSize": NotExists(), @@ -279,6 +215,7 @@ def test_dbStats_total_free_storage_size_relationship(collection): ), DiagnosticTestCase( "free_storage_zero", + setup=[{"insert": "c1", "documents": [{"_id": 1}]}], command={"dbStats": 1, "freeStorage": 0}, checks={ "freeStorageSize": NotExists(), @@ -290,9 +227,12 @@ def test_dbStats_total_free_storage_size_relationship(collection): ] -@pytest.mark.parametrize("test", pytest_params(OMITS_FREE_STORAGE_TESTS)) -def test_dbStats_omits_free_storage_fields(collection, test): - """Test dbStats omits free-storage fields when freeStorage is not set or 0.""" - collection.insert_one({"_id": 1}) +@pytest.mark.parametrize("test", pytest_params(FREE_STORAGE_FIELD_TESTS)) +def test_dbStats_free_storage_fields(collection, test): + """Test dbStats free-storage field presence based on the freeStorage option.""" + for setup_command in test.setup: + setup_result = execute_command(collection, setup_command) + if isinstance(setup_result, Exception): + raise setup_result result = execute_command(collection, test.command) assertProperties(result, test.checks, raw_res=True, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py index 077d8640e..88785f7a8 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py @@ -1,134 +1,74 @@ """Tests for dbStats across collection variants and data scenarios. -Covers empty collections (with and without a secondary index), positive -storage and index sizes for an indexed collection, avgObjSize when there -are no objects, total index counts across multiple collections, capped -collections, and object counts across a range of collection sizes and -document shapes. +Covers empty collections (with and without a secondary index), avgObjSize +when there are no objects, index counts across multiple collections, and +capped collections. """ -from dataclasses import dataclass, field - import pytest -from bson import Decimal128, Int64 +from bson import Int64 -from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +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_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Eq, Gt -from documentdb_tests.framework.target_collection import CappedCollection -from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.property_checks import Eq pytestmark = pytest.mark.admin -def test_dbStats_empty_collection_index_count(collection): - """Test an empty collection with a secondary index reports two indexes.""" - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"indexes": Int64(2)}, +# Each case establishes database state via ``setup`` runCommand documents, +# runs dbStats, then checks the database-level response. The per-test database +# is unique (provided by the ``collection`` fixture), so setup collections use +# fixed names; dbStats aggregates the whole database regardless of which +# collection it is issued against. +COLLECTION_SCENARIO_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="empty_collection_index_count", + setup=[{"createIndexes": "c1", "indexes": [{"key": {"a": 1}, "name": "a_1"}]}], + command={"dbStats": 1}, + checks={"indexes": Eq(Int64(2))}, msg="Empty collection with one secondary index should report indexes:2", - ) - - -def test_dbStats_empty_collection_sizes_positive(collection): - """Test an empty collection with an index reports positive storage and index sizes.""" - collection.create_index("a") - result = execute_command(collection, {"dbStats": 1}) - assertProperties( - result, - {"storageSize": Gt(0.0), "indexSize": Gt(0.0)}, - raw_res=True, - msg="Empty collection should still allocate storage and index space", - ) - - -def test_dbStats_avg_obj_size_zero_when_no_objects(collection): - """Test an empty collection reports zero objects and zero avgObjSize.""" - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"objects": Int64(0), "avgObjSize": 0.0}, - msg="Empty collection should report objects:0 and avgObjSize:0", - ) - - -def test_dbStats_total_index_count_across_collections(collection): - """Test indexes counts the default and secondary indexes across all collections.""" - collection.insert_many([{"_id": i, "a": i} for i in range(5)]) - collection.create_index("a") - c2 = collection.database[f"{collection.name}_c2"] - c2.insert_many([{"_id": i, "b": i} for i in range(5)]) - c2.create_index("b") - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"indexes": Int64(4)}, - msg="indexes should total default plus secondary indexes across collections", - ) - - -def test_dbStats_capped_collection_counted(collection): - """Test dbStats counts a capped collection and its documents.""" - capped = CappedCollection(size=4096).resolve(collection.database, collection) - capped.insert_many([{"_id": i} for i in range(3)]) - result = execute_command(capped, {"dbStats": 1}) - assertSuccessPartial( - result, - {"collections": Int64(1), "objects": Int64(3)}, - msg="Capped collection should be counted with its documents", - ) - - -@dataclass(frozen=True) -class ScenarioTestCase(BaseTestCase): - """A dbStats data scenario: documents to insert plus response checks.""" - - docs: list = field(default_factory=list) - checks: dict = field(default_factory=dict) - - -# Document scenarios covering varied collection sizes and shapes -# (stats_unified scenarios #32-#39). -SCENARIO_TESTS: list[ScenarioTestCase] = [ - ScenarioTestCase( - id="small", - docs=[{"_id": i, "a": i} for i in range(5)], - checks={"objects": Eq(Int64(5)), "storageSize": Gt(0.0)}, - msg="small collection should report objects:5 and positive storageSize", - ), - ScenarioTestCase( - id="medium_mixed_types", - docs=[{"_id": i, "a": Int64(i), "b": float(i), "c": Decimal128(str(i))} for i in range(50)], - checks={"objects": Eq(Int64(50)), "storageSize": Gt(0.0)}, - msg="mixed-type collection should report objects:50 and positive storageSize", ), - ScenarioTestCase( - id="large", - docs=[{"_id": i, "a": i} for i in range(500)], - checks={"objects": Eq(Int64(500)), "storageSize": Gt(0.0)}, - msg="large collection should report objects:500 and positive storageSize", + DiagnosticTestCase( + id="avg_obj_size_zero_when_no_objects", + command={"dbStats": 1}, + checks={"objects": Eq(Int64(0)), "avgObjSize": Eq(0.0)}, + msg="Empty database should report objects:0 and avgObjSize:0", ), - ScenarioTestCase( - id="toast_small", - docs=[{"_id": i, "blob": "x" * 4096} for i in range(5)], - checks={"objects": Eq(Int64(5)), "storageSize": Gt(0.0)}, - msg="toast small collection should report objects:5 and positive storageSize", + DiagnosticTestCase( + id="indexes_across_collections", + setup=[ + {"insert": "c1", "documents": [{"_id": i, "a": i} for i in range(5)]}, + {"createIndexes": "c1", "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + {"insert": "c2", "documents": [{"_id": i, "b": i} for i in range(5)]}, + {"createIndexes": "c2", "indexes": [{"key": {"b": 1}, "name": "b_1"}]}, + ], + command={"dbStats": 1}, + checks={"indexes": Eq(Int64(4))}, + msg="indexes should total default plus secondary indexes across collections", ), - ScenarioTestCase( - id="toast_large", - docs=[{"_id": i, "blob": "x" * 4096} for i in range(50)], - checks={"objects": Eq(Int64(50)), "storageSize": Gt(0.0)}, - msg="toast large collection should report objects:50 and positive storageSize", + DiagnosticTestCase( + id="capped_collection_counted", + setup=[ + {"create": "c1", "capped": True, "size": 4096}, + {"insert": "c1", "documents": [{"_id": i} for i in range(3)]}, + ], + command={"dbStats": 1}, + checks={"collections": Eq(Int64(1)), "objects": Eq(Int64(3))}, + msg="Capped collection should be counted with its documents", ), ] -@pytest.mark.parametrize("test", pytest_params(SCENARIO_TESTS)) -def test_dbStats_object_count_and_storage_across_scenarios(collection, test): - """Test objects and storageSize for varied collection sizes and document shapes.""" - collection.insert_many(test.docs) - result = execute_command(collection, {"dbStats": 1}) +@pytest.mark.parametrize("test", pytest_params(COLLECTION_SCENARIO_TESTS)) +def test_dbStats_collection_scenarios(collection, test): + """Test dbStats database-level counts and sizes across collection variants and data shapes.""" + for setup_command in test.setup: + setup_result = execute_command(collection, setup_command) + if isinstance(setup_result, Exception): + raise setup_result + result = execute_command(collection, test.command) assertProperties(result, test.checks, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py index a3cc9016d..21f7c2fe1 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py @@ -1,40 +1,64 @@ """Tests for dbStats command core behavior. Covers success on populated and empty databases, the all-zero response for -a non-existent database, execution against the admin database, and rejection -of unrecognized command fields. +a non-existent database, and execution against the admin database. +Command-level errors are in test_dbStats_errors.py. """ import pytest from bson import Int64 -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccessPartial -from documentdb_tests.framework.error_codes import UNRECOGNIZED_COMMAND_FIELD_ERROR -from documentdb_tests.framework.executor import execute_command +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties, assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, IsType from documentdb_tests.framework.target_collection import TargetDatabase pytestmark = pytest.mark.admin -def test_dbStats_populated_database_returns_ok(collection): - """Test dbStats returns ok:1 on a database that has collections.""" - collection.insert_many([{"_id": 0, "a": 1}, {"_id": 1, "a": 2}]) - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"ok": 1.0, "db": collection.database.name}, +# Success cases across populated, empty, and admin databases. The db name is +# checked by type (it varies per test/run); its exact value is asserted by the +# smoke test and by the admin case below. +SUCCESS_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="populated_database", + setup=[{"insert": "c1", "documents": [{"_id": 0, "a": 1}, {"_id": 1, "a": 2}]}], + command={"dbStats": 1}, + use_admin=False, + checks={"ok": Eq(1.0), "db": IsType("string")}, msg="Populated database should return ok:1", - ) + ), + DiagnosticTestCase( + id="empty_database", + command={"dbStats": 1}, + use_admin=False, + checks={"ok": Eq(1.0), "db": IsType("string"), "collections": Eq(Int64(0))}, + msg="Empty database should return ok:1 with zero collections", + ), + DiagnosticTestCase( + id="admin_database", + command={"dbStats": 1}, + use_admin=True, + checks={"ok": Eq(1.0), "db": Eq("admin")}, + msg="dbStats on admin database should report db:admin", + ), +] -def test_dbStats_empty_database_returns_ok(collection): - """Test dbStats returns ok:1 with zero collections on an empty database.""" - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - {"ok": 1.0, "db": collection.database.name, "collections": Int64(0)}, - msg="Empty database should return ok:1 with zero collections", - ) +@pytest.mark.parametrize("test", pytest_params(SUCCESS_TESTS)) +def test_dbStats_core_behavior(collection, test): + """Test dbStats succeeds and reports expected top-level fields across databases.""" + for setup_command in test.setup: + setup_result = execute_command(collection, setup_command) + if isinstance(setup_result, Exception): + raise setup_result + executor = execute_admin_command if test.use_admin else execute_command + result = executor(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) def test_dbStats_nonexistent_database_returns_zeros(collection, register_db_cleanup): @@ -55,25 +79,3 @@ def test_dbStats_nonexistent_database_returns_zeros(collection, register_db_clea }, msg="Non-existent database should report all counts and sizes as zero", ) - - -def test_dbStats_admin_database_reports_admin_name(collection): - """Test dbStats executed against the admin database reports db:admin.""" - admin_coll = collection.database.client["admin"]["unused"] - result = execute_command(admin_coll, {"dbStats": 1}) - assertSuccessPartial( - result, - {"ok": 1.0, "db": "admin"}, - msg="dbStats on admin database should report db:admin", - ) - - -def test_dbStats_unrecognized_field_errors(collection): - """Test dbStats rejects an unrecognized command field.""" - collection.insert_one({"_id": 1}) - result = execute_command(collection, {"dbStats": 1, "bogusField": 1}) - assertFailureCode( - result, - UNRECOGNIZED_COMMAND_FIELD_ERROR, - msg="Unrecognized command field should error with code 40415", - ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py new file mode 100644 index 000000000..04eea2765 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py @@ -0,0 +1,68 @@ +"""Tests for dbStats command error conditions. + +Covers value-level errors (BadValue for invalid scale values) and +command-level errors (unrecognized fields). Type-level rejections +(TypeMismatch for invalid scale types) are in test_dbStats_argument_handling.py. +""" + +import pytest +from bson import SON + +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 BAD_VALUE_ERROR, UNRECOGNIZED_COMMAND_FIELD_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.admin + + +# Non-positive or truncate-to-zero values of otherwise-valid numeric types are +# rejected with BadValue (code 2). Type-level rejections (TypeMismatch) are +# covered in test_dbStats_argument_handling.py. +INVALID_SCALE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "zero", + command={"dbStats": 1, "scale": 0}, + error_code=BAD_VALUE_ERROR, + msg="scale=0 should error with BadValue", + ), + DiagnosticTestCase( + "negative_int", + command={"dbStats": 1, "scale": -1}, + error_code=BAD_VALUE_ERROR, + msg="Negative int scale should error with BadValue", + ), + DiagnosticTestCase( + "fractional_lt_1", + command={"dbStats": 1, "scale": 0.5}, + error_code=BAD_VALUE_ERROR, + msg="Fractional scale < 1 should error with BadValue", + ), + DiagnosticTestCase( + "duplicate_keys_last_invalid", + command=SON([("dbStats", 1), ("scale", 1024), ("scale", -1)]), + error_code=BAD_VALUE_ERROR, + msg="Invalid last duplicate scale value should error", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_SCALE_TESTS)) +def test_dbStats_invalid_scale_errors(collection, test): + """Test dbStats rejects invalid (non-positive/truncate-to-zero) scale values with BadValue.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) + + +def test_dbStats_unrecognized_field_errors(collection): + """Test dbStats rejects an unrecognized command field.""" + collection.insert_one({"_id": 1}) + result = execute_command(collection, {"dbStats": 1, "bogusField": 1}) + assertFailureCode( + result, + UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="Unrecognized command field should error with code 40415", + ) 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 546d73093..ce52f4acd 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 @@ -11,15 +11,10 @@ from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( DiagnosticTestCase, ) -from documentdb_tests.framework.assertions import ( - assertProperties, - assertSuccess, - assertSuccessPartial, -) +from documentdb_tests.framework.assertions import assertProperties, assertSuccess from documentdb_tests.framework.executor import execute_command from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.property_checks import Gt, IsType -from documentdb_tests.framework.target_collection import ViewCollection +from documentdb_tests.framework.property_checks import Eq, Gt, IsType pytestmark = pytest.mark.admin @@ -121,18 +116,6 @@ def test_dbStats_total_size_relationship(collection): ) -def test_dbStats_data_size_positive_after_insert(collection): - """Test dataSize is greater than zero after inserting documents.""" - collection.insert_many([{"_id": i, "data": "x" * 50} for i in range(10)]) - result = execute_command(collection, {"dbStats": 1}) - assertProperties( - result, - {"dataSize": Gt(0.0)}, - raw_res=True, - msg="dataSize should be positive after inserts", - ) - - 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)]) @@ -145,27 +128,44 @@ def test_dbStats_avg_obj_size_equals_data_size_over_objects(collection): ) -def test_dbStats_collections_count_includes_system_views(collection): - """Test collections count includes system.views after a view is created.""" - collection.insert_many([{"_id": i} for i in range(3)]) - collection.database.command( - "create", f"{collection.name}_view", viewOn=collection.name, pipeline=[] - ) - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - expected={"collections": Int64(2)}, +# Cases asserting response fields reflect database state established via setup. +RESPONSE_STATE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + id="data_size_positive_after_insert", + setup=[{"insert": "c1", "documents": [{"_id": i, "data": "x" * 50} for i in range(10)]}], + command={"dbStats": 1}, + checks={"dataSize": Gt(0.0)}, + msg="dataSize should be positive after inserts", + ), + DiagnosticTestCase( + id="collections_count_includes_system_views", + setup=[ + {"insert": "c1", "documents": [{"_id": i} for i in range(3)]}, + {"create": "c1_view", "viewOn": "c1", "pipeline": []}, + ], + command={"dbStats": 1}, + checks={"collections": Eq(Int64(2))}, msg="collections should include the base collection and system.views", - ) + ), + DiagnosticTestCase( + id="views_count", + setup=[ + {"insert": "c1", "documents": [{"_id": i} for i in range(3)]}, + {"create": "c1_view", "viewOn": "c1", "pipeline": []}, + ], + command={"dbStats": 1}, + checks={"views": Eq(Int64(1))}, + msg="views should count the created view", + ), +] -def test_dbStats_views_count(collection): - """Test the views field counts created views.""" - collection.insert_many([{"_id": i} for i in range(3)]) - ViewCollection().resolve(collection.database, collection) - result = execute_command(collection, {"dbStats": 1}) - assertSuccessPartial( - result, - expected={"views": Int64(1)}, - msg="views should count the created view", - ) +@pytest.mark.parametrize("test", pytest_params(RESPONSE_STATE_TESTS)) +def test_dbStats_response_reflects_state(collection, test): + """Test dbStats response fields reflect database state from setup commands.""" + for setup_command in test.setup: + setup_result = execute_command(collection, setup_command) + if isinstance(setup_result, Exception): + raise setup_result + result = execute_command(collection, test.command) + assertProperties(result, 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) From 24f031e7e1b1a3462c29f83e0c1b343befb49de8 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 18 Jun 2026 09:54:01 -0700 Subject: [PATCH 4/5] Removed comments Signed-off-by: PatersonProjects --- .../commands/dbStats/test_dbStats_collection_scenarios.py | 5 ----- .../commands/dbStats/test_dbStats_core_behavior.py | 3 --- .../diagnostic/commands/dbStats/test_dbStats_errors.py | 3 --- .../commands/dbStats/test_dbStats_response_structure.py | 2 -- 4 files changed, 13 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py index 88785f7a8..e631c837e 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py @@ -19,11 +19,6 @@ pytestmark = pytest.mark.admin -# Each case establishes database state via ``setup`` runCommand documents, -# runs dbStats, then checks the database-level response. The per-test database -# is unique (provided by the ``collection`` fixture), so setup collections use -# fixed names; dbStats aggregates the whole database regardless of which -# collection it is issued against. COLLECTION_SCENARIO_TESTS: list[DiagnosticTestCase] = [ DiagnosticTestCase( id="empty_collection_index_count", diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py index 21f7c2fe1..3d75e21e6 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py @@ -20,9 +20,6 @@ pytestmark = pytest.mark.admin -# Success cases across populated, empty, and admin databases. The db name is -# checked by type (it varies per test/run); its exact value is asserted by the -# smoke test and by the admin case below. SUCCESS_TESTS: list[DiagnosticTestCase] = [ DiagnosticTestCase( id="populated_database", diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py index 04eea2765..05beb2174 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py @@ -19,9 +19,6 @@ pytestmark = pytest.mark.admin -# Non-positive or truncate-to-zero values of otherwise-valid numeric types are -# rejected with BadValue (code 2). Type-level rejections (TypeMismatch) are -# covered in test_dbStats_argument_handling.py. INVALID_SCALE_TESTS: list[DiagnosticTestCase] = [ DiagnosticTestCase( "zero", 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 ce52f4acd..fc292c1ea 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 @@ -19,7 +19,6 @@ pytestmark = pytest.mark.admin -# One DiagnosticTestCase per documented response field, following the buildInfo pattern. RESPONSE_PROPERTY_TESTS: list[DiagnosticTestCase] = [ DiagnosticTestCase( id="db_is_string", @@ -128,7 +127,6 @@ def test_dbStats_avg_obj_size_equals_data_size_over_objects(collection): ) -# Cases asserting response fields reflect database state established via setup. RESPONSE_STATE_TESTS: list[DiagnosticTestCase] = [ DiagnosticTestCase( id="data_size_positive_after_insert", From 59e95c97d5ce6f4c533d64ea5313d85cd371f26d Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 18 Jun 2026 10:23:18 -0700 Subject: [PATCH 5/5] Fixed failing test Signed-off-by: PatersonProjects --- .../commands/dbStats/test_dbStats_argument_handling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py index 876750ffb..b99d73704 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py @@ -95,7 +95,7 @@ def test_dbStats_accepts_value_edge_cases(collection, test): keyword="scale", valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL, BsonType.NULL], default_error_code=TYPE_MISMATCH_ERROR, - valid_inputs={BsonType.DECIMAL: Decimal128("1024")}, + valid_inputs={BsonType.DECIMAL: Decimal128("1024"), BsonType.LONG: Int64(1024)}, ), ]