diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/__init__.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_combined_params.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_combined_params.py new file mode 100644 index 000000000..617f0a126 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_combined_params.py @@ -0,0 +1,92 @@ +"""Tests for profile command with multiple parameters and cross-database behavior. + +Validates setting multiple parameters simultaneously and cross-database +behavior. All tests in this file verify success cases only. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Exists, IsType + +pytestmark = [pytest.mark.no_parallel] + +# Property [Multi-Parameter Set]: setting level, slowms, sampleRate, and +# filter simultaneously applies all values. +MULTI_PARAM_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "level_slowms_samplerate", + setup=[{"profile": 1, "slowms": 50, "sampleRate": 0.5}], + command={"profile": -1}, + use_admin=False, + checks={"was": Eq(1), "slowms": Eq(50), "sampleRate": Eq(0.5)}, + msg="profile should apply level, slowms, and sampleRate together", + ), + DiagnosticTestCase( + "level_slowms_filter", + setup=[{"profile": 1, "slowms": 50, "filter": {"op": "query"}}], + command={"profile": -1}, + use_admin=False, + checks={"was": Eq(1), "slowms": Eq(50), "filter": Exists()}, + msg="profile should apply level, slowms, and filter together", + ), + DiagnosticTestCase( + "params_persist_when_profiling_disabled", + setup=[{"profile": 0, "slowms": 200, "sampleRate": 0.8}], + command={"profile": -1}, + use_admin=False, + checks={"was": Eq(0), "slowms": Eq(200), "sampleRate": Eq(0.8)}, + msg="profile should apply slowms and sampleRate even when profiler is off", + ), +] + +# Property [Database Scope]: profile operates on both regular and admin +# databases and returns the full response structure. +DATABASE_SCOPE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "regular_database", + command={"profile": -1}, + use_admin=False, + checks={ + "was": IsType("int"), + "slowms": IsType("int"), + "sampleRate": IsType("double"), + "ok": Eq(1.0), + }, + msg="profile should return full response on a regular database", + ), + DiagnosticTestCase( + "admin_database", + command={"profile": -1}, + use_admin=True, + checks={ + "was": IsType("int"), + "slowms": IsType("int"), + "sampleRate": IsType("double"), + "ok": Eq(1.0), + }, + msg="profile should return full response on the admin database", + ), +] + +COMBINED_TESTS = MULTI_PARAM_TESTS + DATABASE_SCOPE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(COMBINED_TESTS)) +def test_profile_combined_params(collection, test): + """Test profile command with multiple parameters and database scope.""" + for cmd in test.setup: + execute_command(collection, cmd) + if test.use_admin: + result = execute_admin_command(collection, test.command) + else: + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0, "sampleRate": 1.0, "filter": "unset"}) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_core_behavior.py new file mode 100644 index 000000000..cd43223fb --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_core_behavior.py @@ -0,0 +1,177 @@ +"""Tests for profile command core behavior. + +Validates profiling level transitions, the 'was' field, out-of-range levels, +fractional level read-back, idempotency, and level persistence via read. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq + +pytestmark = [pytest.mark.no_parallel] + +# Property [Was Field Transitions]: the 'was' field returns the profiling +# level that was active before the current command. +WAS_TRANSITION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "was_0_to_1", + setup=[{"profile": 0}], + command={"profile": 1}, + checks={"was": Eq(0)}, + msg="'was' should be 0 after transition 0->1", + ), + DiagnosticTestCase( + "was_1_to_2", + setup=[{"profile": 1}], + command={"profile": 2}, + checks={"was": Eq(1)}, + msg="'was' should be 1 after transition 1->2", + ), + DiagnosticTestCase( + "was_2_to_0", + setup=[{"profile": 2}], + command={"profile": 0}, + checks={"was": Eq(2)}, + msg="'was' should be 2 after transition 2->0", + ), + DiagnosticTestCase( + "was_0_to_0", + setup=[{"profile": 0}], + command={"profile": 0}, + checks={"was": Eq(0)}, + msg="'was' should be 0 after transition 0->0", + ), +] + +# Property [Level Persistence via Read]: reading with -1 returns the current +# level in the 'was' field. +LEVEL_READ_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "read_level_0", + setup=[{"profile": 0}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="'was' should be 0 when reading at level 0", + ), + DiagnosticTestCase( + "read_level_1", + setup=[{"profile": 1}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="'was' should be 1 when reading at level 1", + ), + DiagnosticTestCase( + "read_level_2", + setup=[{"profile": 2}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="'was' should be 2 when reading at level 2", + ), +] + +# Property [Out-of-Range Levels]: integer levels outside {-1, 0, 1, 2} +# succeed but do not change the profiling level. +OUT_OF_RANGE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "out_of_range_3", + setup=[{"profile": 1}, {"profile": 3}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile level 3 should be a no-op", + ), + DiagnosticTestCase( + "out_of_range_neg2", + setup=[{"profile": 2}, {"profile": -2}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="profile level -2 should be a no-op", + ), + DiagnosticTestCase( + "out_of_range_100", + setup=[{"profile": 0}, {"profile": 100}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="profile level 100 should be a no-op", + ), +] + +# Property [Fractional Level Read-Back]: after setting a fractional level, +# reading with -1 returns the truncated integer level. +FRACTIONAL_READBACK_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "readback_1_5", + setup=[{"profile": 0}, {"profile": 1.5}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile 1.5 should read back as level 1", + ), + DiagnosticTestCase( + "readback_0_5", + setup=[{"profile": 0}, {"profile": 0.5}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="profile 0.5 should read back as level 0", + ), + DiagnosticTestCase( + "readback_2_9", + setup=[{"profile": 0}, {"profile": 2.9}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="profile 2.9 should read back as level 2", + ), +] + +# Property [Idempotency]: repeated identical profile commands produce +# consistent results. +IDEMPOTENCY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "idempotent_set_0", + setup=[{"profile": 0}], + command={"profile": 0}, + checks={"was": Eq(0), "ok": Eq(1.0)}, + msg="Second profile 0 should show was=0", + ), +] + +CORE_BEHAVIOR_TESTS = ( + WAS_TRANSITION_TESTS + + LEVEL_READ_TESTS + + OUT_OF_RANGE_TESTS + + FRACTIONAL_READBACK_TESTS + + IDEMPOTENCY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(CORE_BEHAVIOR_TESTS)) +def test_profile_core_behavior(collection, test): + """Test profile command core behavior.""" + for cmd in test.setup: + execute_command(collection, cmd) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0}) + + +def test_profile_idempotent_read(collection): + """Test running profile -1 twice returns identical results.""" + execute_command(collection, {"profile": 0}) + result1 = execute_command(collection, {"profile": -1}) + result2 = execute_command(collection, {"profile": -1}) + assertProperties( + result2, + { + "was": Eq(result1["was"]), + "slowms": Eq(result1["slowms"]), + "sampleRate": Eq(result1["sampleRate"]), + }, + msg="Two consecutive reads should return identical results", + raw_res=True, + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_errors.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_errors.py new file mode 100644 index 000000000..5ec5b8409 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_errors.py @@ -0,0 +1,145 @@ +"""Tests for profile command validation error cases. + +Validates sampleRate range rejection, filter value rejection, unrecognized +field rejection, and case-sensitive command name rejection. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + COMMAND_NOT_FOUND_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [sampleRate Range Rejection]: sampleRate values outside [0, 1] +# are rejected with BAD_VALUE_ERROR. +SAMPLERATE_RANGE_REJECTION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "samplerate_neg_0_1", + command={"profile": 0, "sampleRate": -0.1}, + error_code=BAD_VALUE_ERROR, + msg="sampleRate should reject -0.1", + ), + DiagnosticTestCase( + "samplerate_1_1", + command={"profile": 0, "sampleRate": 1.1}, + error_code=BAD_VALUE_ERROR, + msg="sampleRate should reject 1.1", + ), + DiagnosticTestCase( + "samplerate_2_0", + command={"profile": 0, "sampleRate": 2.0}, + error_code=BAD_VALUE_ERROR, + msg="sampleRate should reject 2.0", + ), + DiagnosticTestCase( + "samplerate_neg_1_0", + command={"profile": 0, "sampleRate": -1.0}, + error_code=BAD_VALUE_ERROR, + msg="sampleRate should reject -1.0", + ), +] + +# Property [filter Type Rejection]: non-object, non-"unset" values for the +# filter field are rejected with BAD_VALUE_ERROR. +FILTER_REJECTION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "filter_string_UNSET", + command={"profile": 1, "filter": "UNSET"}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject 'UNSET' (case-sensitive)", + ), + DiagnosticTestCase( + "filter_string_Unset", + command={"profile": 1, "filter": "Unset"}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject 'Unset' (case-sensitive)", + ), + DiagnosticTestCase( + "filter_empty_string", + command={"profile": 1, "filter": ""}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject empty string", + ), + DiagnosticTestCase( + "filter_string_hello", + command={"profile": 1, "filter": "hello"}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject arbitrary string", + ), + DiagnosticTestCase( + "filter_int", + command={"profile": 1, "filter": 1}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject int", + ), + DiagnosticTestCase( + "filter_bool", + command={"profile": 1, "filter": True}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject bool", + ), + DiagnosticTestCase( + "filter_null", + command={"profile": 1, "filter": None}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject null", + ), + DiagnosticTestCase( + "filter_array", + command={"profile": 1, "filter": []}, + error_code=BAD_VALUE_ERROR, + msg="filter should reject array", + ), +] + +# Property [Unrecognized Fields]: unrecognized fields in the command document +# are rejected with UNRECOGNIZED_COMMAND_FIELD_ERROR. +UNRECOGNIZED_FIELD_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "unrecognized_unknownField", + command={"profile": 0, "unknownField": 1}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="profile should reject unrecognized field 'unknownField'", + ), + DiagnosticTestCase( + "unrecognized_extraParam", + command={"profile": 0, "extraParam": "value"}, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="profile should reject unrecognized field 'extraParam'", + ), +] + +# Property [Case-Sensitive Command Name]: the command name is case-sensitive; +# mismatched case produces COMMAND_NOT_FOUND_ERROR. +CASE_SENSITIVITY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "case_Profile", + command={"Profile": 0}, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="'Profile' (capital P) should not be recognized", + ), +] + +PROFILE_ERROR_TESTS = ( + SAMPLERATE_RANGE_REJECTION_TESTS + + FILTER_REJECTION_TESTS + + UNRECOGNIZED_FIELD_TESTS + + CASE_SENSITIVITY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(PROFILE_ERROR_TESTS)) +def test_profile_errors(collection, test): + """Test profile command rejects invalid inputs with correct error codes.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_filter.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_filter.py new file mode 100644 index 000000000..4e7a607e0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_filter.py @@ -0,0 +1,88 @@ +"""Tests for profile command filter parameter. + +Validates filter object acceptance, the special "unset" string, filter +normalization, and the set/unset lifecycle. All tests in this file verify +success cases only. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Exists, NotExists + +pytestmark = [pytest.mark.no_parallel] + +# Property [Filter Object Acceptance]: the filter field accepts object values +# and the filter is actually applied. +FILTER_ACCEPT_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "empty_object", + setup=[{"profile": 1, "filter": {}}], + command={"profile": -1}, + checks={"filter": Exists()}, + msg="filter should accept an empty object and be visible", + ), + DiagnosticTestCase( + "op_query", + setup=[{"profile": 1, "filter": {"op": "query"}}], + command={"profile": -1}, + checks={"filter": Exists()}, + msg="filter should accept {op: 'query'} and be visible", + ), + DiagnosticTestCase( + "op_in", + setup=[{"profile": 1, "filter": {"op": {"$in": ["query", "command"]}}}], + command={"profile": -1}, + checks={"filter": Exists()}, + msg="filter should accept $in expression and be visible", + ), + DiagnosticTestCase( + "ns_filter", + setup=[{"profile": 1, "filter": {"ns": "test.users"}}], + command={"profile": -1}, + checks={"filter": Exists()}, + msg="filter should accept namespace filter and be visible", + ), + DiagnosticTestCase( + "unset_removes_filter", + setup=[ + {"profile": 1, "filter": {"op": "query"}}, + {"profile": 1, "filter": "unset"}, + ], + command={"profile": -1}, + checks={"filter": NotExists()}, + msg="filter should accept 'unset' and remove the filter", + ), +] + + +# Property [Filter Normalization]: simple equality in the filter is normalized +# to $eq form in the response. +FILTER_NORMALIZATION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "normalize_eq", + setup=[{"profile": 1, "filter": {"op": "query"}}], + command={"profile": -1}, + checks={"filter": Eq({"op": {"$eq": "query"}})}, + msg="filter should normalize {op: 'query'} to {op: {$eq: 'query'}}", + ), +] + +FILTER_TESTS = FILTER_ACCEPT_TESTS + FILTER_NORMALIZATION_TESTS + + +@pytest.mark.parametrize("test", pytest_params(FILTER_TESTS)) +def test_profile_filter(collection, test): + """Test profile filter parameter behavior.""" + for cmd in test.setup: + execute_command(collection, cmd) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0, "filter": "unset"}) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_response_structure.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_response_structure.py new file mode 100644 index 000000000..174459877 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_response_structure.py @@ -0,0 +1,80 @@ +"""Tests for profile command response structure. + +Validates presence, types, and values of response fields returned by the +profile command, including base fields and filter-related fields. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Exists, IsType, NotExists + +pytestmark = [pytest.mark.no_parallel] + +# Property [Base Response Fields]: every successful profile command response +# includes was (int), slowms (int), sampleRate (double), and ok (1.0). +RESPONSE_BASE_FIELD_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "base_fields_at_level_neg1", + command={"profile": -1}, + checks={ + "was": IsType("int"), + "slowms": IsType("int"), + "sampleRate": IsType("double"), + "ok": Eq(1.0), + }, + msg="profile -1 response should include was, slowms, sampleRate, and ok", + ), + DiagnosticTestCase( + "base_fields_at_level_0", + command={"profile": 0}, + checks={ + "was": IsType("int"), + "slowms": IsType("int"), + "sampleRate": IsType("double"), + "ok": Eq(1.0), + }, + msg="profile 0 response should include was, slowms, sampleRate, and ok", + ), +] + +# Property [Filter Response Fields]: when a filter is active the response +# includes filter and note; after unset they are absent. +RESPONSE_FILTER_FIELD_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "filter_fields_present", + setup=[{"profile": 1, "filter": {"op": "query"}}], + command={"profile": -1}, + checks={"filter": Exists(), "note": IsType("string")}, + msg="profile response should include filter and note when a filter is active", + ), + DiagnosticTestCase( + "filter_fields_absent_after_unset", + setup=[ + {"profile": 1, "filter": {"op": "query"}}, + {"profile": 1, "filter": "unset"}, + ], + command={"profile": -1}, + checks={"filter": NotExists(), "note": NotExists()}, + msg="profile response should exclude filter and note after filter is unset", + ), +] + +RESPONSE_STRUCTURE_TESTS = RESPONSE_BASE_FIELD_TESTS + RESPONSE_FILTER_FIELD_TESTS + + +@pytest.mark.parametrize("test", pytest_params(RESPONSE_STRUCTURE_TESTS)) +def test_profile_response_structure(collection, test): + """Test profile response field presence and types.""" + for cmd in test.setup: + execute_command(collection, cmd) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0, "filter": "unset"}) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_samplerate.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_samplerate.py new file mode 100644 index 000000000..930e4c36e --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_samplerate.py @@ -0,0 +1,91 @@ +"""Tests for profile command sampleRate parameter. + +Validates sampleRate value persistence, boundary values, and null no-op behavior. +All tests in this file verify success cases only. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq + +pytestmark = [pytest.mark.no_parallel] + +# Property [sampleRate Persistence]: setting sampleRate persists the value +# for subsequent reads. +SAMPLERATE_PERSISTENCE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "persist_0_5", + setup=[{"profile": 0, "sampleRate": 0.5}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.5)}, + msg="sampleRate should persist value 0.5", + ), + DiagnosticTestCase( + "persist_boundary_zero", + setup=[{"profile": 0, "sampleRate": 0.0}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.0)}, + msg="sampleRate should persist value 0.0", + ), + DiagnosticTestCase( + "persist_1_0", + setup=[{"profile": 0, "sampleRate": 1.0}], + command={"profile": -1}, + checks={"sampleRate": Eq(1.0)}, + msg="sampleRate should persist value 1.0", + ), +] + +# Property [sampleRate Boundary Acceptance]: sampleRate accepts boundary +# values within [0, 1] using int and double types. +SAMPLERATE_BOUNDARY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "boundary_int_0", + setup=[{"profile": 0, "sampleRate": 0}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.0)}, + msg="sampleRate should accept int 0", + ), + DiagnosticTestCase( + "boundary_int_1", + setup=[{"profile": 0, "sampleRate": 1}], + command={"profile": -1}, + checks={"sampleRate": Eq(1.0)}, + msg="sampleRate should accept int 1", + ), +] + +# Property [sampleRate Null No-Op]: setting sampleRate to null does not +# change the current value. +SAMPLERATE_NULL_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "null_noop", + setup=[ + {"profile": 0, "sampleRate": 0.5}, + {"profile": 0, "sampleRate": None}, + ], + command={"profile": -1}, + checks={"sampleRate": Eq(0.5)}, + msg="sampleRate should remain 0.5 after setting null", + ), +] + +SAMPLERATE_TESTS = SAMPLERATE_PERSISTENCE_TESTS + SAMPLERATE_BOUNDARY_TESTS + SAMPLERATE_NULL_TESTS + + +@pytest.mark.parametrize("test", pytest_params(SAMPLERATE_TESTS)) +def test_profile_samplerate(collection, test): + """Test profile sampleRate parameter behavior.""" + for cmd in test.setup: + execute_command(collection, cmd) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0, "sampleRate": 1.0}) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_slowms.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_slowms.py new file mode 100644 index 000000000..9d917ac9b --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_slowms.py @@ -0,0 +1,99 @@ +"""Tests for profile command slowms parameter. + +Validates slowms value persistence, boundary values, and null no-op behavior. +All tests in this file verify success cases only. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.system.diagnostic.utils.diagnostic_test_case import ( + DiagnosticTestCase, +) +from documentdb_tests.framework.assertions import assertProperties +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MIN + +pytestmark = [pytest.mark.no_parallel] + +# Property [slowms Persistence]: setting slowms persists the value for +# subsequent reads. +SLOWMS_PERSISTENCE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "persist_200", + setup=[{"profile": 0, "slowms": 200}], + command={"profile": -1}, + checks={"slowms": Eq(200)}, + msg="slowms should persist value 200", + ), + DiagnosticTestCase( + "persist_0", + setup=[{"profile": 0, "slowms": 0}], + command={"profile": -1}, + checks={"slowms": Eq(0)}, + msg="slowms should persist value 0", + ), + DiagnosticTestCase( + "persist_negative_value", + setup=[{"profile": 0, "slowms": -1}], + command={"profile": -1}, + checks={"slowms": Eq(-1)}, + msg="slowms should persist negative value -1", + ), +] + +# Property [slowms Boundary Values]: slowms accepts the full int32 range +# including negative values. +SLOWMS_BOUNDARY_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "boundary_int32_max", + setup=[{"profile": 0, "slowms": INT32_MAX}], + command={"profile": -1}, + checks={"slowms": Eq(INT32_MAX)}, + msg="slowms should accept INT32_MAX", + ), + DiagnosticTestCase( + "boundary_int32_min", + setup=[{"profile": 0, "slowms": INT32_MIN}], + command={"profile": -1}, + checks={"slowms": Eq(INT32_MIN)}, + msg="slowms should accept INT32_MIN", + ), + DiagnosticTestCase( + "boundary_neg100", + setup=[{"profile": 0, "slowms": -100}], + command={"profile": -1}, + checks={"slowms": Eq(-100)}, + msg="slowms should accept -100", + ), +] + +# Property [slowms Null No-Op]: setting slowms to null does not change +# the current value. +SLOWMS_NULL_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "null_noop", + setup=[ + {"profile": 0, "slowms": 500}, + {"profile": 0, "slowms": None}, + ], + command={"profile": -1}, + checks={"slowms": Eq(500)}, + msg="slowms should remain 500 after setting null", + ), +] + +SLOWMS_TESTS = SLOWMS_PERSISTENCE_TESTS + SLOWMS_BOUNDARY_TESTS + SLOWMS_NULL_TESTS + + +@pytest.mark.parametrize("test", pytest_params(SLOWMS_TESTS)) +def test_profile_slowms(collection, test): + """Test profile slowms parameter behavior.""" + for cmd in test.setup: + execute_command(collection, cmd) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0, "slowms": 100}) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_type_acceptance.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_type_acceptance.py new file mode 100644 index 000000000..cbbbb4983 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_type_acceptance.py @@ -0,0 +1,245 @@ +"""Tests for profile command type acceptance. + +Validates that the profile, slowms, and sampleRate fields accept all valid +numeric BSON types and that the values are actually applied. +""" + +from __future__ import annotations + +import pytest +from bson import Decimal128, 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, Gte + +pytestmark = [pytest.mark.no_parallel] + +# Property [Profile Field Type Acceptance]: the profile field accepts int, +# Int64, double, and Decimal128 for valid level values, and the level is +# actually applied. +PROFILE_TYPE_ACCEPTANCE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "profile_int_0", + setup=[{"profile": 1}, {"profile": 0}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="profile should accept int 0 and apply level", + ), + DiagnosticTestCase( + "profile_int_1", + setup=[{"profile": 0}, {"profile": 1}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile should accept int 1 and apply level", + ), + DiagnosticTestCase( + "profile_int_2", + setup=[{"profile": 0}, {"profile": 2}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="profile should accept int 2 and apply level", + ), + DiagnosticTestCase( + "profile_int_neg1", + setup=[{"profile": 1}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile should accept int -1 and read current level", + ), + DiagnosticTestCase( + "profile_int64_0", + setup=[{"profile": 1}, {"profile": Int64(0)}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="profile should accept Int64(0) and apply level", + ), + DiagnosticTestCase( + "profile_int64_1", + setup=[{"profile": 0}, {"profile": Int64(1)}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile should accept Int64(1) and apply level", + ), + DiagnosticTestCase( + "profile_int64_2", + setup=[{"profile": 0}, {"profile": Int64(2)}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="profile should accept Int64(2) and apply level", + ), + DiagnosticTestCase( + "profile_double_0", + setup=[{"profile": 1}, {"profile": 0.0}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="profile should accept double 0.0 and apply level", + ), + DiagnosticTestCase( + "profile_double_1", + setup=[{"profile": 0}, {"profile": 1.0}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile should accept double 1.0 and apply level", + ), + DiagnosticTestCase( + "profile_double_2", + setup=[{"profile": 0}, {"profile": 2.0}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="profile should accept double 2.0 and apply level", + ), + DiagnosticTestCase( + "profile_double_neg1", + setup=[{"profile": 1}], + command={"profile": -1.0}, + checks={"was": Eq(1)}, + msg="profile should accept double -1.0 and read current level", + ), + DiagnosticTestCase( + "profile_decimal128_0", + setup=[{"profile": 1}, {"profile": Decimal128("0")}], + command={"profile": -1}, + checks={"was": Eq(0)}, + msg="profile should accept Decimal128('0') and apply level", + ), + DiagnosticTestCase( + "profile_decimal128_1", + setup=[{"profile": 0}, {"profile": Decimal128("1")}], + command={"profile": -1}, + checks={"was": Eq(1)}, + msg="profile should accept Decimal128('1') and apply level", + ), + DiagnosticTestCase( + "profile_decimal128_2", + setup=[{"profile": 0}, {"profile": Decimal128("2")}], + command={"profile": -1}, + checks={"was": Eq(2)}, + msg="profile should accept Decimal128('2') and apply level", + ), +] + +# Property [slowms Type Acceptance]: the slowms field accepts numeric BSON +# types (int, Int64, double, Decimal128) and null, and the value is applied. +SLOWMS_TYPE_ACCEPTANCE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "slowms_int_100", + setup=[{"profile": 0, "slowms": 100}], + command={"profile": -1}, + checks={"slowms": Eq(100)}, + msg="slowms should accept int 100 and persist value", + ), + DiagnosticTestCase( + "slowms_int64_100", + setup=[{"profile": 0, "slowms": Int64(100)}], + command={"profile": -1}, + checks={"slowms": Eq(100)}, + msg="slowms should accept Int64(100) and persist value", + ), + DiagnosticTestCase( + "slowms_double_100", + setup=[{"profile": 0, "slowms": 100.0}], + command={"profile": -1}, + checks={"slowms": Eq(100)}, + msg="slowms should accept double 100.0 and persist value", + ), + DiagnosticTestCase( + "slowms_decimal128_100", + setup=[{"profile": 0, "slowms": Decimal128("100")}], + command={"profile": -1}, + checks={"slowms": Eq(100)}, + msg="slowms should accept Decimal128('100') and persist value", + ), + DiagnosticTestCase( + "slowms_null", + command={"profile": 0, "slowms": None}, + checks={"ok": Eq(1.0)}, + msg="slowms should accept null (no-op)", + ), +] + +# Property [sampleRate Type Acceptance]: the sampleRate field accepts numeric +# BSON types (int, Int64, double, Decimal128) within [0, 1] and null, and the +# value is applied. +SAMPLERATE_TYPE_ACCEPTANCE_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "samplerate_double_0_5", + setup=[{"profile": 0, "sampleRate": 0.5}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.5)}, + msg="sampleRate should accept double 0.5 and persist value", + ), + DiagnosticTestCase( + "samplerate_int_0", + setup=[{"profile": 0, "sampleRate": 0}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.0)}, + msg="sampleRate should accept int 0 and persist value", + ), + DiagnosticTestCase( + "samplerate_int_1", + setup=[{"profile": 0, "sampleRate": 1}], + command={"profile": -1}, + checks={"sampleRate": Eq(1.0)}, + msg="sampleRate should accept int 1 and persist value", + ), + DiagnosticTestCase( + "samplerate_int64_0", + setup=[{"profile": 0, "sampleRate": Int64(0)}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.0)}, + msg="sampleRate should accept Int64(0) and persist value", + ), + DiagnosticTestCase( + "samplerate_int64_1", + setup=[{"profile": 0, "sampleRate": Int64(1)}], + command={"profile": -1}, + checks={"sampleRate": Eq(1.0)}, + msg="sampleRate should accept Int64(1) and persist value", + ), + DiagnosticTestCase( + "samplerate_decimal128_0", + setup=[{"profile": 0, "sampleRate": Decimal128("0")}], + command={"profile": -1}, + checks={"sampleRate": Gte(0.0)}, + msg="sampleRate should accept Decimal128('0') and persist value", + ), + DiagnosticTestCase( + "samplerate_decimal128_0_5", + setup=[{"profile": 0, "sampleRate": Decimal128("0.5")}], + command={"profile": -1}, + checks={"sampleRate": Eq(0.5)}, + msg="sampleRate should accept Decimal128('0.5') and persist value", + ), + DiagnosticTestCase( + "samplerate_decimal128_1", + setup=[{"profile": 0, "sampleRate": Decimal128("1")}], + command={"profile": -1}, + checks={"sampleRate": Eq(1.0)}, + msg="sampleRate should accept Decimal128('1') and persist value", + ), + DiagnosticTestCase( + "samplerate_null", + command={"profile": 0, "sampleRate": None}, + checks={"ok": Eq(1.0)}, + msg="sampleRate should accept null (no-op)", + ), +] + +TYPE_ACCEPTANCE_TESTS = ( + PROFILE_TYPE_ACCEPTANCE_TESTS + SLOWMS_TYPE_ACCEPTANCE_TESTS + SAMPLERATE_TYPE_ACCEPTANCE_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(TYPE_ACCEPTANCE_TESTS)) +def test_profile_type_acceptance(collection, test): + """Test profile command field type acceptance with readback verification.""" + for cmd in test.setup: + execute_command(collection, cmd) + result = execute_command(collection, test.command) + assertProperties(result, test.checks, msg=test.msg, raw_res=True) + execute_command(collection, {"profile": 0, "slowms": 100, "sampleRate": 1.0}) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_type_errors.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_type_errors.py new file mode 100644 index 000000000..638d0719f --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/profile/test_profile_type_errors.py @@ -0,0 +1,310 @@ +"""Tests for profile command BSON type rejection. + +Validates that non-numeric types for the profile, slowms, and sampleRate +fields are rejected with TYPE_MISMATCH_ERROR, and that null for the required +profile field produces MISSING_FIELD_ERROR. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +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 ( + MISSING_FIELD_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Profile Type Rejection]: non-numeric types for the profile field +# are rejected with TYPE_MISMATCH_ERROR. +PROFILE_TYPE_REJECTION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "profile_bool_true", + command={"profile": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject bool true", + ), + DiagnosticTestCase( + "profile_bool_false", + command={"profile": False}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject bool false", + ), + DiagnosticTestCase( + "profile_string", + command={"profile": "hello"}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject string", + ), + DiagnosticTestCase( + "profile_array", + command={"profile": [1]}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject array", + ), + DiagnosticTestCase( + "profile_empty_array", + command={"profile": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject empty array", + ), + DiagnosticTestCase( + "profile_object", + command={"profile": {"a": 1}}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject object", + ), + DiagnosticTestCase( + "profile_empty_object", + command={"profile": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject empty object", + ), + DiagnosticTestCase( + "profile_objectid", + command={"profile": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject ObjectId", + ), + DiagnosticTestCase( + "profile_binary", + command={"profile": Binary(b"")}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject Binary", + ), + DiagnosticTestCase( + "profile_regex", + command={"profile": Regex("test")}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject Regex", + ), + DiagnosticTestCase( + "profile_timestamp", + command={"profile": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject Timestamp", + ), + DiagnosticTestCase( + "profile_code", + command={"profile": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject Code", + ), + DiagnosticTestCase( + "profile_datetime", + command={"profile": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject datetime", + ), + DiagnosticTestCase( + "profile_minkey", + command={"profile": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject MinKey", + ), + DiagnosticTestCase( + "profile_maxkey", + command={"profile": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="profile should reject MaxKey", + ), +] + +# Property [Profile Null Rejection]: null for the required profile field +# produces MISSING_FIELD_ERROR. +PROFILE_NULL_REJECTION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "profile_null", + command={"profile": None}, + error_code=MISSING_FIELD_ERROR, + msg="profile should reject null with missing field error", + ), +] + +# Property [slowms Type Rejection]: non-numeric types for the slowms field +# are rejected with TYPE_MISMATCH_ERROR. +SLOWMS_TYPE_REJECTION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "slowms_string", + command={"profile": 0, "slowms": "hello"}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject string", + ), + DiagnosticTestCase( + "slowms_bool_true", + command={"profile": 0, "slowms": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject bool true", + ), + DiagnosticTestCase( + "slowms_bool_false", + command={"profile": 0, "slowms": False}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject bool false", + ), + DiagnosticTestCase( + "slowms_array", + command={"profile": 0, "slowms": [1]}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject array", + ), + DiagnosticTestCase( + "slowms_object", + command={"profile": 0, "slowms": {"a": 1}}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject object", + ), + DiagnosticTestCase( + "slowms_objectid", + command={"profile": 0, "slowms": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject ObjectId", + ), + DiagnosticTestCase( + "slowms_regex", + command={"profile": 0, "slowms": Regex("test")}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject Regex", + ), + DiagnosticTestCase( + "slowms_timestamp", + command={"profile": 0, "slowms": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject Timestamp", + ), + DiagnosticTestCase( + "slowms_binary", + command={"profile": 0, "slowms": Binary(b"")}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject Binary", + ), + DiagnosticTestCase( + "slowms_code", + command={"profile": 0, "slowms": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject Code", + ), + DiagnosticTestCase( + "slowms_minkey", + command={"profile": 0, "slowms": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject MinKey", + ), + DiagnosticTestCase( + "slowms_maxkey", + command={"profile": 0, "slowms": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject MaxKey", + ), + DiagnosticTestCase( + "slowms_datetime", + command={"profile": 0, "slowms": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + error_code=TYPE_MISMATCH_ERROR, + msg="slowms should reject datetime", + ), +] + +# Property [sampleRate Type Rejection]: non-numeric types for the sampleRate +# field are rejected with TYPE_MISMATCH_ERROR. +SAMPLERATE_TYPE_REJECTION_TESTS: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + "samplerate_string", + command={"profile": 0, "sampleRate": "hello"}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject string", + ), + DiagnosticTestCase( + "samplerate_bool_true", + command={"profile": 0, "sampleRate": True}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject bool true", + ), + DiagnosticTestCase( + "samplerate_bool_false", + command={"profile": 0, "sampleRate": False}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject bool false", + ), + DiagnosticTestCase( + "samplerate_array", + command={"profile": 0, "sampleRate": [1]}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject array", + ), + DiagnosticTestCase( + "samplerate_object", + command={"profile": 0, "sampleRate": {"a": 1}}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject object", + ), + DiagnosticTestCase( + "samplerate_objectid", + command={"profile": 0, "sampleRate": ObjectId()}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject ObjectId", + ), + DiagnosticTestCase( + "samplerate_regex", + command={"profile": 0, "sampleRate": Regex("test")}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject Regex", + ), + DiagnosticTestCase( + "samplerate_timestamp", + command={"profile": 0, "sampleRate": Timestamp(0, 0)}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject Timestamp", + ), + DiagnosticTestCase( + "samplerate_binary", + command={"profile": 0, "sampleRate": Binary(b"")}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject Binary", + ), + DiagnosticTestCase( + "samplerate_code", + command={"profile": 0, "sampleRate": Code("function(){}")}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject Code", + ), + DiagnosticTestCase( + "samplerate_minkey", + command={"profile": 0, "sampleRate": MinKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject MinKey", + ), + DiagnosticTestCase( + "samplerate_maxkey", + command={"profile": 0, "sampleRate": MaxKey()}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject MaxKey", + ), + DiagnosticTestCase( + "samplerate_datetime", + command={"profile": 0, "sampleRate": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + error_code=TYPE_MISMATCH_ERROR, + msg="sampleRate should reject datetime", + ), +] + +PROFILE_TYPE_ERROR_TESTS = ( + PROFILE_TYPE_REJECTION_TESTS + + PROFILE_NULL_REJECTION_TESTS + + SLOWMS_TYPE_REJECTION_TESTS + + SAMPLERATE_TYPE_REJECTION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(PROFILE_TYPE_ERROR_TESTS)) +def test_profile_type_errors(collection, test): + """Test profile command rejects non-numeric types with correct error codes.""" + result = execute_command(collection, test.command) + assertFailureCode(result, test.error_code, msg=test.msg) 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)