diff --git a/README.md b/README.md index 56b0a78..218ecb0 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ from tests.common.assertions import ( assert_document_match(actual, expected, ignore_id=True) # Compare lists of documents -assert_documents_match(actual_list, expected_list, ignore_order=True) +assert_documents_match(actual_list, expected_list, ignore_doc_order=True) # Check field existence assert_field_exists(document, "user.name") diff --git a/documentdb_tests/compatibility/tests/__init__.py b/documentdb_tests/compatibility/tests/__init__.py index 91b3213..3492497 100644 --- a/documentdb_tests/compatibility/tests/__init__.py +++ b/documentdb_tests/compatibility/tests/__init__.py @@ -2,23 +2,4 @@ DocumentDB Functional Tests End-to-end functional testing suite for DocumentDB. - -This package provides test utilities and common assertions for testing -DocumentDB functionality. """ - -from documentdb_tests.framework.assertions import ( - assert_count, - assert_document_match, - assert_documents_match, - assert_field_exists, - assert_field_not_exists, -) - -__all__ = [ - "assert_count", - "assert_document_match", - "assert_documents_match", - "assert_field_exists", - "assert_field_not_exists", -] diff --git a/documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections_create.py b/documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections_create.py index 0f1acd4..9df2989 100644 --- a/documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections_create.py +++ b/documentdb_tests/compatibility/tests/core/collections/capped_collections/test_capped_collections_create.py @@ -1,43 +1,18 @@ """ Tests for capped collection operations. -NOTE: This test was added by design to demonstrate how the Result Analyzer -automatically detects and categorizes unsupported features (error code 115). -When run against DocumentDB, this test will fail with error code 115, which -the analyzer will correctly categorize as UNSUPPORTED rather than FAIL. +Capped collections are fixed-size collections that maintain insertion order. +This feature may not be supported on all engines. """ import pytest -from pymongo.errors import OperationFailure + +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccessPartial @pytest.mark.collection_mgmt -def test_create_capped_collection(database_client): - """ - Test creating a capped collection. - - Capped collections are fixed-size collections that maintain insertion order. - This feature is not supported in DocumentDB and should return error code 115. - - Expected behavior: - - Creates a capped collection successfully - """ - collection_name = "capped_test_collection" - - try: - # Attempt to create capped collection - database_client.create_collection( - collection_name, - capped=True, - size=100000 - ) - - # Verify it's actually capped - collection_info = database_client[collection_name].options() - assert collection_info.get("capped") is True, "Collection should be capped" - - # Cleanup - database_client.drop_collection(collection_name) - - except OperationFailure as e: - raise +def test_create_capped_collection(collection): + """Test creating a capped collection.""" + result = execute_command(collection, {"create": collection.name + "_capped", "capped": True, "size": 100000}) + assertSuccessPartial(result, {"ok": 1.0}, "Should create capped collection") diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/group/test_group_stage.py b/documentdb_tests/compatibility/tests/core/operator/stages/group/test_group_stage.py index 3af0710..9d59905 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/group/test_group_stage.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/group/test_group_stage.py @@ -6,123 +6,94 @@ import pytest +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess + @pytest.mark.aggregate @pytest.mark.smoke def test_group_with_count(collection): """Test $group stage with count aggregation.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "department": "Engineering", "salary": 100000}, - {"name": "Bob", "department": "Engineering", "salary": 90000}, - {"name": "Charlie", "department": "Sales", "salary": 80000}, - {"name": "David", "department": "Sales", "salary": 75000}, + {"a": "A", "b": 100}, + {"a": "A", "b": 90}, + {"a": "B", "b": 80}, + {"a": "B", "b": 75}, ]) + pipeline = [{"$group": {"_id": "$a", "count": {"$sum": 1}}}] + result = execute_command(collection, {"aggregate": collection.name, "pipeline": pipeline, "cursor": {}}) - # Act - Execute aggregation to count documents by department - pipeline = [{"$group": {"_id": "$department", "count": {"$sum": 1}}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 departments" - - # Convert to dict for easier verification - dept_counts = {doc["_id"]: doc["count"] for doc in result} - assert dept_counts["Engineering"] == 2, "Expected 2 employees in Engineering" - assert dept_counts["Sales"] == 2, "Expected 2 employees in Sales" + expected = [ + {"_id": "A", "count": 2}, + {"_id": "B", "count": 2} + ] + assertSuccess(result, expected, "Should group and count by a", ignore_doc_order=True) @pytest.mark.aggregate def test_group_with_sum(collection): """Test $group stage with sum aggregation.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "department": "Engineering", "salary": 100000}, - {"name": "Bob", "department": "Engineering", "salary": 90000}, - {"name": "Charlie", "department": "Sales", "salary": 80000}, + {"a": "A", "b": 100}, + {"a": "A", "b": 90}, + {"a": "B", "b": 80}, ]) + pipeline = [{"$group": {"_id": "$a", "total": {"$sum": "$b"}}}] + result = execute_command(collection, {"aggregate": collection.name, "pipeline": pipeline, "cursor": {}}) - # Act - Execute aggregation to sum salaries by department - pipeline = [{"$group": {"_id": "$department", "totalSalary": {"$sum": "$salary"}}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 departments" - - # Convert to dict for easier verification - dept_salaries = {doc["_id"]: doc["totalSalary"] for doc in result} - assert dept_salaries["Engineering"] == 190000, "Expected total Engineering salary of 190000" - assert dept_salaries["Sales"] == 80000, "Expected total Sales salary of 80000" + expected = [ + {"_id": "A", "total": 190}, + {"_id": "B", "total": 80} + ] + assertSuccess(result, expected, "Should sum by group", ignore_doc_order=True) @pytest.mark.aggregate def test_group_with_avg(collection): """Test $group stage with average aggregation.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "department": "Engineering", "salary": 100000}, - {"name": "Bob", "department": "Engineering", "salary": 90000}, - {"name": "Charlie", "department": "Sales", "salary": 80000}, + {"a": "A", "b": 100}, + {"a": "A", "b": 90}, + {"a": "B", "b": 80}, ]) + pipeline = [{"$group": {"_id": "$a", "avg": {"$avg": "$b"}}}] + result = execute_command(collection, {"aggregate": collection.name, "pipeline": pipeline, "cursor": {}}) - # Act - Execute aggregation to calculate average salary by department - pipeline = [{"$group": {"_id": "$department", "avgSalary": {"$avg": "$salary"}}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 departments" - - # Convert to dict for easier verification - dept_avg = {doc["_id"]: doc["avgSalary"] for doc in result} - assert dept_avg["Engineering"] == 95000, "Expected average Engineering salary of 95000" - assert dept_avg["Sales"] == 80000, "Expected average Sales salary of 80000" + expected = [ + {"_id": "A", "avg": 95.0}, + {"_id": "B", "avg": 80.0} + ] + assertSuccess(result, expected, "Should calculate average by group", ignore_doc_order=True) @pytest.mark.aggregate def test_group_with_min_max(collection): """Test $group stage with min and max aggregations.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "department": "Engineering", "salary": 100000}, - {"name": "Bob", "department": "Engineering", "salary": 90000}, - {"name": "Charlie", "department": "Sales", "salary": 80000}, + {"a": "A", "b": 100}, + {"a": "A", "b": 90}, + {"a": "B", "b": 80}, ]) + pipeline = [{"$group": {"_id": "$a", "min": {"$min": "$b"}, "max": {"$max": "$b"}}}] + result = execute_command(collection, {"aggregate": collection.name, "pipeline": pipeline, "cursor": {}}) - # Act - Execute aggregation to find min and max salary by department - pipeline = [ - { - "$group": { - "_id": "$department", - "minSalary": {"$min": "$salary"}, - "maxSalary": {"$max": "$salary"}, - } - } + expected = [ + {"_id": "A", "min": 90, "max": 100}, + {"_id": "B", "min": 80, "max": 80} ] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 departments" - - # Verify Engineering department - eng_dept = next(doc for doc in result if doc["_id"] == "Engineering") - assert eng_dept["minSalary"] == 90000, "Expected min Engineering salary of 90000" - assert eng_dept["maxSalary"] == 100000, "Expected max Engineering salary of 100000" + assertSuccess(result, expected, "Should find min and max by group", ignore_doc_order=True) @pytest.mark.aggregate def test_group_all_documents(collection): """Test $group stage grouping all documents (using null as _id).""" - # Arrange - Insert test data collection.insert_many([ - {"item": "A", "quantity": 5}, - {"item": "B", "quantity": 10}, - {"item": "A", "quantity": 3}, + {"a": "A", "b": 5}, + {"a": "B", "b": 10}, + {"a": "A", "b": 3}, ]) + pipeline = [{"$group": {"_id": None, "total": {"$sum": "$b"}}}] + result = execute_command(collection, {"aggregate": collection.name, "pipeline": pipeline, "cursor": {}}) - # Act - Execute aggregation to sum quantities across all documents - pipeline = [{"$group": {"_id": None, "totalQuantity": {"$sum": "$quantity"}}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 1, "Expected single result grouping all documents" - assert result[0]["totalQuantity"] == 18, "Expected total quantity of 18" + expected = [{"_id": None, "total": 18}] + assertSuccess(result, expected, "Should sum across all documents") diff --git a/documentdb_tests/compatibility/tests/core/operator/stages/match/test_match_stage.py b/documentdb_tests/compatibility/tests/core/operator/stages/match/test_match_stage.py index 0643fea..a3aeee2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/stages/match/test_match_stage.py +++ b/documentdb_tests/compatibility/tests/core/operator/stages/match/test_match_stage.py @@ -6,81 +6,68 @@ import pytest +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess + @pytest.mark.aggregate @pytest.mark.smoke def test_match_simple_filter(collection): """Test $match stage with simple equality filter.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "status": "active"}, - {"name": "Bob", "age": 25, "status": "active"}, - {"name": "Charlie", "age": 35, "status": "inactive"}, + {"_id": 0, "a": "A", "b": 30, "c": "active"}, + {"_id": 1, "a": "B", "b": 25, "c": "active"}, + {"_id": 2, "a": "C", "b": 35, "c": "inactive"}, ]) - - # Act - Execute aggregation with $match stage - pipeline = [{"$match": {"status": "active"}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 active users" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Bob"}, "Expected Alice and Bob" + result = execute_command(collection, {"aggregate": collection.name, "pipeline": [{"$match": {"c": "active"}}], "cursor": {}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30, "c": "active"}, + {"_id": 1, "a": "B", "b": 25, "c": "active"} + ] + assertSuccess(result, expected, "Should match active documents") @pytest.mark.aggregate def test_match_with_comparison_operator(collection): """Test $match stage with comparison operators.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Execute aggregation with $match using $gt - pipeline = [{"$match": {"age": {"$gt": 25}}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 users with age > 25" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Charlie"}, "Expected Alice and Charlie" + result = execute_command(collection, {"aggregate": collection.name, "pipeline": [{"$match": {"b": {"$gt": 25}}}], "cursor": {}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 2, "a": "C", "b": 35} + ] + assertSuccess(result, expected, "Should match documents with b > 25") @pytest.mark.aggregate def test_match_multiple_conditions(collection): """Test $match stage with multiple filter conditions.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "city": "NYC"}, - {"name": "Bob", "age": 25, "city": "SF"}, - {"name": "Charlie", "age": 35, "city": "NYC"}, + {"_id": 0, "a": "A", "b": 30, "c": "NYC"}, + {"_id": 1, "a": "B", "b": 25, "c": "SF"}, + {"_id": 2, "a": "C", "b": 35, "c": "SF"}, ]) - - # Act - Execute aggregation with multiple conditions in $match - pipeline = [{"$match": {"city": "NYC", "age": {"$gte": 30}}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 users from NYC with age >= 30" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Charlie"}, "Expected Alice and Charlie" + result = execute_command(collection, {"aggregate": collection.name, "pipeline": [{"$match": {"c": "NYC", "b": {"$gte": 30}}}], "cursor": {}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30, "c": "NYC"} + ] + assertSuccess(result, expected, "Should match multiple conditions") @pytest.mark.aggregate -@pytest.mark.find def test_match_empty_result(collection): """Test $match stage that matches no documents.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "status": "active"}, - {"name": "Bob", "status": "active"}, + {"_id": 0, "a": "A", "b": "active"}, + {"_id": 1, "a": "B", "b": "active"}, ]) - - # Act - Execute aggregation with $match that matches nothing - pipeline = [{"$match": {"status": "inactive"}}] - result = list(collection.aggregate(pipeline)) - - # Assert - Verify empty result - assert result == [], "Expected empty result when no documents match" + result = execute_command(collection, {"aggregate": collection.name, "pipeline": [{"$match": {"b": "inactive"}}], "cursor": {}}) + + assertSuccess(result, [], "Should return empty result when no match") diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_basic_queries.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_basic_queries.py index ba40c61..cf756b6 100644 --- a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_basic_queries.py +++ b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_basic_queries.py @@ -1,4 +1,4 @@ -""" +""" Basic find operation tests. Tests for fundamental find() and findOne() operations. @@ -6,129 +6,123 @@ import pytest -from documentdb_tests.framework.assertions import assert_document_match +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess, assertFailureCode @pytest.mark.find @pytest.mark.smoke def test_find_all_documents(collection): """Test finding all documents in a collection.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "status": "active"}, - {"name": "Bob", "age": 25, "status": "active"}, - {"name": "Charlie", "age": 35, "status": "inactive"}, + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 1, "a": 1, "b": 2, "c": 1}, + {"_id": 2, "a": 2, "b": 1, "c": 1}, ]) - - # Act - Execute find operation - result = list(collection.find()) - - # Assert - Verify all documents are returned - assert len(result) == 3, "Expected to find 3 documents" - - # Verify document content - names = {doc["name"] for doc in result} - assert names == {"Alice", "Bob", "Charlie"}, "Expected to find all three users" + result = execute_command(collection, {"find": collection.name}) + + expected = [ + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 1, "a": 1, "b": 2, "c": 1}, + {"_id": 2, "a": 2, "b": 1, "c": 1}, + ] + assertSuccess(result, expected, "Should return all 3 documents") @pytest.mark.find @pytest.mark.smoke def test_find_with_filter(collection): """Test find operation with a simple equality filter.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "status": "active"}, - {"name": "Bob", "age": 25, "status": "active"}, - {"name": "Charlie", "age": 35, "status": "inactive"}, + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 1, "a": 1, "b": 2, "c": 1}, + {"_id": 2, "a": 2, "b": 1, "c": 1}, ]) - - # Act - Execute find with filter - result = list(collection.find({"status": "active"})) - - # Assert - Verify only active users are returned - assert len(result) == 2, "Expected to find 2 active users" - - # Verify all returned documents have status "active" - for doc in result: - assert doc["status"] == "active", "All returned documents should have status 'active'" - - # Verify correct users are returned - names = {doc["name"] for doc in result} - assert names == {"Alice", "Bob"}, "Expected to find Alice and Bob" + result = execute_command(collection, {"find": collection.name, "filter": {"b": 1}}) + + expected = [ + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 2, "a": 2, "b": 1, "c": 1}, + ] + assertSuccess(result, expected, "Should return only documents with b=1") @pytest.mark.find def test_find_one(collection): """Test findOne operation returns a single document.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 1, "a": 1, "b": 2, "c": 1}, + {"_id": 2, "a": 2, "b": 1, "c": 1}, ]) - - # Act - Execute findOne - result = collection.find_one({"name": "Alice"}) - - # Assert - Verify document is returned and content matches - assert result is not None, "Expected to find a document" - assert_document_match(result, {"name": "Alice", "age": 30}) + result = execute_command(collection, {"find": collection.name, "filter": {"b": 1}, "limit": 1}) + + expected = [{"_id": 0, "a": 1, "b": 1, "c": 1}] + assertSuccess(result, expected, "Should return single document with limit=1") @pytest.mark.find def test_find_one_not_found(collection): """Test findOne returns None when no document matches.""" - # Arrange - Insert test data - collection.insert_one({"name": "Alice", "age": 30}) - - # Act - Execute findOne with non-matching filter - result = collection.find_one({"name": "NonExistent"}) - - # Assert - Verify None is returned - assert result is None, "Expected None when no document matches" + collection.insert_many([ + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 1, "a": 1, "b": 2, "c": 1}, + ]) + result = execute_command(collection, {"find": collection.name, "filter": {"a": 2}, "limit": 1}) + + assertSuccess(result, [], "Should return empty array when no match") @pytest.mark.find def test_find_empty_collection(collection): """Test find on an empty collection returns empty result.""" - # Arrange - Collection is already empty (no insertion needed) - - # Act - Execute find on empty collection - result = list(collection.find()) - - # Assert - Verify empty result - assert result == [], "Expected empty result for empty collection" + result = execute_command(collection, {"find": collection.name}) + + assertSuccess(result, [], "Should return empty array for empty collection") @pytest.mark.find def test_find_with_multiple_conditions(collection): """Test find with multiple filter conditions (implicit AND).""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "city": "NYC"}, - {"name": "Bob", "age": 25, "city": "SF"}, - {"name": "Charlie", "age": 35, "city": "NYC"}, + {"_id": 0, "a": 1, "b": 1, "c": 1}, + {"_id": 1, "a": 1, "b": 2, "c": 1}, + {"_id": 2, "a": 2, "b": 1, "c": 1}, ]) - - # Act - Execute find with multiple conditions - result = list(collection.find({"city": "NYC", "age": 30})) - - # Assert - Verify only matching document is returned - assert len(result) == 1, "Expected to find 1 document" - assert_document_match(result[0], {"name": "Alice", "age": 30, "city": "NYC"}) + result = execute_command(collection, {"find": collection.name, "filter": {"a": 1, "b": 2}}) + + expected = [{"_id": 1, "a": 1, "b": 2, "c": 1}] + assertSuccess(result, expected, "Should return document matching both conditions") @pytest.mark.find def test_find_nested_field(collection): """Test find with nested field query using dot notation.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "profile": {"age": 30, "city": "NYC"}}, - {"name": "Bob", "profile": {"age": 25, "city": "SF"}}, + {"_id": 0, "a": 1, "b": {"b1": 1}, "c": 1}, + {"_id": 1, "a": 1, "b": {"b1": 2}, "c": 1}, + {"_id": 2, "a": 2, "b": {"b1": 1}, "c": 1}, ]) + result = execute_command(collection, {"find": collection.name, "filter": {"b.b1": 1}}) + + expected = [ + {"_id": 0, "a": 1, "b": {"b1": 1}, "c": 1}, + {"_id": 2, "a": 2, "b": {"b1": 1}, "c": 1} + ] + assertSuccess(result, expected, "Should match nested field using dot notation") + - # Act - Execute find with nested field - result = list(collection.find({"profile.city": "NYC"})) +@pytest.mark.find +def test_find_invalid_collection(collection): + """Test find on non-existent collection with invalid name.""" + result = execute_command(collection, {"find": "$invalid"}) + + assertFailureCode(result, 73, "Should reject invalid collection name") - # Assert - Verify correct document is returned - assert len(result) == 1, "Expected to find 1 document" - assert result[0]["name"] == "Alice", "Expected to find Alice" + +@pytest.mark.find +def test_find_invalid_filter_type(collection): + """Test find with non-object filter returns error.""" + result = execute_command(collection, {"find": collection.name, "filter": "invalid"}) + + assertFailureCode(result, 14, "Should reject non-object filter") diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_projections.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_projections.py index 6853e67..15abb86 100644 --- a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_projections.py +++ b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_projections.py @@ -6,112 +6,71 @@ import pytest -from documentdb_tests.framework.assertions import assert_field_exists, assert_field_not_exists +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess @pytest.mark.find def test_find_with_field_inclusion(collection): """Test find with explicit field inclusion.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "email": "alice@example.com", "city": "NYC"}, - {"name": "Bob", "age": 25, "email": "bob@example.com", "city": "SF"}, + {"_id": 0, "a": "A", "b": 30, "c": "alice@example.com", "d": "NYC"}, + {"_id": 1, "a": "B", "b": 25, "c": "bob@example.com", "d": "SF"}, ]) - - # Act - Find with projection to include only name and age - result = list(collection.find({}, {"name": 1, "age": 1})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents" - - # Verify included fields exist - for doc in result: - assert_field_exists(doc, "_id") # _id is included by default - assert_field_exists(doc, "name") - assert_field_exists(doc, "age") - - # Verify excluded fields don't exist - assert_field_not_exists(doc, "email") - assert_field_not_exists(doc, "city") + result = execute_command(collection, {"find": collection.name, "projection": {"a": 1, "b": 1}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25} + ] + assertSuccess(result, expected, "Should include only specified fields") @pytest.mark.find def test_find_with_field_exclusion(collection): """Test find with explicit field exclusion.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30, "email": "alice@example.com", "city": "NYC"}, - {"name": "Bob", "age": 25, "email": "bob@example.com", "city": "SF"}, + {"_id": 0, "a": "A", "b": 30, "c": "alice@example.com", "d": "NYC"}, + {"_id": 1, "a": "B", "b": 25, "c": "bob@example.com", "d": "SF"}, ]) - - # Act - Find with projection to exclude email and city - result = list(collection.find({}, {"email": 0, "city": 0})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents" - - # Verify included fields exist - for doc in result: - assert_field_exists(doc, "_id") - assert_field_exists(doc, "name") - assert_field_exists(doc, "age") - - # Verify excluded fields don't exist - assert_field_not_exists(doc, "email") - assert_field_not_exists(doc, "city") + result = execute_command(collection, {"find": collection.name, "projection": {"c": 0, "d": 0}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25} + ] + assertSuccess(result, expected, "Should exclude specified fields") @pytest.mark.find def test_find_exclude_id(collection): """Test find with _id exclusion.""" - # Arrange - Insert test data - collection.insert_one({"name": "Alice", "age": 30, "email": "alice@example.com"}) - - # Act - Find with projection to exclude _id - result = list(collection.find({}, {"_id": 0, "name": 1, "age": 1})) - - # Assert - Verify results - assert len(result) == 1, "Expected 1 document" - - # Verify _id is excluded - assert_field_not_exists(result[0], "_id") - assert_field_exists(result[0], "name") - assert_field_exists(result[0], "age") + collection.insert_many([ + {"_id": 0, "a": "A", "b": 30, "c": "alice@example.com"}, + ]) + result = execute_command(collection, {"find": collection.name, "projection": {"_id": 0, "a": 1, "b": 1}}) + + expected = [{"a": "A", "b": 30}] + assertSuccess(result, expected, "Should exclude _id field") @pytest.mark.find def test_find_nested_field_projection(collection): """Test find with nested field projection.""" - # Arrange - Insert test data - collection.insert_one({ - "name": "Alice", - "profile": {"age": 30, "city": "NYC", "email": "alice@example.com"} - }) - - # Act - Find with projection for nested field - result = list(collection.find({}, {"name": 1, "profile.age": 1})) - - # Assert - Verify results - assert len(result) == 1, "Expected 1 document" - assert_field_exists(result[0], "name") - assert_field_exists(result[0], "profile.age") - - # Verify other nested fields are not included - assert "city" not in result[0].get("profile", {}), "city should not be in profile" - assert "email" not in result[0].get("profile", {}), "email should not be in profile" + collection.insert_many([ + {"_id": 0, "a": "A", "b": {"b1": 30, "b2": "NYC", "b3": "alice@example.com"}}, + ]) + result = execute_command(collection, {"find": collection.name, "projection": {"a": 1, "b.b1": 1}}) + + expected = [{"_id": 0, "a": "A", "b": {"b1": 30}}] + assertSuccess(result, expected, "Should project nested field") @pytest.mark.find -@pytest.mark.smoke def test_find_empty_projection(collection): """Test find with empty projection returns all fields.""" - # Arrange - Insert test data - collection.insert_one({"name": "Alice", "age": 30}) - - # Act - Find with empty projection - result = collection.find_one({}, {}) - - # Assert - Verify all fields are present - assert_field_exists(result, "_id") - assert_field_exists(result, "name") - assert_field_exists(result, "age") + collection.insert_many([{"_id": 0, "a": "A", "b": 30}]) + result = execute_command(collection, {"find": collection.name, "limit": 1}) + + expected = [{"_id": 0, "a": "A", "b": 30}] + assertSuccess(result, expected, "Should return all fields") diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_query_operators.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_query_operators.py index 9cd6bd7..6e7a0d3 100644 --- a/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_query_operators.py +++ b/documentdb_tests/compatibility/tests/core/query-and-write/commands/find/test_find_query_operators.py @@ -6,154 +6,137 @@ import pytest +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess + @pytest.mark.find def test_find_gt_operator(collection): """Test find with $gt (greater than) operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Find documents where age > 25 - result = list(collection.find({"age": {"$gt": 25}})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents with age > 25" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Charlie"}, "Expected Alice and Charlie" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$gt": 25}}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 2, "a": "C", "b": 35} + ] + assertSuccess(result, expected, "Should match documents with b > 25") @pytest.mark.find def test_find_gte_operator(collection): """Test find with $gte (greater than or equal) operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Find documents where age >= 30 - result = list(collection.find({"age": {"$gte": 30}})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents with age >= 30" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Charlie"}, "Expected Alice and Charlie" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$gte": 30}}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 2, "a": "C", "b": 35} + ] + assertSuccess(result, expected, "Should match documents with b >= 30") @pytest.mark.find def test_find_lt_operator(collection): """Test find with $lt (less than) operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Find documents where age < 30 - result = list(collection.find({"age": {"$lt": 30}})) - - # Assert - Verify results - assert len(result) == 1, "Expected 1 document with age < 30" - assert result[0]["name"] == "Bob", "Expected Bob" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$lt": 30}}}) + + expected = [{"_id": 1, "a": "B", "b": 25}] + assertSuccess(result, expected, "Should match documents with b < 30") @pytest.mark.find def test_find_lte_operator(collection): """Test find with $lte (less than or equal) operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Find documents where age <= 30 - result = list(collection.find({"age": {"$lte": 30}})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents with age <= 30" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Bob"}, "Expected Alice and Bob" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$lte": 30}}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25} + ] + assertSuccess(result, expected, "Should match documents with b <= 30") @pytest.mark.find def test_find_ne_operator(collection): """Test find with $ne (not equal) operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Find documents where age != 30 - result = list(collection.find({"age": {"$ne": 30}})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents with age != 30" - names = {doc["name"] for doc in result} - assert names == {"Bob", "Charlie"}, "Expected Bob and Charlie" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$ne": 30}}}) + + expected = [ + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35} + ] + assertSuccess(result, expected, "Should match documents with b != 30") @pytest.mark.find def test_find_in_operator(collection): """Test find with $in operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "status": "active"}, - {"name": "Bob", "status": "inactive"}, - {"name": "Charlie", "status": "pending"}, - {"name": "David", "status": "active"}, + {"_id": 0, "a": "A", "b": "active"}, + {"_id": 1, "a": "B", "b": "inactive"}, + {"_id": 2, "a": "C", "b": "pending"}, + {"_id": 3, "a": "D", "b": "active"}, ]) - - # Act - Find documents where status is in ["active", "pending"] - result = list(collection.find({"status": {"$in": ["active", "pending"]}})) - - # Assert - Verify results - assert len(result) == 3, "Expected 3 documents with status in [active, pending]" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Charlie", "David"}, "Expected Alice, Charlie, and David" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$in": ["active", "pending"]}}}) + + expected = [ + {"_id": 0, "a": "A", "b": "active"}, + {"_id": 2, "a": "C", "b": "pending"}, + {"_id": 3, "a": "D", "b": "active"} + ] + assertSuccess(result, expected, "Should match documents with b in [active, pending]") @pytest.mark.find def test_find_nin_operator(collection): """Test find with $nin (not in) operator.""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "status": "active"}, - {"name": "Bob", "status": "inactive"}, - {"name": "Charlie", "status": "pending"}, + {"_id": 0, "a": "A", "b": "active"}, + {"_id": 1, "a": "B", "b": "inactive"}, + {"_id": 2, "a": "C", "b": "pending"}, ]) - - # Act - Find documents where status is not in ["active", "pending"] - result = list(collection.find({"status": {"$nin": ["active", "pending"]}})) - - # Assert - Verify results - assert len(result) == 1, "Expected 1 document with status not in [active, pending]" - assert result[0]["name"] == "Bob", "Expected Bob" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$nin": ["active", "pending"]}}}) + + expected = [{"_id": 1, "a": "B", "b": "inactive"}] + assertSuccess(result, expected, "Should match documents with b not in [active, pending]") @pytest.mark.find -@pytest.mark.smoke def test_find_range_query(collection): """Test find with range query (combining $gte and $lte).""" - # Arrange - Insert test data collection.insert_many([ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25}, + {"_id": 2, "a": "C", "b": 35}, ]) - - # Act - Find documents where 25 <= age <= 30 - result = list(collection.find({"age": {"$gte": 25, "$lte": 30}})) - - # Assert - Verify results - assert len(result) == 2, "Expected 2 documents with age between 25 and 30" - names = {doc["name"] for doc in result} - assert names == {"Alice", "Bob"}, "Expected Alice and Bob" + result = execute_command(collection, {"find": collection.name, "filter": {"b": {"$gte": 25, "$lte": 30}}}) + + expected = [ + {"_id": 0, "a": "A", "b": 30}, + {"_id": 1, "a": "B", "b": 25} + ] + assertSuccess(result, expected, "Should match documents with 25 <= b <= 30") diff --git a/documentdb_tests/compatibility/tests/core/query-and-write/commands/insert/test_insert_operations.py b/documentdb_tests/compatibility/tests/core/query-and-write/commands/insert/test_insert_operations.py index 21cdd31..b820c43 100644 --- a/documentdb_tests/compatibility/tests/core/query-and-write/commands/insert/test_insert_operations.py +++ b/documentdb_tests/compatibility/tests/core/query-and-write/commands/insert/test_insert_operations.py @@ -5,142 +5,65 @@ """ import pytest -from bson import ObjectId -from pymongo.errors import DuplicateKeyError -from documentdb_tests.framework.assertions import assert_count +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess, assertFailureCode, assertSuccessPartial @pytest.mark.insert @pytest.mark.smoke def test_insert_one_document(collection): """Test inserting a single document.""" - # Insert one document - document = {"name": "Alice", "age": 30} - result = collection.insert_one(document) - - # Verify insert was acknowledged - assert result.acknowledged, "Insert should be acknowledged" - assert result.inserted_id is not None, "Should return inserted _id" - assert isinstance(result.inserted_id, ObjectId), "Inserted _id should be ObjectId" - - # Verify document exists in collection - assert_count(collection, {}, 1) - - # Verify document content - found = collection.find_one({"name": "Alice"}) - assert found is not None, "Document should exist" - assert found["name"] == "Alice" - assert found["age"] == 30 + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": 0, "a": "A", "b": 30}]}) + + assertSuccessPartial(result, {"n": 1}, "Should insert one document") @pytest.mark.insert @pytest.mark.smoke def test_insert_many_documents(collection): """Test inserting multiple documents.""" - # Insert multiple documents - documents = [ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35}, - ] - result = collection.insert_many(documents) - - # Verify insert was acknowledged - assert result.acknowledged, "Insert should be acknowledged" - assert len(result.inserted_ids) == 3, "Should return 3 inserted IDs" - - # Verify all IDs are ObjectIds - for inserted_id in result.inserted_ids: - assert isinstance(inserted_id, ObjectId), "Each inserted _id should be ObjectId" - - # Verify all documents exist - assert_count(collection, {}, 3) + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": 0, "a": "A", "b": 30}, {"_id": 1, "a": "B", "b": 25}, {"_id": 2, "a": "C", "b": 35}]}) + + assertSuccessPartial(result, {"n": 3}, "Should insert three documents") @pytest.mark.insert def test_insert_with_custom_id(collection): """Test inserting document with custom _id.""" - # Insert document with custom _id - custom_id = "custom_123" - document = {"_id": custom_id, "name": "Alice"} - result = collection.insert_one(document) - - # Verify custom _id is used - assert result.inserted_id == custom_id, "Should use custom _id" - - # Verify document can be retrieved by custom _id - found = collection.find_one({"_id": custom_id}) - assert found is not None, "Document should exist" - assert found["name"] == "Alice" + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": "custom_123", "a": "A"}]}) + + assertSuccessPartial(result, {"n": 1}, "Should insert with custom _id") @pytest.mark.insert def test_insert_duplicate_id_fails(collection): """Test that inserting duplicate _id raises error.""" - # Insert first document - document = {"_id": "duplicate_id", "name": "Alice"} - collection.insert_one(document) - - # Try to insert document with same _id - duplicate = {"_id": "duplicate_id", "name": "Bob"} - - # Should raise DuplicateKeyError - with pytest.raises(DuplicateKeyError): - collection.insert_one(duplicate) - - # Verify only first document exists - assert_count(collection, {}, 1) - found = collection.find_one({"_id": "duplicate_id"}) - assert found["name"] == "Alice", "Should have first document" + execute_command(collection, {"insert": collection.name, "documents": [{"_id": "dup", "a": "A"}]}) + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": "dup", "a": "B"}]}) + + assertFailureCode(result, 11000, "Should reject duplicate _id") @pytest.mark.insert def test_insert_nested_document(collection): """Test inserting document with nested structure.""" - # Insert document with nested fields - document = { - "name": "Alice", - "profile": {"age": 30, "address": {"city": "NYC", "country": "USA"}}, - } - result = collection.insert_one(document) - - # Verify insert - assert result.inserted_id is not None - - # Verify nested structure is preserved - found = collection.find_one({"name": "Alice"}) - assert found["profile"]["age"] == 30 - assert found["profile"]["address"]["city"] == "NYC" + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": 0, "a": "A", "b": {"b1": 30, "b2": {"b3": "NYC"}}}]}) + + assertSuccessPartial(result, {"n": 1}, "Should insert nested document") @pytest.mark.insert def test_insert_array_field(collection): """Test inserting document with array fields.""" - # Insert document with array - document = {"name": "Alice", "tags": ["python", "mongodb", "testing"], "scores": [95, 87, 92]} - result = collection.insert_one(document) - - # Verify insert - assert result.inserted_id is not None - - # Verify array fields are preserved - found = collection.find_one({"name": "Alice"}) - assert found["tags"] == ["python", "mongodb", "testing"] - assert found["scores"] == [95, 87, 92] + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": 0, "a": "A", "b": ["python", "mongodb"], "c": [95, 87]}]}) + + assertSuccessPartial(result, {"n": 1}, "Should insert document with arrays") @pytest.mark.insert def test_insert_empty_document(collection): """Test inserting an empty document.""" - # Insert empty document - result = collection.insert_one({}) - - # Verify insert was successful - assert result.inserted_id is not None - - # Verify document exists (only has _id) - found = collection.find_one({"_id": result.inserted_id}) - assert found is not None - assert len(found) == 1 # Only _id field - assert "_id" in found + result = execute_command(collection, {"insert": collection.name, "documents": [{"_id": 0}]}) + + assertSuccessPartial(result, {"n": 1}, "Should insert empty document") diff --git a/documentdb_tests/conftest.py b/documentdb_tests/conftest.py index 28d4838..e31f0d3 100644 --- a/documentdb_tests/conftest.py +++ b/documentdb_tests/conftest.py @@ -8,6 +8,10 @@ """ import pytest + +# Enable assertion rewriting BEFORE importing framework modules +pytest.register_assert_rewrite("documentdb_tests.framework.assertions") + from documentdb_tests.framework import fixtures from documentdb_tests.framework.test_structure_validator import validate_python_files_in_tests from pathlib import Path diff --git a/documentdb_tests/framework/__init__.py b/documentdb_tests/framework/__init__.py index 2f91d5c..625918d 100644 --- a/documentdb_tests/framework/__init__.py +++ b/documentdb_tests/framework/__init__.py @@ -5,7 +5,7 @@ - Assertion helpers for common test scenarios - Fixture utilities for test isolation and database management """ +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.assertions import assertSuccess, assertFailure -from . import assertions, fixtures - -__all__ = ["assertions", "fixtures"] \ No newline at end of file +__all__ = ["execute_command", "assertSuccess", "assertFailure"] diff --git a/documentdb_tests/framework/assertions.py b/documentdb_tests/framework/assertions.py index c55a280..41cbe75 100644 --- a/documentdb_tests/framework/assertions.py +++ b/documentdb_tests/framework/assertions.py @@ -1,103 +1,130 @@ -""" +""" Custom assertion helpers for functional tests. Provides convenient assertion methods for common test scenarios. """ +import math +import pprint +from typing import Any, Dict, List, Union, Callable, Optional -from typing import Dict, List +from documentdb_tests.framework.infra_exceptions import INFRA_EXCEPTION_TYPES as _INFRA_TYPES -def assert_document_match(actual: Dict, expected: Dict, ignore_id: bool = True): - """ - Assert that a document matches the expected structure and values. +class TestSetupError(AssertionError): + """Raised when a test has invalid setup (bad arguments, malformed expected values).""" + pass - Args: - actual: The actual document from the database - expected: The expected document structure - ignore_id: If True, ignore _id field in comparison (default: True) - """ - if ignore_id: - actual = {k: v for k, v in actual.items() if k != "_id"} - expected = {k: v for k, v in expected.items() if k != "_id"} - assert actual == expected, f"Document mismatch.\nExpected: {expected}\nActual: {actual}" +def _format_exception_error(result: Exception) -> str: + """Format a non-infra exception result into an assertion error message.""" + code = getattr(result, 'code', None) + msg = getattr(result, 'details', {}).get('errmsg', str(result)) + return f"[UNEXPECTED_ERROR] Expected success but got exception:\n{pprint.pformat({'code': code, 'msg': msg}, width=100)}\n" -def assert_documents_match( - actual: List[Dict], expected: List[Dict], ignore_id: bool = True, ignore_order: bool = False -): +def assertSuccess(result: Union[Any, Exception], expected: Any, msg: str = None, raw_res: bool = False, transform: Optional[Callable] = None, ignore_doc_order: bool = False): """ - Assert that a list of documents matches the expected list. + Assert command succeeded and optionally check result. Args: - actual: List of actual documents from the database - expected: List of expected documents - ignore_id: If True, ignore _id field in comparison (default: True) - ignore_order: If True, sort both lists before comparison (default: False) + result: Result from execute_command + expected: Expected result value (required) + msg: Custom assertion message (optional) + raw_res: If asserting raw result. False by default, only compare content of ["cursor"]["firstBatch"] + transform: Optional callback to transform result before comparison + ignore_doc_order: If True, compare lists as sets (order-independent) """ - if ignore_id: - actual = [{k: v for k, v in doc.items() if k != "_id"} for doc in actual] - expected = [{k: v for k, v in doc.items() if k != "_id"} for doc in expected] + if isinstance(result, Exception): + if isinstance(result, _INFRA_TYPES): + raise result + raise AssertionError(_format_exception_error(result)) - assert len(actual) == len( - expected - ), f"Document count mismatch. Expected {len(expected)}, got {len(actual)}" + if not raw_res: + result = result["cursor"]["firstBatch"] - if ignore_order: - # Sort for comparison - actual = sorted(actual, key=lambda x: str(x)) - expected = sorted(expected, key=lambda x: str(x)) + if transform: + result = transform(result) - for i, (act, exp) in enumerate(zip(actual, expected)): - assert act == exp, f"Document at index {i} does not match.\nExpected: {exp}\nActual: {act}" + error_text = "[RESULT_MISMATCH]" + if msg: + error_text += f" {msg}" + error_text += f"\n\nExpected:\n{pprint.pformat(expected, width=100)}" + error_text += f"\n\nActual:\n{pprint.pformat(result, width=100)}\n" + if ignore_doc_order and isinstance(result, list) and isinstance(expected, list): + assert sorted(result, key=lambda x: str(x)) == sorted(expected, key=lambda x: str(x)), error_text + else: + assert result == expected, error_text -def assert_field_exists(document: Dict, field_path: str): - """ - Assert that a field exists in a document (supports nested paths). - Args: - document: The document to check - field_path: Dot-notation field path (e.g., "user.name") - """ - parts = field_path.split(".") - current = document +def assertSuccessPartial(result: Union[Any, Exception], expected: Dict[str, Any], msg: str = None): + """Assert command succeeded and check only specified fields.""" + assertSuccess(result, expected, msg, raw_res=True, transform=partial_match(expected)) - for part in parts: - assert part in current, f"Field '{field_path}' does not exist in document" - current = current[part] +def partial_match(expected: Dict[str, Any]): + """Create transform function that extracts only expected fields.""" + return lambda r: {k: r[k] for k in expected.keys() if k in r} -def assert_field_not_exists(document: Dict, field_path: str): + + +def assertFailure(result: Union[Any, Exception], expected: Dict[str, Any], msg: str = None, transform: Optional[Callable] = None): """ - Assert that a field does not exist in a document (supports nested paths). + Assert command failed with expected error. Args: - document: The document to check - field_path: Dot-notation field path (e.g., "user.name") + result: Result from execute_command + expected: Expected error dict with 'code' and 'msg' keys (required unless transform is provided) + msg: Custom assertion message (optional) + transform: Optional callback to transform actual error before comparison """ - parts = field_path.split(".") - current = document + custom_msg = f" {msg}" if msg else "" + + # Check if error is in writeErrors field + if isinstance(result, dict) and "writeErrors" in result: + actual = {"code": result["writeErrors"][0]["code"], "msg": result["writeErrors"][0]["errmsg"]} + elif isinstance(result, Exception): + if isinstance(result, _INFRA_TYPES): + raise result + actual = {"code": getattr(result, 'code', None), "msg": getattr(result, 'details', {}).get('errmsg', str(result))} + else: + error_text = f"[UNEXPECTED_SUCCESS]{custom_msg} Expected error but got result:\n{pprint.pformat(result, width=100)}\n" + raise AssertionError(error_text) + + if transform: + actual = transform(actual) + + # Validate expected format only if no transform + if not transform and (not isinstance(expected, dict) or 'code' not in expected or 'msg' not in expected): + raise TestSetupError(f"[TEST_EXCEPTION] Expected must be dict with 'code' and 'msg' keys, got: {expected}") + + error_text = f"[ERROR_MISMATCH]{custom_msg}\n\nExpected:\n{pprint.pformat(expected, width=100)}\n\nActual:\n{pprint.pformat(actual, width=100)}\n" + assert actual == expected, error_text + - for i, part in enumerate(parts): - if part not in current: - return # Field doesn't exist, assertion passes - if i == len(parts) - 1: - raise AssertionError(f"Field '{field_path}' exists in document but should not") - current = current[part] +def assertFailureCode(result: Union[Any, Exception], expected_code: int, msg: str = None): + """Assert command failed and check only the code field.""" + expected = {"code": expected_code} + assertFailure(result, expected, msg, transform=partial_match(expected)) -def assert_count(collection, filter_query: Dict, expected_count: int): +def assertResult(result: Union[Any, Exception], expected: Any = None, error_code: int = None, msg: str = None): """ - Assert that a collection contains the expected number of documents matching a filter. + Universal assertion that handles success and error cases. Args: - collection: MongoDB collection object - filter_query: Query filter - expected_count: Expected number of matching documents + result: Result from execute_command + expected: Expected result value. + error_code: Expected error code (mutually exclusive with expected) + msg: Custom assertion message (optional) + + Usage: + assertResult(result, expected=5) # Success case + assertResult(result, error_code=16555) # Error case """ - actual_count = collection.count_documents(filter_query) - assert actual_count == expected_count, ( - f"Document count mismatch for filter {filter_query}. " - f"Expected {expected_count}, got {actual_count}" - ) \ No newline at end of file + if error_code is not None: + # Error case + assertFailureCode(result, error_code, msg) + else: + # Success case + assertSuccess(result, [{"result": expected}], msg) diff --git a/documentdb_tests/framework/executor.py b/documentdb_tests/framework/executor.py new file mode 100644 index 0000000..2cb18ac --- /dev/null +++ b/documentdb_tests/framework/executor.py @@ -0,0 +1,43 @@ +""" +Unified execution and assertion utilities for tests. +""" + +from typing import Any, Dict, Union + + +def execute_command(collection, command: Dict) -> Union[Any, Exception]: + """ + Execute a DocumentDB command and return result or exception. + + Args: + collection: DocumentDB collection + command: Command to execute via runCommand + + Returns: + Result if successful, Exception if failed + """ + try: + db = collection.database + result = db.command(command) + return result + except Exception as e: + return e + + +def execute_admin_command(collection, command: Dict) -> Union[Any, Exception]: + """ + Execute a DocumentDB command on admin database and return result or exception. + + Args: + collection: DocumentDB collection + command: Command to execute via runCommand + + Returns: + Result if successful, Exception if failed + """ + try: + db = collection.database.client.admin + result = db.command(command) + return result + except Exception as e: + return e diff --git a/documentdb_tests/framework/infra_exceptions.py b/documentdb_tests/framework/infra_exceptions.py new file mode 100644 index 0000000..ea5a4a7 --- /dev/null +++ b/documentdb_tests/framework/infra_exceptions.py @@ -0,0 +1,41 @@ +# Infrastructure exception types that indicate environment issues, not test failures. +# Used by both framework/assertions.py (isinstance checks) and +# result_analyzer/analyzer.py (string name checks in JSON crash info). + +INFRA_EXCEPTION_NAMES = { + # Python built-in + "ConnectionError", + "ConnectionRefusedError", + "ConnectionResetError", + "ConnectionAbortedError", + "TimeoutError", + "OSError", + "socket.timeout", + "socket.error", + # PyMongo + "pymongo.errors.ConnectionFailure", + "pymongo.errors.ServerSelectionTimeoutError", + "pymongo.errors.NetworkTimeout", + "pymongo.errors.AutoReconnect", + "pymongo.errors.ExecutionTimeout", +} + + +def _resolve_types(): + """Resolve exception names to actual types for isinstance() checks.""" + import builtins + types = [] + for name in INFRA_EXCEPTION_NAMES: + if hasattr(builtins, name): + types.append(getattr(builtins, name)) + elif "." in name: + parts = name.rsplit(".", 1) + try: + mod = __import__(parts[0], fromlist=[parts[1]]) + types.append(getattr(mod, parts[1])) + except (ImportError, AttributeError): + pass + return tuple(types) + + +INFRA_EXCEPTION_TYPES = _resolve_types()