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..52f7bbb6c --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_accuracy.py @@ -0,0 +1,69 @@ +"""Tests for dbStats accuracy and state changes. + +Covers count fields (collections, objects, indexes) reflecting database +state. +""" + +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 +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq + +pytestmark = pytest.mark.admin + + +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", + ), + 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", + ), + 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", + ), +] + + +@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 new file mode 100644 index 000000000..b99d73704 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_argument_handling.py @@ -0,0 +1,238 @@ +"""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. + +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 +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 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 +from documentdb_tests.framework.test_constants import FLOAT_INFINITY, BsonType + +pytestmark = pytest.mark.admin + + +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), + ), +] + +VALUE_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(DBSTATS_VALUE_PARAMS) + + +@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}) + assertSuccessPartial( + result, + {"ok": 1.0, "db": collection.database.name}, + msg=f"dbStats should accept {bson_type.value} for the command field value", + ) + + +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) + + +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"), BsonType.LONG: Int64(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) + + +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) + + +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("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) + + +@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) + + +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(), + }, + msg="freeStorage:1 should include free-storage fields", + ), + DiagnosticTestCase( + "no_free_storage_param", + setup=[{"insert": "c1", "documents": [{"_id": 1}]}], + command={"dbStats": 1}, + checks={ + "freeStorageSize": NotExists(), + "indexFreeStorageSize": NotExists(), + "totalFreeStorageSize": NotExists(), + }, + msg="Omitting freeStorage should omit free-storage fields", + ), + DiagnosticTestCase( + "free_storage_zero", + setup=[{"insert": "c1", "documents": [{"_id": 1}]}], + 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(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 new file mode 100644 index 000000000..e631c837e --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_collection_scenarios.py @@ -0,0 +1,69 @@ +"""Tests for dbStats across collection variants and data scenarios. + +Covers empty collections (with and without a secondary index), avgObjSize +when there are no objects, index counts across multiple collections, and +capped collections. +""" + +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 +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq + +pytestmark = pytest.mark.admin + + +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", + ), + 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", + ), + 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", + ), + 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(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 new file mode 100644 index 000000000..3d75e21e6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_core_behavior.py @@ -0,0 +1,78 @@ +"""Tests for dbStats command core behavior. + +Covers success on populated and empty databases, the all-zero response for +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.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 + + +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", + ), +] + + +@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): + """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", + ) 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..05beb2174 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_errors.py @@ -0,0 +1,65 @@ +"""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 + + +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 new file mode 100644 index 000000000..fc292c1ea --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/dbStats/test_dbStats_response_structure.py @@ -0,0 +1,169 @@ +"""Tests for the dbStats response structure. + +Covers the presence and BSON type of every documented response field, the +totalSize relationship, dataSize positivity after inserts, 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 +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, IsType + +pytestmark = pytest.mark.admin + + +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.get("totalSize"), + expected=result.get("storageSize") + result.get("indexSize"), + raw_res=True, + msg="totalSize should equal storageSize + indexSize", + ) + + +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.get("avgObjSize"), + expected=result.get("dataSize") / result.get("objects"), + raw_res=True, + msg="avgObjSize should equal dataSize / objects", + ) + + +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", + ), +] + + +@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)