diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index ec0404df2..e36b9bb94 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -53,6 +53,36 @@ jobs: - name: Run compatibility tests against ${{ matrix.target.name }} run: | + # Sample memory, load, and per-container usage to the live step log + # while the tests run. Writing to the live log means the samples + # survive even when the job is killed before it finishes, which is + # when a resource problem is most likely to show up. Every line begins + # with resource-monitor so the timeline can be read on its own by + # filtering the log for that word. + echo "resource-monitor samples follow. Each line shows the UTC time, host memory as used over total in megabytes, and swap in megabytes. It then shows the 1, 5, and 15 minute load average and the memory each container is using. Filter the log for resource-monitor to read them on their own." + sample_resources() { + set +e + while true; do + memory=$(free -m | awk '/^Mem:/{print $3"/"$2}') + swap=$(free -m | awk '/^Swap:/{print $3}') + load=$(cut -d' ' -f1-3 /proc/loadavg | tr ' ' ',') + containers=$(docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' 2>/dev/null \ + | awk '{print $1"="$2}' | paste -sd' ' -) + echo "resource-monitor $(date -u +%H:%M:%S) memory=${memory}MB swap=${swap}MB load=${load} ${containers}" + sleep 10 + done + } + sample_resources & + # When the machine runs out of memory the kernel logs a line naming + # the process it kills. Follow the kernel log and forward that line so + # it lands in the step log, since it is the clearest evidence of an + # out of memory kill. The search terms are the exact phrases the + # kernel writes. + ( sudo -n dmesg --follow 2>/dev/null \ + | grep --line-buffered -iE 'out of memory|killed process|oom-kill' \ + | sed -u 's/^/resource-monitor out-of-memory /' ) & + trap 'kill %1 %2 2>/dev/null || true' EXIT + pytest documentdb_tests/compatibility/tests \ --connection-string "${{ matrix.target.connection_string }}" \ --engine-name "${{ matrix.target.engine }}" \ diff --git a/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_allowdiskuse.py b/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_allowdiskuse.py index 31aad9358..5bf6d22a4 100644 --- a/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_allowdiskuse.py +++ b/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_allowdiskuse.py @@ -27,6 +27,7 @@ TYPE_MISMATCH_ERROR, ) from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq, Exists from documentdb_tests.framework.target_collection import ViewCollection @@ -120,7 +121,9 @@ ), CommandTestCase( "allowdiskuse_true_memory_exceeded", - docs=[{"_id": i, "data": "x" * 100_000, "sort_key": 1050 - i} for i in range(1050)], + docs=lazy( + lambda: [{"_id": i, "data": "x" * 100_000, "sort_key": 1050 - i} for i in range(1050)] + ), command=lambda ctx: { "aggregate": ctx.collection, "pipeline": [{"$sort": {"sort_key": 1}}], @@ -170,7 +173,9 @@ ], CommandTestCase( "allowdiskuse_reject_memory_exceeded", - docs=[{"_id": i, "data": "x" * 100_000, "sort_key": 1050 - i} for i in range(1050)], + docs=lazy( + lambda: [{"_id": i, "data": "x" * 100_000, "sort_key": 1050 - i} for i in range(1050)] + ), command=lambda ctx: { "aggregate": ctx.collection, "pipeline": [{"$sort": {"sort_key": 1}}], diff --git a/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_cursor_acceptance.py b/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_cursor_acceptance.py index 3af51ca3c..8166053db 100644 --- a/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_cursor_acceptance.py +++ b/documentdb_tests/compatibility/tests/core/aggregation/commands/aggregate/test_aggregate_cursor_acceptance.py @@ -11,6 +11,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq, Ne from documentdb_tests.framework.test_constants import ( @@ -492,7 +493,7 @@ AGGREGATE_CURSOR_RESPONSE_LIMIT_TESTS: list[CommandTestCase] = [ CommandTestCase( "cursor_batchsize_16mb_limit", - docs=[{"_id": i, "data": "x" * 1_000_000} for i in range(20)], + docs=lazy(lambda: [{"_id": i, "data": "x" * 1_000_000} for i in range(20)]), command=lambda ctx: { "aggregate": ctx.collection, "pipeline": [], diff --git a/documentdb_tests/compatibility/tests/core/aggregation/commands/distinct/test_distinct_command_errors.py b/documentdb_tests/compatibility/tests/core/aggregation/commands/distinct/test_distinct_command_errors.py index bae3b78a0..c6f4a2413 100644 --- a/documentdb_tests/compatibility/tests/core/aggregation/commands/distinct/test_distinct_command_errors.py +++ b/documentdb_tests/compatibility/tests/core/aggregation/commands/distinct/test_distinct_command_errors.py @@ -24,6 +24,7 @@ UNRECOGNIZED_COMMAND_FIELD_ERROR, ) from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( DECIMAL128_INFINITY, @@ -281,7 +282,7 @@ DISTINCT_BSON_SIZE_LIMIT_TESTS: list[CommandTestCase] = [ CommandTestCase( "bson_size_limit_exceeded", - docs=[{"_id": i, "x": f"v{i}" + "x" * 17_000} for i in range(1100)], + docs=lazy(lambda: [{"_id": i, "x": f"v{i}" + "x" * 17_000} for i in range(1100)]), command=lambda ctx: {"distinct": ctx.collection, "key": "x"}, error_code=DISTINCT_TOO_BIG_ERROR, msg="distinct should produce an error when results exceed the 16MB BSON size limit", diff --git a/documentdb_tests/compatibility/tests/core/operator/accumulators/sum/test_accumulator_sum_boundary.py b/documentdb_tests/compatibility/tests/core/operator/accumulators/sum/test_accumulator_sum_boundary.py index 5436159bf..e1c17b24d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/accumulators/sum/test_accumulator_sum_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/accumulators/sum/test_accumulator_sum_boundary.py @@ -10,6 +10,7 @@ ) from documentdb_tests.framework.assertions import assertSuccess from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy, materialize from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( DECIMAL128_NEGATIVE_ZERO, @@ -158,7 +159,7 @@ SUM_LARGE_GROUP_TESTS: list[AccumulatorTestCase] = [ AccumulatorTestCase( "large_group_10k_int1", - docs=[{"v": 1} for _ in range(10_000)], + docs=lazy(lambda: [{"v": 1} for _ in range(10_000)]), pipeline=[ {"$group": {"_id": None, "result": {"$sum": "$v"}}}, {"$project": {"_id": 0, "value": "$result", "type": {"$type": "$result"}}}, @@ -175,7 +176,7 @@ def test_accumulator_sum_boundary(collection, test_case: AccumulatorTestCase): """Test $sum integer boundary values and large group accumulation.""" if test_case.docs: - collection.insert_many(test_case.docs) + collection.insert_many(materialize(test_case.docs)) result = execute_command( collection, {"aggregate": collection.name, "pipeline": test_case.pipeline or [], "cursor": {}}, @@ -237,7 +238,7 @@ def test_accumulator_sum_boundary(collection, test_case: AccumulatorTestCase): def test_accumulator_sum_negative_zero(collection, test_case: AccumulatorTestCase): """Test $sum negative zero normalization.""" if test_case.docs: - collection.insert_many(test_case.docs) + collection.insert_many(materialize(test_case.docs)) result = execute_command( collection, {"aggregate": collection.name, "pipeline": test_case.pipeline or [], "cursor": {}}, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/concat/test_concat_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/concat/test_concat_size_limit.py index 7c061e611..56ee75885 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/concat/test_concat_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/concat/test_concat_size_limit.py @@ -9,6 +9,7 @@ ) from documentdb_tests.framework.assertions import assertFailureCode from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -22,38 +23,48 @@ # Two large strings concatenated, just under the limit. ConcatTest( "size_two_args_one_under", - args=[ - "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), - "b" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), - ], - expected="a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) - + "b" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), + args=lazy( + lambda: [ + "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), + "b" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), + ] + ), + expected=lazy( + lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + + "b" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + ), msg="$concat should handle two large strings concatenated together", ), ConcatTest( "size_one_under", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 1)], - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 1)]), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$concat should succeed when result is one byte under the size limit", ), # 2-byte chars: one byte under the limit. ConcatTest( "size_one_under_2byte", - args=["é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"], - expected="é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", + args=lazy(lambda: ["é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"]), + expected=lazy(lambda: "é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), msg="$concat should succeed with 2-byte chars one byte under the limit", ), # 4-byte chars: one byte under the limit. ConcatTest( "size_one_under_4byte", - args=["😀" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4) + "abc"], - expected="😀" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4) + "abc", + args=lazy(lambda: ["😀" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4) + "abc"]), + expected=lazy(lambda: "😀" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4) + "abc"), msg="$concat should succeed with 4-byte chars one byte under the limit", ), # Null propagation wins when individual args are under the limit. ConcatTest( "size_null_precedence", - args=["a" * (STRING_SIZE_LIMIT_BYTES // 2), None, "b" * (STRING_SIZE_LIMIT_BYTES // 2)], + args=lazy( + lambda: [ + "a" * (STRING_SIZE_LIMIT_BYTES // 2), + None, + "b" * (STRING_SIZE_LIMIT_BYTES // 2), + ] + ), expected=None, msg="$concat should return null when null appears among large strings under the limit", ), @@ -63,45 +74,49 @@ STRING_SIZE_LIMIT_ERROR_TESTS: list[ConcatTest] = [ ConcatTest( "size_at_limit", - args=["a" * STRING_SIZE_LIMIT_BYTES], + args=lazy(lambda: ["a" * STRING_SIZE_LIMIT_BYTES]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$concat should reject result at the size limit", ), # Two halves summing to exactly the limit. ConcatTest( "size_two_halves", - args=["a" * (STRING_SIZE_LIMIT_BYTES // 2), "b" * (STRING_SIZE_LIMIT_BYTES // 2)], + args=lazy( + lambda: ["a" * (STRING_SIZE_LIMIT_BYTES // 2), "b" * (STRING_SIZE_LIMIT_BYTES // 2)] + ), error_code=STRING_SIZE_LIMIT_ERROR, msg="$concat should reject two strings summing to the size limit", ), # 2-byte chars totaling exactly the limit. ConcatTest( "size_at_limit_2byte", - args=["é" * (STRING_SIZE_LIMIT_BYTES // 2)], + args=lazy(lambda: ["é" * (STRING_SIZE_LIMIT_BYTES // 2)]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$concat should reject 2-byte chars totaling the size limit", ), # 4-byte chars totaling exactly the limit. ConcatTest( "size_at_limit_4byte", - args=["😀" * (STRING_SIZE_LIMIT_BYTES // 4)], + args=lazy(lambda: ["😀" * (STRING_SIZE_LIMIT_BYTES // 4)]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$concat should reject 4-byte chars totaling the size limit", ), # Many small operands summing to exactly the limit. ConcatTest( "size_many_small", - args=["a" * (STRING_SIZE_LIMIT_BYTES // 1024)] * 1024, + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES // 1024)] * 1024), error_code=STRING_SIZE_LIMIT_ERROR, msg="$concat should reject many small operands summing to the size limit", ), # Operand produced by a nested expression rather than a literal. ConcatTest( "size_nested", - args=[ - {"$concat": ["a" * (STRING_SIZE_LIMIT_BYTES // 2)]}, - "b" * (STRING_SIZE_LIMIT_BYTES // 2), - ], + args=lazy( + lambda: [ + {"$concat": ["a" * (STRING_SIZE_LIMIT_BYTES // 2)]}, + "b" * (STRING_SIZE_LIMIT_BYTES // 2), + ] + ), error_code=STRING_SIZE_LIMIT_ERROR, msg="$concat should reject nested expression result exceeding the size limit", ), @@ -123,8 +138,8 @@ def test_concat_size_limit_stored_field(collection): """Test $concat size limit is enforced when stored fields contribute to the result.""" result = execute_project_with_insert( collection, - {"s": "a" * (STRING_SIZE_LIMIT_BYTES // 2)}, - {"result": {"$concat": ["$s", "b" * (STRING_SIZE_LIMIT_BYTES // 2)]}}, + lazy(lambda: {"s": "a" * (STRING_SIZE_LIMIT_BYTES // 2)}), + lazy(lambda: {"result": {"$concat": ["$s", "b" * (STRING_SIZE_LIMIT_BYTES // 2)]}}), ) assertFailureCode( result, STRING_SIZE_LIMIT_ERROR, msg="$concat should enforce size limit with stored fields" diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_size_limit.py index d13a3cdf4..9109a4459 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,34 +20,34 @@ INDEXOFBYTES_SIZE_LIMIT_SUCCESS_TESTS: list[IndexOfBytesTest] = [ IndexOfBytesTest( "size_string_one_under", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 1), "a"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 1), "a"]), expected=0, msg="$indexOfBytes should accept string one byte under the size limit", ), IndexOfBytesTest( "size_substring_one_under", - args=["hello", "a" * (STRING_SIZE_LIMIT_BYTES - 1)], + args=lazy(lambda: ["hello", "a" * (STRING_SIZE_LIMIT_BYTES - 1)]), expected=-1, msg="$indexOfBytes should accept substring one byte under the size limit", ), # 2-byte chars: one byte under the limit. Limit is byte-based, not codepoint-based. IndexOfBytesTest( "size_string_one_under_2byte", - args=["é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", "a"], + args=lazy(lambda: ["é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", "a"]), expected=STRING_SIZE_LIMIT_BYTES - 2, msg="$indexOfBytes should accept 2-byte char string one byte under limit", ), # Found at end of a large string. IndexOfBytesTest( "size_found_at_end", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"]), expected=STRING_SIZE_LIMIT_BYTES - 2, msg="$indexOfBytes should find match at end of a large string", ), # Not found in a large string. IndexOfBytesTest( "size_not_found", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 1), "b"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 1), "b"]), expected=-1, msg="$indexOfBytes should return -1 for no match in a large string", ), @@ -57,20 +58,20 @@ INDEXOFBYTES_SIZE_LIMIT_ERROR_TESTS: list[IndexOfBytesTest] = [ IndexOfBytesTest( "size_string_at_limit", - args=["a" * STRING_SIZE_LIMIT_BYTES, "a"], + args=lazy(lambda: ["a" * STRING_SIZE_LIMIT_BYTES, "a"]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$indexOfBytes should reject string at the size limit", ), IndexOfBytesTest( "size_substring_at_limit", - args=["hello", "a" * STRING_SIZE_LIMIT_BYTES], + args=lazy(lambda: ["hello", "a" * STRING_SIZE_LIMIT_BYTES]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$indexOfBytes should reject substring at the size limit", ), # 2-byte chars: exactly at the limit. Limit is byte-based, not codepoint-based. IndexOfBytesTest( "size_string_at_limit_2byte", - args=["é" * (STRING_SIZE_LIMIT_BYTES // 2), "a"], + args=lazy(lambda: ["é" * (STRING_SIZE_LIMIT_BYTES // 2), "a"]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$indexOfBytes should reject 2-byte char string at the byte size limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_usage.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_usage.py index 63eefece7..e169e9745 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_usage.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfBytes/test_indexOfBytes_usage.py @@ -8,6 +8,7 @@ execute_project_with_insert, ) from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -123,7 +124,7 @@ def test_indexofbytes_nested_field_paths(collection): ), IndexOfBytesTest( "return_type_large_index", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"]), msg="$indexOfBytes should return int type for large index value", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_size_limit.py index b9ded658d..f3bd9d46b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,34 +20,34 @@ INDEXOFCP_SIZE_LIMIT_SUCCESS_TESTS: list[IndexOfCPTest] = [ IndexOfCPTest( "size_string_one_under", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 1), "a"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 1), "a"]), expected=0, msg="$indexOfCP should accept string one byte under the size limit", ), IndexOfCPTest( "size_substr_one_under", - args=["hello", "a" * (STRING_SIZE_LIMIT_BYTES - 1)], + args=lazy(lambda: ["hello", "a" * (STRING_SIZE_LIMIT_BYTES - 1)]), expected=-1, msg="$indexOfCP should accept substring one byte under the size limit", ), # 2-byte chars: one byte under the limit. Limit is byte-based, not code-point-based. IndexOfCPTest( "size_string_one_under_2byte", - args=["é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", "a"], + args=lazy(lambda: ["é" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", "a"]), expected=(STRING_SIZE_LIMIT_BYTES - 1) // 2, msg="$indexOfCP should accept 2-byte char string one byte under limit", ), # Found at end of a large string. IndexOfCPTest( "size_found_at_end", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"]), expected=STRING_SIZE_LIMIT_BYTES - 2, msg="$indexOfCP should find match at end of a large string", ), # Not found in a large string. IndexOfCPTest( "size_not_found", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 1), "b"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 1), "b"]), expected=-1, msg="$indexOfCP should return -1 for no match in a large string", ), @@ -58,20 +59,20 @@ INDEXOFCP_SIZE_LIMIT_ERROR_TESTS: list[IndexOfCPTest] = [ IndexOfCPTest( "size_string_at_limit", - args=["a" * STRING_SIZE_LIMIT_BYTES, "b"], + args=lazy(lambda: ["a" * STRING_SIZE_LIMIT_BYTES, "b"]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$indexOfCP should reject string at the size limit", ), IndexOfCPTest( "size_substr_at_limit", - args=["hello", "a" * STRING_SIZE_LIMIT_BYTES], + args=lazy(lambda: ["hello", "a" * STRING_SIZE_LIMIT_BYTES]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$indexOfCP should reject substring at the size limit", ), # 2-byte chars: exactly at the limit. Limit is byte-based, not code-point-based. IndexOfCPTest( "size_string_at_limit_2byte", - args=["é" * (STRING_SIZE_LIMIT_BYTES // 2), "b"], + args=lazy(lambda: ["é" * (STRING_SIZE_LIMIT_BYTES // 2), "b"]), error_code=STRING_SIZE_LIMIT_ERROR, msg="$indexOfCP should reject 2-byte char string at the byte size limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_usage.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_usage.py index e7ee72d66..43c98d6f9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_usage.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/indexOfCP/test_indexOfCP_usage.py @@ -8,6 +8,7 @@ execute_project_with_insert, ) from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -124,7 +125,7 @@ def test_indexofcp_nested_field_paths(collection): ), IndexOfCPTest( "return_type_large_index", - args=["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"], + args=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 2) + "b", "b"]), msg="$indexOfCP should return int type for large index value", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/ltrim/test_ltrim_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/ltrim/test_ltrim_size_limit.py index cb62bd5aa..12c5bda80 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/ltrim/test_ltrim_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/ltrim/test_ltrim_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,21 +20,21 @@ LTRIM_SIZE_LIMIT_SUCCESS_TESTS: list[LtrimTest] = [ LtrimTest( "size_one_under", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$ltrim should accept input one byte under the size limit", ), # 2-byte chars: one byte under the limit. LtrimTest( "size_one_under_2byte", - input="\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", - expected="\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", + input=lazy(lambda: "\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), + expected=lazy(lambda: "\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), msg="$ltrim should accept 2-byte character input one byte under the size limit", ), # Large input with many leading trim characters, just under the limit. LtrimTest( "size_trim_leading", - input="a" * (STRING_SIZE_LIMIT_BYTES - 6) + "hello", + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 6) + "hello"), chars="a", expected="hello", msg="$ltrim should trim many leading characters near the size limit", @@ -45,13 +46,13 @@ LTRIM_SIZE_LIMIT_ERROR_TESTS: list[LtrimTest] = [ LtrimTest( "size_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$ltrim should reject input at the BSON string byte limit", ), LtrimTest( "size_at_limit_2byte", - input="\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2), + input=lazy(lambda: "\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2)), error_code=STRING_SIZE_LIMIT_ERROR, msg="$ltrim should reject 2-byte character input at the BSON string byte limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFind/test_regexFind_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFind/test_regexFind_size_limit.py index 0c9eb6a0e..b440102dd 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFind/test_regexFind_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFind/test_regexFind_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import REGEX_BAD_PATTERN_ERROR, STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( REGEX_PATTERN_LIMIT_BYTES, @@ -22,16 +23,16 @@ REGEXFIND_SIZE_LIMIT_SUCCESS_TESTS: list[RegexFindTest] = [ RegexFindTest( "size_one_under", - input="a" * (STRING_SIZE_LIMIT_BYTES - 4) + "XYZ", + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 4) + "XYZ"), regex="XYZ", - expected={"match": "XYZ", "idx": STRING_SIZE_LIMIT_BYTES - 4, "captures": []}, + expected=lazy(lambda: {"match": "XYZ", "idx": STRING_SIZE_LIMIT_BYTES - 4, "captures": []}), msg="$regexFind should accept input one byte under the size limit", ), RegexFindTest( "size_regex_at_pattern_limit", - input="a" * REGEX_PATTERN_LIMIT_BYTES, - regex="a" * REGEX_PATTERN_LIMIT_BYTES, - expected={"match": "a" * REGEX_PATTERN_LIMIT_BYTES, "idx": 0, "captures": []}, + input=lazy(lambda: "a" * REGEX_PATTERN_LIMIT_BYTES), + regex=lazy(lambda: "a" * REGEX_PATTERN_LIMIT_BYTES), + expected=lazy(lambda: {"match": "a" * REGEX_PATTERN_LIMIT_BYTES, "idx": 0, "captures": []}), msg="$regexFind should accept regex at the pattern length limit", ), ] @@ -41,7 +42,7 @@ REGEXFIND_SIZE_LIMIT_ERROR_TESTS: list[RegexFindTest] = [ RegexFindTest( "size_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), regex="a", error_code=STRING_SIZE_LIMIT_ERROR, msg="$regexFind should reject input at the size limit", @@ -49,7 +50,7 @@ RegexFindTest( "size_regex_over_pattern_limit", input="a", - regex="a" * (REGEX_PATTERN_LIMIT_BYTES + 1), + regex=lazy(lambda: "a" * (REGEX_PATTERN_LIMIT_BYTES + 1)), error_code=REGEX_BAD_PATTERN_ERROR, msg="$regexFind should reject regex over the pattern length limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_matching.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_matching.py index 02af66a43..2e4149ff1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_matching.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_matching.py @@ -6,6 +6,7 @@ assert_expression_result, execute_expression, ) +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from .utils.regexFindAll_common import ( @@ -135,9 +136,11 @@ # performant; scaling to STRING_SIZE_LIMIT_BYTES would produce ~8M matches and hang. RegexFindAllTest( "edge_large_input_many_matches", - input="ab" * 5_000, + input=lazy(lambda: "ab" * 5_000), regex="ab", - expected=[{"match": "ab", "idx": i * 2, "captures": []} for i in range(5_000)], + expected=lazy( + lambda: [{"match": "ab", "idx": i * 2, "captures": []} for i in range(5_000)] + ), msg="$regexFindAll should return all 5000 matches from a large repeated input", ), # Newline in input. @@ -196,7 +199,7 @@ expected=[{"match": "aaa", "idx": 0, "captures": []}], msg="$regexFindAll greedy quantifier should consume maximum input in one match", ), - # Lazy quantifier consumes minimum input, increasing match count. + # lazy quantifier consumes minimum input, increasing match count. RegexFindAllTest( "multi_nongreedy_more_matches", input="aaa", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_size_limit.py index 12299a447..9f4a4fc7e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexFindAll/test_regexFindAll_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import REGEX_BAD_PATTERN_ERROR, STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( REGEX_PATTERN_LIMIT_BYTES, @@ -22,21 +23,25 @@ REGEXFINDALL_SIZE_LIMIT_SUCCESS_TESTS: list[RegexFindAllTest] = [ RegexFindAllTest( "size_one_under", - input="a" * (STRING_SIZE_LIMIT_BYTES - 4) + "XYZ", + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 4) + "XYZ"), regex="XYZ", - expected=[{"match": "XYZ", "idx": STRING_SIZE_LIMIT_BYTES - 4, "captures": []}], + expected=lazy( + lambda: [{"match": "XYZ", "idx": STRING_SIZE_LIMIT_BYTES - 4, "captures": []}] + ), msg="$regexFindAll should accept input one byte under the size limit", ), RegexFindAllTest( "size_regex_at_pattern_limit", - input="a" * REGEX_PATTERN_LIMIT_BYTES, - regex="a" * REGEX_PATTERN_LIMIT_BYTES, - expected=[{"match": "a" * REGEX_PATTERN_LIMIT_BYTES, "idx": 0, "captures": []}], + input=lazy(lambda: "a" * REGEX_PATTERN_LIMIT_BYTES), + regex=lazy(lambda: "a" * REGEX_PATTERN_LIMIT_BYTES), + expected=lazy( + lambda: [{"match": "a" * REGEX_PATTERN_LIMIT_BYTES, "idx": 0, "captures": []}] + ), msg="$regexFindAll should accept regex at the pattern length limit", ), RegexFindAllTest( "size_two_matches", - input="XY" + "a" * (STRING_SIZE_LIMIT_BYTES - 5) + "XY", + input=lazy(lambda: "XY" + "a" * (STRING_SIZE_LIMIT_BYTES - 5) + "XY"), regex="XY", expected=[ {"match": "XY", "idx": 0, "captures": []}, @@ -52,7 +57,7 @@ REGEXFINDALL_SIZE_LIMIT_ERROR_TESTS: list[RegexFindAllTest] = [ RegexFindAllTest( "size_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), regex="a", error_code=STRING_SIZE_LIMIT_ERROR, msg="$regexFindAll should reject input at the size limit", @@ -60,7 +65,7 @@ RegexFindAllTest( "size_regex_over_pattern_limit", input="a", - regex="a" * (REGEX_PATTERN_LIMIT_BYTES + 1), + regex=lazy(lambda: "a" * (REGEX_PATTERN_LIMIT_BYTES + 1)), error_code=REGEX_BAD_PATTERN_ERROR, msg="$regexFindAll should reject regex over the pattern length limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexMatch/test_regexMatch_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexMatch/test_regexMatch_size_limit.py index 03cdc329f..02c29a34e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexMatch/test_regexMatch_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/regexMatch/test_regexMatch_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import REGEX_BAD_PATTERN_ERROR, STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( REGEX_PATTERN_LIMIT_BYTES, @@ -22,15 +23,15 @@ REGEXMATCH_SIZE_LIMIT_SUCCESS_TESTS: list[RegexMatchTest] = [ RegexMatchTest( "size_one_under", - input="a" * (STRING_SIZE_LIMIT_BYTES - 4) + "XYZ", + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 4) + "XYZ"), regex="XYZ", expected=True, msg="$regexMatch should accept input one byte under the size limit", ), RegexMatchTest( "size_regex_at_pattern_limit", - input="a" * REGEX_PATTERN_LIMIT_BYTES, - regex="a" * REGEX_PATTERN_LIMIT_BYTES, + input=lazy(lambda: "a" * REGEX_PATTERN_LIMIT_BYTES), + regex=lazy(lambda: "a" * REGEX_PATTERN_LIMIT_BYTES), expected=True, msg="$regexMatch should accept regex at the pattern length limit", ), @@ -41,7 +42,7 @@ REGEXMATCH_SIZE_LIMIT_ERROR_TESTS: list[RegexMatchTest] = [ RegexMatchTest( "size_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), regex="a", error_code=STRING_SIZE_LIMIT_ERROR, msg="$regexMatch should reject input at the size limit", @@ -49,7 +50,7 @@ RegexMatchTest( "size_regex_over_pattern_limit", input="a", - regex="a" * (REGEX_PATTERN_LIMIT_BYTES + 1), + regex=lazy(lambda: "a" * (REGEX_PATTERN_LIMIT_BYTES + 1)), error_code=REGEX_BAD_PATTERN_ERROR, msg="$regexMatch should reject regex over the pattern length limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceAll/test_replaceAll_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceAll/test_replaceAll_size_limit.py index 4d6356bd8..8ff8f2dd5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceAll/test_replaceAll_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceAll/test_replaceAll_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -21,10 +22,10 @@ REPLACEALL_SIZE_LIMIT_SUCCESS_TESTS: list[ReplaceAllTest] = [ ReplaceAllTest( "size_success_input_max", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), find="xyz", replacement="abc", - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$replaceAll size limit: success input max", ), # Replacement amplification producing result one under the limit. @@ -32,26 +33,26 @@ "size_success_amplification_max", input="a", find="a", - replacement="a" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + replacement=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$replaceAll size limit: success amplification max", ), # 4-byte emoji at one character below the byte limit. ReplaceAllTest( "size_success_4byte_max", - input="\U0001f600" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4), + input=lazy(lambda: "\U0001f600" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4)), find="xyz", replacement="abc", - expected="\U0001f600" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4), + expected=lazy(lambda: "\U0001f600" * ((STRING_SIZE_LIMIT_BYTES - 1) // 4)), msg="$replaceAll size limit: success 4byte max", ), # Empty find amplification just under the limit. ReplaceAllTest( "size_success_empty_find_amplification", - input="a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2), + input=lazy(lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2)), find="", replacement="X", - expected="X" + "aX" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2), + expected=lazy(lambda: "X" + "aX" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2)), msg="$replaceAll size limit: success empty find amplification", ), ] @@ -63,7 +64,7 @@ # Exactly at the limit. ReplaceAllTest( "size_error_input_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), find="a", replacement="b", error_code=STRING_SIZE_LIMIT_ERROR, @@ -72,7 +73,7 @@ # 2-byte chars at the limit. ReplaceAllTest( "size_error_byte_based_2byte", - input="\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2), + input=lazy(lambda: "\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2)), find="\u00e9", replacement="e", error_code=STRING_SIZE_LIMIT_ERROR, @@ -81,7 +82,7 @@ # 4-byte chars at the limit. ReplaceAllTest( "size_error_byte_based_4byte", - input="\U0001f600" * (STRING_SIZE_LIMIT_BYTES // 4), + input=lazy(lambda: "\U0001f600" * (STRING_SIZE_LIMIT_BYTES // 4)), find="\U0001f600", replacement="x", error_code=STRING_SIZE_LIMIT_ERROR, @@ -90,7 +91,7 @@ # Input literal rejected even when replacement would shrink the result. ReplaceAllTest( "size_error_input_shrinking_rejected", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), find="a" * 100, replacement="", error_code=STRING_SIZE_LIMIT_ERROR, @@ -99,7 +100,7 @@ # Result amplification: input under limit, replacement grows result to limit. ReplaceAllTest( "size_error_result_amplification", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), find="a", replacement="aa", error_code=STRING_SIZE_LIMIT_ERROR, @@ -109,7 +110,7 @@ ReplaceAllTest( "size_error_find_at_limit", input="hello", - find="a" * STRING_SIZE_LIMIT_BYTES, + find=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), replacement="b", error_code=STRING_SIZE_LIMIT_ERROR, msg="$replaceAll size limit: error find at limit", @@ -119,7 +120,7 @@ "size_error_replacement_at_limit", input="hello", find="a", - replacement="a" * STRING_SIZE_LIMIT_BYTES, + replacement=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$replaceAll size limit: error replacement at limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceOne/test_replaceOne_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceOne/test_replaceOne_size_limit.py index 286f93391..3b9973173 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceOne/test_replaceOne_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/replaceOne/test_replaceOne_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -21,25 +22,25 @@ REPLACEONE_SIZE_LIMIT_SUCCESS_TESTS: list[ReplaceOneTest] = [ ReplaceOneTest( "size_success_input_max", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), find="xyz", replacement="abc", - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$replaceOne size limit: success input max", ), # Large input with a shrinking replacement stays under the limit. ReplaceOneTest( "size_success_shrinking_replacement", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), find="a", replacement="", - expected="a" * (STRING_SIZE_LIMIT_BYTES - 2), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 2)), msg="$replaceOne size limit: success shrinking replacement", ), ReplaceOneTest( "size_success_find_max", input="hello", - find="a" * (STRING_SIZE_LIMIT_BYTES - 1), + find=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), replacement="X", expected="hello", msg="$replaceOne size limit: success find max", @@ -47,40 +48,44 @@ # Replace at start of a near-limit string. ReplaceOneTest( "size_find_at_start", - input="X" + "a" * (STRING_SIZE_LIMIT_BYTES - 2), + input=lazy(lambda: "X" + "a" * (STRING_SIZE_LIMIT_BYTES - 2)), find="X", replacement="Y", - expected="Y" + "a" * (STRING_SIZE_LIMIT_BYTES - 2), + expected=lazy(lambda: "Y" + "a" * (STRING_SIZE_LIMIT_BYTES - 2)), msg="$replaceOne size limit: find at start", ), # Replace at end of a near-limit string. ReplaceOneTest( "size_find_at_end", - input="a" * (STRING_SIZE_LIMIT_BYTES - 2) + "X", + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 2) + "X"), find="X", replacement="Y", - expected="a" * (STRING_SIZE_LIMIT_BYTES - 2) + "Y", + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 2) + "Y"), msg="$replaceOne size limit: find at end", ), # Replace in middle of a near-limit string. ReplaceOneTest( "size_find_in_middle", - input="a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2) - + "X" - + "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2), + input=lazy( + lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2) + + "X" + + "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2) + ), find="X", replacement="Y", - expected="a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2) - + "Y" - + "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2), + expected=lazy( + lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2) + + "Y" + + "a" * ((STRING_SIZE_LIMIT_BYTES - 2) // 2) + ), msg="$replaceOne size limit: find in middle", ), # Large find string replaces entire input. Both at half-limit because two # near-limit strings in one command would exceed the BSON document size. ReplaceOneTest( "size_large_find", - input="a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), - find="a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), + input=lazy(lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2)), + find=lazy(lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2)), replacement="X", expected="X", msg="$replaceOne size limit: large find", @@ -90,8 +95,8 @@ "size_large_replacement", input="X", find="X", - replacement="a" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + replacement=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$replaceOne size limit: large replacement", ), ] @@ -104,7 +109,7 @@ # Exactly at the limit. ReplaceOneTest( "size_error_input_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), find="a", replacement="b", error_code=STRING_SIZE_LIMIT_ERROR, @@ -113,7 +118,7 @@ # 2-byte chars at the limit. ReplaceOneTest( "size_error_byte_based_2byte", - input="\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2), + input=lazy(lambda: "\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2)), find="\u00e9", replacement="e", error_code=STRING_SIZE_LIMIT_ERROR, @@ -122,7 +127,7 @@ # 4-byte chars at the limit. ReplaceOneTest( "size_error_byte_based_4byte", - input="\U0001f600" * (STRING_SIZE_LIMIT_BYTES // 4), + input=lazy(lambda: "\U0001f600" * (STRING_SIZE_LIMIT_BYTES // 4)), find="\U0001f600", replacement="x", error_code=STRING_SIZE_LIMIT_ERROR, @@ -131,7 +136,7 @@ # Input literal rejected even when replacement would shrink the result. ReplaceOneTest( "size_error_input_shrinking_rejected", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), find="a" * 100, replacement="", error_code=STRING_SIZE_LIMIT_ERROR, @@ -140,7 +145,7 @@ # Result amplification: input under limit, replacement grows result to limit. ReplaceOneTest( "size_error_result_amplification", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), find="a", replacement="aa", error_code=STRING_SIZE_LIMIT_ERROR, @@ -150,7 +155,7 @@ ReplaceOneTest( "size_error_find_at_limit", input="hello", - find="a" * STRING_SIZE_LIMIT_BYTES, + find=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), replacement="b", error_code=STRING_SIZE_LIMIT_ERROR, msg="$replaceOne size limit: error find at limit", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/rtrim/test_rtrim_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/rtrim/test_rtrim_size_limit.py index 839b8bb9d..c334bd207 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/rtrim/test_rtrim_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/rtrim/test_rtrim_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,21 +20,21 @@ RTRIM_SIZE_LIMIT_SUCCESS_TESTS: list[RtrimTest] = [ RtrimTest( "size_one_under", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$rtrim should accept input one byte under the size limit", ), # 2-byte chars: one byte under the limit. RtrimTest( "size_one_under_2byte", - input="\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", - expected="\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", + input=lazy(lambda: "\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), + expected=lazy(lambda: "\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), msg="$rtrim should accept 2-byte character input one byte under the size limit", ), # Large input with many trailing trim characters, just under the limit. RtrimTest( "size_trim_trailing", - input="hello" + "a" * (STRING_SIZE_LIMIT_BYTES - 6), + input=lazy(lambda: "hello" + "a" * (STRING_SIZE_LIMIT_BYTES - 6)), chars="a", expected="hello", msg="$rtrim should trim many trailing characters near the size limit", @@ -45,13 +46,13 @@ RTRIM_SIZE_LIMIT_ERROR_TESTS: list[RtrimTest] = [ RtrimTest( "size_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$rtrim should reject input at the BSON string byte limit", ), RtrimTest( "size_at_limit_2byte", - input="\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2), + input=lazy(lambda: "\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2)), error_code=STRING_SIZE_LIMIT_ERROR, msg="$rtrim should reject 2-byte character input at the BSON string byte limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/split/test_split_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/split/test_split_size_limit.py index d11c6387d..88f6e3512 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/split/test_split_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/split/test_split_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,15 +20,15 @@ SPLIT_SIZE_LIMIT_SUCCESS_TESTS: list[SplitTest] = [ SplitTest( "size_string_one_under", - string="a" * (STRING_SIZE_LIMIT_BYTES - 1), + string=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), delimiter="-", - expected=["a" * (STRING_SIZE_LIMIT_BYTES - 1)], + expected=lazy(lambda: ["a" * (STRING_SIZE_LIMIT_BYTES - 1)]), msg="$split should accept input string one byte under the size limit", ), SplitTest( "size_delim_one_under", string="hello", - delimiter="a" * (STRING_SIZE_LIMIT_BYTES - 1), + delimiter=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), expected=["hello"], msg="$split should accept delimiter one byte under the size limit", ), @@ -39,7 +40,7 @@ SPLIT_SIZE_LIMIT_ERROR_TESTS: list[SplitTest] = [ SplitTest( "size_limit_input_at_boundary", - string="a" * STRING_SIZE_LIMIT_BYTES, + string=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), delimiter="-", error_code=STRING_SIZE_LIMIT_ERROR, msg="$split should reject input string at the size limit", @@ -47,13 +48,13 @@ SplitTest( "size_limit_delim_at_boundary", string="hello", - delimiter="a" * STRING_SIZE_LIMIT_BYTES, + delimiter=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$split should reject delimiter at the size limit", ), SplitTest( "size_limit_input_2byte", - string="\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2), + string=lazy(lambda: "\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2)), delimiter="-", error_code=STRING_SIZE_LIMIT_ERROR, msg="$split should reject 2-byte char input totaling the size limit", @@ -61,9 +62,14 @@ # Sub-expression producing oversized result is caught before $split runs. SplitTest( "size_limit_input_subexpr", - string={ - "$concat": ["a" * (STRING_SIZE_LIMIT_BYTES // 2), "a" * (STRING_SIZE_LIMIT_BYTES // 2)] - }, + string=lazy( + lambda: { + "$concat": [ + "a" * (STRING_SIZE_LIMIT_BYTES // 2), + "a" * (STRING_SIZE_LIMIT_BYTES // 2), + ] + } + ), delimiter="-", error_code=STRING_SIZE_LIMIT_ERROR, msg="$split should reject sub-expression producing oversized string", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenBytes/test_strLenBytes_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenBytes/test_strLenBytes_size_limit.py index c5417e2aa..49d1f518b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenBytes/test_strLenBytes_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenBytes/test_strLenBytes_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,13 +20,13 @@ STRLENBYTES_SIZE_LIMIT_SUCCESS_TESTS: list[StrLenBytesTest] = [ StrLenBytesTest( "size_one_under", - value="a" * (STRING_SIZE_LIMIT_BYTES - 1), + value=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), expected=STRING_SIZE_LIMIT_BYTES - 1, msg="$strLenBytes should handle a string one byte under the size limit", ), StrLenBytesTest( "size_one_under_3byte", - value="寿" * ((STRING_SIZE_LIMIT_BYTES - 1) // 3), + value=lazy(lambda: "寿" * ((STRING_SIZE_LIMIT_BYTES - 1) // 3)), expected=(STRING_SIZE_LIMIT_BYTES - 1) // 3 * 3, msg="$strLenBytes should handle 3-byte chars near the size limit", ), @@ -35,7 +36,7 @@ STRLENBYTES_SIZE_LIMIT_ERROR_TESTS: list[StrLenBytesTest] = [ StrLenBytesTest( "size_at_limit", - value="a" * STRING_SIZE_LIMIT_BYTES, + value=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$strLenBytes should reject a string at the size limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenCP/test_strLenCP_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenCP/test_strLenCP_size_limit.py index 271ecae74..8b1cf33c3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenCP/test_strLenCP_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/strLenCP/test_strLenCP_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,13 +20,13 @@ STRLENCP_SIZE_LIMIT_SUCCESS_TESTS: list[StrLenCPTest] = [ StrLenCPTest( "size_one_under", - value="a" * (STRING_SIZE_LIMIT_BYTES - 1), + value=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), expected=STRING_SIZE_LIMIT_BYTES - 1, msg="$strLenCP should handle a string one byte under the size limit", ), StrLenCPTest( "size_one_under_3byte", - value="寿" * ((STRING_SIZE_LIMIT_BYTES - 1) // 3), + value=lazy(lambda: "寿" * ((STRING_SIZE_LIMIT_BYTES - 1) // 3)), expected=(STRING_SIZE_LIMIT_BYTES - 1) // 3, msg="$strLenCP should count 3-byte chars as code points near the size limit", ), @@ -36,7 +37,7 @@ STRLENCP_SIZE_LIMIT_ERROR_TESTS: list[StrLenCPTest] = [ StrLenCPTest( "size_at_limit", - value="a" * STRING_SIZE_LIMIT_BYTES, + value=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$strLenCP should reject a string at the size limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/strcasecmp/test_strcasecmp_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/strcasecmp/test_strcasecmp_size_limit.py index 5457ea8db..40e7f2385 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/strcasecmp/test_strcasecmp_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/strcasecmp/test_strcasecmp_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -20,14 +21,14 @@ STRCASECMP_SIZE_LIMIT_SUCCESS_TESTS: list[StrcasecmpTest] = [ StrcasecmpTest( "size_both_half_limit_equal", - string1="a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), - string2="a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2), + string1=lazy(lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2)), + string2=lazy(lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2)), expected=0, msg="$strcasecmp should return 0 for identical large strings", ), StrcasecmpTest( "size_one_under_vs_short", - string1="a" * (STRING_SIZE_LIMIT_BYTES - 1), + string1=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), string2="a", expected=1, msg="$strcasecmp should detect length difference with one string near the size limit", @@ -38,7 +39,7 @@ STRCASECMP_SIZE_LIMIT_ERROR_TESTS: list[StrcasecmpTest] = [ StrcasecmpTest( "size_at_limit", - string1="a" * STRING_SIZE_LIMIT_BYTES, + string1=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), string2="a", error_code=STRING_SIZE_LIMIT_ERROR, msg="$strcasecmp should reject a string at the size limit", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrBytes/test_substrBytes_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrBytes/test_substrBytes_size_limit.py index 1558cb958..8d6dc06d9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrBytes/test_substrBytes_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrBytes/test_substrBytes_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -20,7 +21,7 @@ SUBSTRBYTES_SIZE_LIMIT_SUCCESS_TESTS: list[SubstrBytesTest] = [ SubstrBytesTest( "size_one_under_first", - string="a" * (STRING_SIZE_LIMIT_BYTES - 1), + string=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), byte_index=0, byte_count=1, expected="a", @@ -28,15 +29,15 @@ ), SubstrBytesTest( "size_one_under_full", - string="a" * (STRING_SIZE_LIMIT_BYTES - 1), + string=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), byte_index=0, byte_count=STRING_SIZE_LIMIT_BYTES - 1, - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$substrBytes should extract full string one byte under the 16 MB limit", ), SubstrBytesTest( "size_one_under_last", - string="a" * (STRING_SIZE_LIMIT_BYTES - 1), + string=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), byte_index=STRING_SIZE_LIMIT_BYTES - 2, byte_count=1, expected="a", @@ -49,7 +50,7 @@ SUBSTRBYTES_SIZE_LIMIT_ERROR_TESTS: list[SubstrBytesTest] = [ SubstrBytesTest( "size_at_limit", - string="a" * STRING_SIZE_LIMIT_BYTES, + string=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), byte_index=0, byte_count=1, error_code=STRING_SIZE_LIMIT_ERROR, @@ -58,7 +59,7 @@ # 2-byte chars exceeding 16 MB in bytes. SubstrBytesTest( "size_multibyte_at_limit", - string="é" * (STRING_SIZE_LIMIT_BYTES // 2), + string=lazy(lambda: "é" * (STRING_SIZE_LIMIT_BYTES // 2)), byte_index=0, byte_count=1, error_code=STRING_SIZE_LIMIT_ERROR, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrCP/test_substrCP_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrCP/test_substrCP_size_limit.py index 1bb6dbfe7..c0ea8769d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrCP/test_substrCP_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/substrCP/test_substrCP_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -24,7 +25,7 @@ ), SubstrCPTest( "size_one_under", - string="a" * (STRING_SIZE_LIMIT_BYTES - 1), + string=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), index=0, count=1, expected="a", @@ -37,21 +38,21 @@ SUBSTRCP_SIZE_LIMIT_ERROR_TESTS: list[SubstrCPTest] = [ SubstrCPTest( "size_at_limit", - string="a" * STRING_SIZE_LIMIT_BYTES, + string=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$substrCP should reject input string at the 16 MB byte limit", ), # 2-byte chars exceeding 16 MB in bytes. SubstrCPTest( "size_multibyte_at_limit", - string="é" * (STRING_SIZE_LIMIT_BYTES // 2), + string=lazy(lambda: "é" * (STRING_SIZE_LIMIT_BYTES // 2)), error_code=STRING_SIZE_LIMIT_ERROR, msg="$substrCP should reject multi-byte string exceeding 16 MB in bytes", ), # Eager size check: untaken $cond branch with 16 MB string still errors. SubstrCPTest( "size_cond_untaken_branch", - string={"$cond": [True, "hello", "a" * STRING_SIZE_LIMIT_BYTES]}, + string=lazy(lambda: {"$cond": [True, "hello", "a" * STRING_SIZE_LIMIT_BYTES]}), error_code=STRING_SIZE_LIMIT_ERROR, msg="$substrCP should reject 16 MB string in untaken $cond branch", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/toLower/test_toLower_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/toLower/test_toLower_size_limit.py index 37702891e..69a849426 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/toLower/test_toLower_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/toLower/test_toLower_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,8 +20,8 @@ TOLOWER_SIZE_LIMIT_SUCCESS_TESTS: list[ToLowerTest] = [ ToLowerTest( "size_one_under", - value="A" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + value=lazy(lambda: "A" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$toLower should accept input string one byte under the 16 MB limit", ), ] @@ -29,7 +30,7 @@ TOLOWER_SIZE_LIMIT_ERROR_TESTS: list[ToLowerTest] = [ ToLowerTest( "size_at_limit", - value="a" * STRING_SIZE_LIMIT_BYTES, + value=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$toLower should reject input string at the 16 MB byte limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/toUpper/test_toUpper_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/toUpper/test_toUpper_size_limit.py index f63bfb917..3734fe9c7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/toUpper/test_toUpper_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/toUpper/test_toUpper_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,8 +20,8 @@ TOUPPER_SIZE_LIMIT_SUCCESS_TESTS: list[ToUpperTest] = [ ToUpperTest( "size_one_under", - value="a" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="A" * (STRING_SIZE_LIMIT_BYTES - 1), + value=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "A" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$toUpper should accept input string one byte under the 16 MB limit", ), ] @@ -29,7 +30,7 @@ TOUPPER_SIZE_LIMIT_ERROR_TESTS: list[ToUpperTest] = [ ToUpperTest( "size_at_limit", - value="a" * STRING_SIZE_LIMIT_BYTES, + value=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$toUpper should reject input string at the 16 MB byte limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/string/trim/test_trim_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/string/trim/test_trim_size_limit.py index df781ff97..4ae6f97ab 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/string/trim/test_trim_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/string/trim/test_trim_size_limit.py @@ -7,6 +7,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -19,22 +20,24 @@ TRIM_SIZE_LIMIT_SUCCESS_TESTS: list[TrimTest] = [ TrimTest( "size_one_under", - input="a" * (STRING_SIZE_LIMIT_BYTES - 1), - expected="a" * (STRING_SIZE_LIMIT_BYTES - 1), + input=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), + expected=lazy(lambda: "a" * (STRING_SIZE_LIMIT_BYTES - 1)), msg="$trim should accept input one byte under the size limit", ), TrimTest( "size_one_under_2byte", - input="\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", - expected="\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a", + input=lazy(lambda: "\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), + expected=lazy(lambda: "\u00e9" * ((STRING_SIZE_LIMIT_BYTES - 1) // 2) + "a"), msg="$trim should accept 2-byte character input one byte under the size limit", ), # Large input with many leading and trailing trim characters, just under the limit. TrimTest( "size_trim_both_sides", - input="a" * ((STRING_SIZE_LIMIT_BYTES - 6) // 2) - + "hello" - + "a" * ((STRING_SIZE_LIMIT_BYTES - 6) // 2), + input=lazy( + lambda: "a" * ((STRING_SIZE_LIMIT_BYTES - 6) // 2) + + "hello" + + "a" * ((STRING_SIZE_LIMIT_BYTES - 6) // 2) + ), chars="a", expected="hello", msg="$trim should trim many characters from both sides near the size limit", @@ -46,13 +49,13 @@ TRIM_SIZE_LIMIT_ERROR_TESTS: list[TrimTest] = [ TrimTest( "size_at_limit", - input="a" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "a" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$trim should reject input at the BSON string byte limit", ), TrimTest( "size_at_limit_2byte", - input="\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2), + input=lazy(lambda: "\u00e9" * (STRING_SIZE_LIMIT_BYTES // 2)), error_code=STRING_SIZE_LIMIT_ERROR, msg="$trim should reject 2-byte character input at the BSON string byte limit", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_on_error.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_on_error.py index 2a7c82800..aad4ccff9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_on_error.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_on_error.py @@ -11,6 +11,7 @@ assert_expression_result, execute_expression, ) +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( DECIMAL128_NAN, @@ -99,7 +100,7 @@ ), ConvertTest( "on_error_catches_string_size_limit", - input=Binary(b"A" * (STRING_SIZE_LIMIT_BYTES // 2)), + input=lazy(lambda: Binary(b"A" * (STRING_SIZE_LIMIT_BYTES // 2))), to="string", format="hex", on_error="caught", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_size_limit.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_size_limit.py index d15dd8f49..0ff5fab41 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_size_limit.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/convert/test_convert_size_limit.py @@ -12,6 +12,7 @@ execute_expression, ) from documentdb_tests.framework.error_codes import CONVERSION_FAILURE_ERROR, STRING_SIZE_LIMIT_ERROR +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES @@ -20,10 +21,10 @@ CONVERT_STRING_SIZE_LIMIT_SUCCESS_TESTS: list[ConvertTest] = [ ConvertTest( "size_limit_hex_under", - input=Binary(b"A" * (STRING_SIZE_LIMIT_BYTES // 2 - 1)), + input=lazy(lambda: Binary(b"A" * (STRING_SIZE_LIMIT_BYTES // 2 - 1))), to="string", format="hex", - expected="41" * (STRING_SIZE_LIMIT_BYTES // 2 - 1), + expected=lazy(lambda: "41" * (STRING_SIZE_LIMIT_BYTES // 2 - 1)), msg=( "$convert should succeed for BinData-to-string when output is" " just under the size limit" @@ -37,7 +38,7 @@ # hex format: 8_388_608 bytes -> 16_777_216 chars (exactly the limit). ConvertTest( "size_limit_err_hex_at_limit", - input=Binary(b"A" * (STRING_SIZE_LIMIT_BYTES // 2)), + input=lazy(lambda: Binary(b"A" * (STRING_SIZE_LIMIT_BYTES // 2))), to="string", format="hex", error_code=CONVERSION_FAILURE_ERROR, @@ -46,7 +47,7 @@ # base64 format: 12_582_912 bytes -> 16_777_216 chars (exactly the limit). ConvertTest( "size_limit_err_base64_at_limit", - input=Binary(b"A" * (STRING_SIZE_LIMIT_BYTES * 3 // 4)), + input=lazy(lambda: Binary(b"A" * (STRING_SIZE_LIMIT_BYTES * 3 // 4))), to="string", format="base64", error_code=CONVERSION_FAILURE_ERROR, @@ -56,7 +57,7 @@ # error code than other formats. ConvertTest( "size_limit_err_utf8_at_limit", - input=Binary(b"A" * STRING_SIZE_LIMIT_BYTES), + input=lazy(lambda: Binary(b"A" * STRING_SIZE_LIMIT_BYTES)), to="string", format="utf8", error_code=STRING_SIZE_LIMIT_ERROR, @@ -64,7 +65,7 @@ ), ConvertTest( "size_limit_err_oversized_input", - input="A" * STRING_SIZE_LIMIT_BYTES, + input=lazy(lambda: "A" * STRING_SIZE_LIMIT_BYTES), to="int", error_code=STRING_SIZE_LIMIT_ERROR, msg="$convert should reject oversized string in input parameter", @@ -72,21 +73,21 @@ ConvertTest( "size_limit_err_oversized_to", input=42, - to="A" * STRING_SIZE_LIMIT_BYTES, + to=lazy(lambda: "A" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$convert should reject oversized string in to parameter", ), ConvertTest( "size_limit_err_oversized_to_type", input=42, - to={"type": "A" * STRING_SIZE_LIMIT_BYTES}, + to=lazy(lambda: {"type": "A" * STRING_SIZE_LIMIT_BYTES}), error_code=STRING_SIZE_LIMIT_ERROR, msg="$convert should reject oversized string in to.type parameter", ), ConvertTest( "size_limit_err_oversized_to_subtype", input=42, - to={"type": "binData", "subtype": "A" * STRING_SIZE_LIMIT_BYTES}, + to=lazy(lambda: {"type": "binData", "subtype": "A" * STRING_SIZE_LIMIT_BYTES}), error_code=STRING_SIZE_LIMIT_ERROR, msg="$convert should reject oversized string in to.subtype parameter", ), @@ -94,7 +95,7 @@ "size_limit_err_oversized_format", input=42, to="string", - format="A" * STRING_SIZE_LIMIT_BYTES, + format=lazy(lambda: "A" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$convert should reject oversized string in format parameter", ), @@ -102,7 +103,7 @@ "size_limit_err_oversized_byte_order", input=42, to="binData", - byte_order="A" * STRING_SIZE_LIMIT_BYTES, + byte_order=lazy(lambda: "A" * STRING_SIZE_LIMIT_BYTES), error_code=STRING_SIZE_LIMIT_ERROR, msg="$convert should reject oversized string in byteOrder parameter", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/utils/utils.py b/documentdb_tests/compatibility/tests/core/operator/expressions/utils/utils.py index fa290ebdf..ec6d21c75 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/utils/utils.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/utils/utils.py @@ -7,6 +7,7 @@ from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import materialize def build_nested_expr(value, operator, depth): @@ -56,7 +57,7 @@ def execute_project(collection, project): "aggregate": 1, "pipeline": [ {"$documents": [{}]}, - {"$project": {**project, "_id": 0}}, + {"$project": {**materialize(project), "_id": 0}}, ], "cursor": {}, }, @@ -84,13 +85,13 @@ def execute_project_with_insert(collection, document, project): ... ) # Returns result with {"quotient": 3.33...} in firstBatch """ - collection.insert_one(document or {}) + collection.insert_one(materialize(document) or {}) return execute_command( collection, { "aggregate": collection.name, "pipeline": [ - {"$project": {**project, "_id": 0}}, + {"$project": {**materialize(project), "_id": 0}}, ], "cursor": {}, }, @@ -145,7 +146,7 @@ def execute_expression_with_insert(collection, expression, document): Result from execute_command with structure: {"cursor": {"firstBatch": [{"result": }]}} """ - collection.insert_one(document or {}) + collection.insert_one(materialize(document) or {}) return execute_command( collection, { diff --git a/documentdb_tests/compatibility/tests/core/operator/query/arrays/all/test_all_matching_behavior.py b/documentdb_tests/compatibility/tests/core/operator/query/arrays/all/test_all_matching_behavior.py index e299c05d8..847338f1b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/arrays/all/test_all_matching_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/arrays/all/test_all_matching_behavior.py @@ -16,6 +16,7 @@ ) from documentdb_tests.framework.assertions import assertSuccess from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params SCALAR_MATCHING_TESTS: list[QueryTestCase] = [ @@ -388,9 +389,9 @@ def test_all_null_and_missing(collection, test): LARGE_ARRAY_TESTS: list[QueryTestCase] = [ QueryTestCase( id="10000_elements_match", - filter={"a": {"$all": list(range(10000))}}, + filter=lazy(lambda: {"a": {"$all": list(range(10000))}}), doc=[{"_id": 1, "a": list(range(10000))}], - expected=[{"_id": 1, "a": list(range(10000))}], + expected=lazy(lambda: [{"_id": 1, "a": list(range(10000))}]), msg="$all with 10000 elements should match field with 10000 matching elements", ), QueryTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/count/test_count_behavior.py b/documentdb_tests/compatibility/tests/core/operator/stages/count/test_count_behavior.py index f4b7c3a89..be5e3d157 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/count/test_count_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/count/test_count_behavior.py @@ -10,6 +10,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params # Property [Core Counting Behavior]: the output is exactly one document whose @@ -31,7 +32,7 @@ ), StageTestCase( "core_large_collection", - docs=[{"_id": i} for i in range(10_000)], + docs=lazy(lambda: [{"_id": i} for i in range(10_000)]), pipeline=[{"$count": "total"}], expected=[{"total": 10_000}], msg="$count should return correct count for a large number of documents", @@ -90,7 +91,7 @@ ), StageTestCase( "return_type_multiple", - docs=[{"_id": i} for i in range(10_000)], + docs=lazy(lambda: [{"_id": i} for i in range(10_000)]), pipeline=[ {"$count": "n"}, {"$addFields": {"type": {"$type": "$n"}}}, diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_result_size.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_result_size.py index bc547278c..e7e225790 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_result_size.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/test_lookup_result_size.py @@ -12,6 +12,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params # Property [Large Result Sets]: joining many matching documents succeeds @@ -39,7 +40,9 @@ LookupTestCase( "joined_array_exceeds_16mb", docs=[{"_id": 1, "lf": "m"}], - foreign_docs=[{"_id": i, "ff": "m", "data": "x" * 1_000_000} for i in range(20)], + foreign_docs=lazy( + lambda: [{"_id": i, "ff": "m", "data": "x" * 1_000_000} for i in range(20)] + ), pipeline=[ { "$lookup": { diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/utils/lookup_common.py b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/utils/lookup_common.py index 1b5b40a1c..e278a84d0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/lookup/utils/lookup_common.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/lookup/utils/lookup_common.py @@ -10,6 +10,7 @@ from documentdb_tests.compatibility.tests.core.operator.stages.utils.stage_test_case import ( StageTestCase, ) +from documentdb_tests.framework.lazy_payload import materialize FOREIGN = object() @@ -38,13 +39,13 @@ def setup_lookup( if test_case.docs is not None: db.create_collection(collection.name) if test_case.docs: - collection.insert_many(test_case.docs) + collection.insert_many(materialize(test_case.docs)) # Set up foreign collection if test_case.foreign_docs is not None: db.create_collection(foreign_name) if test_case.foreign_docs: - db[foreign_name].insert_many(test_case.foreign_docs) + db[foreign_name].insert_many(materialize(test_case.foreign_docs)) if test_case.foreign_indexes: db[foreign_name].create_indexes(test_case.foreign_indexes) diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/out/test_out_write_properties.py b/documentdb_tests/compatibility/tests/core/operator/stages/out/test_out_write_properties.py index 26aeefe18..55bd81ef3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/out/test_out_write_properties.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/out/test_out_write_properties.py @@ -29,6 +29,7 @@ assertSuccess, ) from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params # Property [Write Behavior - Auto-Generated _id]: documents with _id removed @@ -266,7 +267,7 @@ def test_out_bson_round_trip(collection, test_case: OutTestCase): OUT_LARGE_DOCUMENT_TESTS: list[OutTestCase] = [ OutTestCase( "large_doc", - docs=[{"_id": 1, "data": "x" * (15 * 1_024 * 1_024)}], + docs=lazy(lambda: [{"_id": 1, "data": "x" * (15 * 1_024 * 1_024)}]), expected=[{"_id": 1}], msg="$out should successfully write a 15 MB document", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/redact/test_redact_descend_structure.py b/documentdb_tests/compatibility/tests/core/operator/stages/redact/test_redact_descend_structure.py index 37a9505e3..5cc4437f3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/redact/test_redact_descend_structure.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/redact/test_redact_descend_structure.py @@ -16,6 +16,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params # Property [Recursion Depth and Scale]: $redact descends through deep nesting and @@ -23,9 +24,13 @@ REDACT_STRUCTURAL_TESTS: list[StageTestCase] = [ StageTestCase( "structural_large_array_descended_element_wise", - docs=[{"_id": 1, "items": [{"n": i, "sub": {"secret": True}} for i in range(10_000)]}], + docs=lazy( + lambda: [ + {"_id": 1, "items": [{"n": i, "sub": {"secret": True}} for i in range(10_000)]} + ] + ), pipeline=[{"$redact": {"$cond": [{"$eq": ["$secret", True]}, "$$PRUNE", "$$DESCEND"]}}], - expected=[{"_id": 1, "items": [{"n": i} for i in range(10_000)]}], + expected=lazy(lambda: [{"_id": 1, "items": [{"n": i} for i in range(10_000)]}]), msg="$redact under $$DESCEND should descend element-wise into a large array of " "embedded documents without error", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/replaceRoot/test_replaceRoot_limits.py b/documentdb_tests/compatibility/tests/core/operator/stages/replaceRoot/test_replaceRoot_limits.py index 78b574dbc..1f89e01dd 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/replaceRoot/test_replaceRoot_limits.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/replaceRoot/test_replaceRoot_limits.py @@ -24,6 +24,7 @@ OVERFLOW_ERROR, ) from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params @@ -61,7 +62,7 @@ def _nest(depth: int, leaf: Any) -> Any: REPLACEROOT_SIZE_BOUNDARY_TESTS: list[StageTestCase] = [ StageTestCase( "size_boundary_max_output_accepted", - docs=[{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}], + docs=lazy(lambda: [{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}]), pipeline=[ { "$replaceRoot": { @@ -95,7 +96,7 @@ def _nest(depth: int, leaf: Any) -> Any: REPLACEROOT_SIZE_LIMIT_ERROR_TESTS: list[StageTestCase] = [ StageTestCase( "size_limit_one_over_rejected", - docs=[{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}], + docs=lazy(lambda: [{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}]), pipeline=[ { "$replaceRoot": { diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/replaceWith/test_replaceWith_limits.py b/documentdb_tests/compatibility/tests/core/operator/stages/replaceWith/test_replaceWith_limits.py index 26801d609..5d2b5291f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/replaceWith/test_replaceWith_limits.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/replaceWith/test_replaceWith_limits.py @@ -23,6 +23,7 @@ OVERFLOW_ERROR, ) from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params @@ -59,7 +60,7 @@ def _nest(depth: int, leaf: Any) -> Any: REPLACEWITH_SIZE_BOUNDARY_TESTS: list[StageTestCase] = [ StageTestCase( "size_boundary_max_output_accepted", - docs=[{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}], + docs=lazy(lambda: [{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}]), pipeline=[ {"$replaceWith": {"$mergeObjects": ["$data", {"pad": "y" * BOUNDARY_PAD_BYTES}]}}, {"$project": {"_id": 0, "size": {"$bsonSize": "$$ROOT"}}}, @@ -87,7 +88,7 @@ def _nest(depth: int, leaf: Any) -> Any: REPLACEWITH_SIZE_LIMIT_ERROR_TESTS: list[StageTestCase] = [ StageTestCase( "size_limit_one_over_rejected", - docs=[{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}], + docs=lazy(lambda: [{"_id": 1, "data": {"s": "x" * BIG_STORED_STRING_BYTES}}]), pipeline=[ {"$replaceWith": {"$mergeObjects": ["$data", {"pad": "y" * OVER_LIMIT_PAD_BYTES}]}} ], diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/test_stages_position_lookup.py b/documentdb_tests/compatibility/tests/core/operator/stages/test_stages_position_lookup.py index aaff93e22..7f2ddab04 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/test_stages_position_lookup.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/test_stages_position_lookup.py @@ -13,6 +13,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import DOUBLE_ZERO @@ -256,7 +257,9 @@ LookupTestCase( "lookup_then_unwind_exceeds_16mb", docs=[{"_id": 1, "lf": "m"}], - foreign_docs=[{"_id": i, "ff": "m", "data": "x" * 1_000_000} for i in range(20)], + foreign_docs=lazy( + lambda: [{"_id": i, "ff": "m", "data": "x" * 1_000_000} for i in range(20)] + ), pipeline=[ { "$lookup": { diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/unset/test_unset_paths.py b/documentdb_tests/compatibility/tests/core/operator/stages/unset/test_unset_paths.py index fe9165560..fbc043255 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/unset/test_unset_paths.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/unset/test_unset_paths.py @@ -13,6 +13,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params # Property [Dotted Path Removal]: dot notation removes a nested field within a @@ -137,10 +138,12 @@ UNSET_LARGE_FIELD_COUNT_TESTS: list[StageTestCase] = [ StageTestCase( "large_field_count_array_form", - docs=[ - {"_id": 1, **{f"f{i}": i for i in range(10_000)}}, - ], - pipeline=[{"$unset": [f"f{i}" for i in range(10_000)]}], + docs=lazy( + lambda: [ + {"_id": 1, **{f"f{i}": i for i in range(10_000)}}, + ] + ), + pipeline=lazy(lambda: [{"$unset": [f"f{i}" for i in range(10_000)]}]), expected=[{"_id": 1}], msg="$unset should handle a large number of fields in the array form", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/utils/stage_test_case.py b/documentdb_tests/compatibility/tests/core/operator/stages/utils/stage_test_case.py index 0fa6bb76e..1b1e7e3e7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/utils/stage_test_case.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/utils/stage_test_case.py @@ -11,6 +11,7 @@ from pymongo.collection import Collection from pymongo.operations import IndexModel +from documentdb_tests.framework.lazy_payload import materialize from documentdb_tests.framework.target_collection import TargetCollection from documentdb_tests.framework.test_case import BaseTestCase @@ -48,7 +49,7 @@ def populate_collection(collection: Collection, test_case: StageTestCase) -> Col writable = test_case.target_collection.writable(collection, coll) if test_case.docs: - writable.insert_many(test_case.docs) + writable.insert_many(materialize(test_case.docs)) if test_case.indexes: writable.create_indexes(test_case.indexes) return coll diff --git a/documentdb_tests/compatibility/tests/core/operator/system-stages/listLocalSessions/test_listLocalSessions_success.py b/documentdb_tests/compatibility/tests/core/operator/system-stages/listLocalSessions/test_listLocalSessions_success.py index 0ef6e4f33..6a501ce37 100644 --- a/documentdb_tests/compatibility/tests/core/operator/system-stages/listLocalSessions/test_listLocalSessions_success.py +++ b/documentdb_tests/compatibility/tests/core/operator/system-stages/listLocalSessions/test_listLocalSessions_success.py @@ -10,6 +10,7 @@ ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.lazy_payload import lazy from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.property_checks import Eq @@ -161,13 +162,15 @@ ), StageTestCase( "users_large_array_10000", - pipeline=[ - { - "$listLocalSessions": { - "users": [{"user": f"u{i}", "db": "admin"} for i in range(10_000)] + pipeline=lazy( + lambda: [ + { + "$listLocalSessions": { + "users": [{"user": f"u{i}", "db": "admin"} for i in range(10_000)] + } } - } - ], + ] + ), expected={"ok": Eq(1.0), "cursor": {"firstBatch": Eq([])}}, msg="$listLocalSessions should accept a large users array", ), @@ -217,7 +220,9 @@ ), StageTestCase( "user_long", - pipeline=[{"$listLocalSessions": {"users": [{"user": "x" * 10_000, "db": "admin"}]}}], + pipeline=lazy( + lambda: [{"$listLocalSessions": {"users": [{"user": "x" * 10_000, "db": "admin"}]}}] + ), expected={"ok": Eq(1.0), "cursor": {"firstBatch": Eq([])}}, msg="$listLocalSessions should accept a very long user with no " "operator-specific length limit", diff --git a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py index c4785c54e..2abeefd3f 100644 --- a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py +++ b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py @@ -10,6 +10,7 @@ from pymongo.collection import Collection from pymongo.database import Database +from documentdb_tests.framework.lazy_payload import materialize from documentdb_tests.framework.target_collection import ( SiblingCollection, TargetCollection, @@ -106,7 +107,7 @@ def prepare(self, db: Database, collection: Collection) -> Collection: if target.name not in target.database.list_collection_names(): target.database.create_collection(target.name) if self.docs: - target.insert_many(self.docs) + target.insert_many(materialize(self.docs)) if self.siblings: for sibling in self.siblings: sibling.create(db, resolved) diff --git a/documentdb_tests/conftest.py b/documentdb_tests/conftest.py index 90bf88938..ad11014b2 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -25,6 +25,10 @@ from documentdb_tests.framework.error_codes_validator import ( # noqa: E402 validate_error_codes_sorted, ) +from documentdb_tests.framework.large_payload_guard import ( # noqa: E402 + PARAM_SIZE_LIMIT_BYTES, + exceeds_size_limit, +) from documentdb_tests.framework.preconditions import ( # noqa: E402 REQUIRES_MARKER, detect_capabilities, @@ -410,7 +414,23 @@ def pytest_collection_modifyitems(session, config, items): # Validate framework error code invariants structure_errors.extend(validate_error_codes_sorted()) - if structure_errors or format_errors: + # No test may embed a large payload in its parametrized data. Such values are + # duplicated across xdist workers and held for the whole session; they belong + # behind Lazy so they are built at test time. + large_param_errors = [] + for item in items: + callspec = getattr(item, "callspec", None) + if callspec is None: + continue + for value in callspec.params.values(): + if exceeds_size_limit(value): + large_param_errors.append( + f" {item.nodeid} exceeds the {PARAM_SIZE_LIMIT_BYTES:,}-byte " + "parametrized-data limit" + ) + break + + if structure_errors or format_errors or large_param_errors: import sys if structure_errors: @@ -425,6 +445,15 @@ def pytest_collection_modifyitems(session, config, items): print("\n".join(file_errors), file=sys.stderr) print("\nSee docs/testing/TEST_FORMAT.md for rules.\n", file=sys.stderr) + if large_param_errors: + print("\n❌ Large Test Payloads:", file=sys.stderr) + print("\n".join(large_param_errors), file=sys.stderr) + print( + "\nWrap large values in lazy(...) so they are built at test time " + "instead of held in the collected test data.\n", + file=sys.stderr, + ) + pytest.exit("Test validation failed", returncode=1) diff --git a/documentdb_tests/framework/assertions.py b/documentdb_tests/framework/assertions.py index b4326463e..ebd12e287 100644 --- a/documentdb_tests/framework/assertions.py +++ b/documentdb_tests/framework/assertions.py @@ -11,6 +11,7 @@ from bson import Decimal128, Int64 from documentdb_tests.framework.infra_exceptions import INFRA_EXCEPTION_TYPES as _INFRA_TYPES +from documentdb_tests.framework.lazy_payload import materialize from documentdb_tests.framework.property_checks import _FIELD_ABSENT, Check, PerDoc _MAX_REPR_LEN = 1000 @@ -165,6 +166,7 @@ def assertSuccess( transform: Optional callback to transform result before comparison ignore_doc_order: If True, compare lists ignoring order (duplicates still matter) """ + expected = materialize(expected) if isinstance(result, Exception): if isinstance(result, _INFRA_TYPES): raise result @@ -338,6 +340,7 @@ def assertResult( assertResult(result, expected=[{"r": [3, 1, 2]}], ignore_order_in=["r"]) assertResult(result, expected={"ok": 1.0}, raw_res=True) # Raw command result """ + expected = materialize(expected) if error_code is not None: assertFailureCode(result, error_code, msg) elif isinstance(expected, PerDoc) or ( diff --git a/documentdb_tests/framework/executor.py b/documentdb_tests/framework/executor.py index 49febfa3b..e028b4a11 100644 --- a/documentdb_tests/framework/executor.py +++ b/documentdb_tests/framework/executor.py @@ -8,6 +8,8 @@ from bson.codec_options import CodecOptions +from documentdb_tests.framework.lazy_payload import materialize + TZ_AWARE_CODEC: CodecOptions = CodecOptions(tz_aware=True, tzinfo=timezone.utc) @@ -27,7 +29,7 @@ def execute_command(collection, command: Dict, codec_options=TZ_AWARE_CODEC, ses """ try: db = collection.database - result = db.command(command, codec_options=codec_options, session=session) + result = db.command(materialize(command), codec_options=codec_options, session=session) return result except Exception as e: return e @@ -47,7 +49,7 @@ def execute_admin_command(collection, command: Dict, session=None) -> Any: """ try: db = collection.database.client.admin - result = db.command(command, session=session) + result = db.command(materialize(command), session=session) return result except Exception as e: return e diff --git a/documentdb_tests/framework/large_payload_guard.py b/documentdb_tests/framework/large_payload_guard.py new file mode 100644 index 000000000..6b53859e9 --- /dev/null +++ b/documentdb_tests/framework/large_payload_guard.py @@ -0,0 +1,53 @@ +"""Detection of oversized test payloads. + +A test's parametrized data is kept by pytest for the whole session and copied +onto every xdist worker, so a large value embedded there is a heavy, needless +footprint. This measures a value's size so the collection guardrail can reject +such tests and point them at ``lazy``, which defers construction to test time. +""" + +from __future__ import annotations + +import sys +from typing import Any + +from documentdb_tests.framework.lazy_payload import Lazy + +# A test's parametrized data should not embed a payload larger than this. +PARAM_SIZE_LIMIT_BYTES = 1_000_000 + + +def _deep_size(value: Any, seen: set[int], budget: int) -> int: + """Sum the byte size of ``value``, stopping early once it exceeds ``budget``. + + A ``Lazy`` is treated as its small placeholder, not the value it would build, + so deferred payloads count as tiny. + """ + if isinstance(value, Lazy) or id(value) in seen: + return 0 + seen.add(id(value)) + total = sys.getsizeof(value, 0) + if isinstance(value, (str, bytes, bytearray)): + return total + if isinstance(value, dict): + for key, item in value.items(): + total += _deep_size(key, seen, budget) + _deep_size(item, seen, budget) + if total > budget: + return total + elif isinstance(value, (list, tuple, set, frozenset)): + for item in value: + total += _deep_size(item, seen, budget) + if total > budget: + return total + elif hasattr(value, "__dict__"): + total += _deep_size(vars(value), seen, budget) + return total + + +def exceeds_size_limit(value: Any) -> bool: + """Return whether ``value``'s byte size exceeds ``PARAM_SIZE_LIMIT_BYTES``. + + Sizing stops as soon as the limit is passed, so ordinary small values cost + only a shallow walk. + """ + return _deep_size(value, set(), PARAM_SIZE_LIMIT_BYTES) > PARAM_SIZE_LIMIT_BYTES diff --git a/documentdb_tests/framework/lazy_payload.py b/documentdb_tests/framework/lazy_payload.py new file mode 100644 index 000000000..b3ed2f4a8 --- /dev/null +++ b/documentdb_tests/framework/lazy_payload.py @@ -0,0 +1,70 @@ +"""Deferred construction of large test payloads. + +Some tests operate on multi-megabyte strings and byte buffers. Building those +values directly in the parametrized test data keeps every copy in memory for the +whole session, on every xdist worker, which is a large and needless footprint. + +Wrap the large value in ``Lazy`` so the test data holds only a small builder. +The framework calls ``materialize`` where it consumes the value (building the +command and comparing the expected result), so the large value is created only +while its own test runs and is freed afterward. Plain values pass through +``materialize`` untouched, so wrapping is needed only on the large fields. +""" + +from __future__ import annotations + +from typing import Any, Callable, TypeVar, cast + +T = TypeVar("T") + + +class Lazy: + """A value built on demand rather than when the test data is defined.""" + + def __init__(self, build: Callable[[], Any]) -> None: + self.build = build + + def resolve(self) -> Any: + return materialize(self.build()) + + def __repr__(self) -> str: + return "Lazy(...)" + + +def lazy(build: Callable[[], T]) -> T: + """Defer building a value until test time, keeping the field's normal type. + + Returns a ``Lazy`` at runtime but is typed as the value ``build`` produces, so + a field annotated ``list[dict]`` can hold ``lazy(lambda: [...])`` without + widening its type and ordinary consumers still see the built type. + ``materialize`` replaces it with the built value where the framework uses it. + """ + return cast(T, Lazy(build)) + + +def materialize(value: Any) -> Any: + """Return ``value`` with any ``Lazy`` in it replaced by its built result. + + Recurses through dicts, lists, and tuples so a ``Lazy`` nested inside a + command or an expected document is resolved too. When a container holds no + ``Lazy``, the original object is returned unchanged, so calling this on + ordinary values on a hot path is cheap and allocation-free. + """ + if isinstance(value, Lazy): + return value.resolve() + if isinstance(value, dict): + built_dict = {key: materialize(item) for key, item in value.items()} + if any(built_dict[key] is not value[key] for key in value): + return built_dict + return value + if isinstance(value, list): + built_list = [materialize(item) for item in value] + if any(new is not old for new, old in zip(built_list, value)): + return built_list + return value + if isinstance(value, tuple): + built_tuple = tuple(materialize(item) for item in value) + if any(new is not old for new, old in zip(built_tuple, value)): + return built_tuple + return value + return value diff --git a/documentdb_tests/framework/target_collection.py b/documentdb_tests/framework/target_collection.py index 82b9cbf4d..2870d45c6 100644 --- a/documentdb_tests/framework/target_collection.py +++ b/documentdb_tests/framework/target_collection.py @@ -15,6 +15,8 @@ from pymongo.database import Database from pymongo.operations import IndexModel +from documentdb_tests.framework.lazy_payload import materialize + @dataclass(frozen=True) class TargetCollection: @@ -406,6 +408,6 @@ def create(self, db: Database, collection: Collection) -> None: if self.indexes: db[name].create_indexes(self.indexes) if self.docs: - db[name].insert_many(self.docs) + db[name].insert_many(materialize(self.docs)) if self.indexes: db[name].create_indexes(self.indexes)