From 41c8d4c2a148a9fc246d2bee8c74bd60d35b2b16 Mon Sep 17 00:00:00 2001 From: Tony Burns Date: Thu, 1 Jan 2026 18:34:46 -0500 Subject: [PATCH] test: consolidate conftest files and add shared fixtures - Remove 17 redundant unit tests covered by property tests - Consolidate 6 conftest.py files into root with directory-based markers - Add shared fixtures: fake_fs, make_table, make_table_with_records - Migrate 51 tests to use new fixtures for cleaner test code --- tests/benchmarks/conftest.py | 10 -- tests/conftest.py | 34 ++++++ tests/examples/conftest.py | 10 -- tests/fuzz/conftest.py | 10 -- tests/integration/conftest.py | 10 -- tests/properties/conftest.py | 10 -- tests/unit/conftest.py | 166 ++++++++++++++++++++++++++-- tests/unit/test_json.py | 14 --- tests/unit/test_keys.py | 20 ---- tests/unit/test_state.py | 46 -------- tests/unit/test_table.py | 157 ++++++++++++++------------- tests/unit/test_transaction.py | 192 +++++++++++++++++---------------- 12 files changed, 373 insertions(+), 306 deletions(-) delete mode 100644 tests/benchmarks/conftest.py delete mode 100644 tests/examples/conftest.py delete mode 100644 tests/fuzz/conftest.py delete mode 100644 tests/integration/conftest.py delete mode 100644 tests/properties/conftest.py diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py deleted file mode 100644 index c870fa2..0000000 --- a/tests/benchmarks/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - test_dir = Path(__file__).parent - for item in items: - if Path(item.fspath).is_relative_to(test_dir): - item.add_marker(pytest.mark.benchmark) diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..dd7f0c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +"""Pytest configuration and shared fixtures for the test suite.""" + +from pathlib import Path + +import pytest + +# Directory-to-marker mapping +_DIRECTORY_MARKERS: dict[str, str] = { + "unit": "unit", + "integration": "integration", + "properties": "property", + "benchmarks": "benchmark", + "examples": "example", + "fuzz": "fuzz", +} + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + """Automatically apply markers based on test directory.""" + tests_dir = Path(__file__).parent + + for item in items: + item_path = Path(item.fspath) + + try: + relative = item_path.relative_to(tests_dir) + if relative.parts: + subdir = relative.parts[0] + if marker_name := _DIRECTORY_MARKERS.get(subdir): + # Dynamic marker access returns Any + marker = getattr(pytest.mark, marker_name) # pyright: ignore[reportAny] + item.add_marker(marker) # pyright: ignore[reportAny] + except ValueError: + pass diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py deleted file mode 100644 index 1df1f63..0000000 --- a/tests/examples/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - test_dir = Path(__file__).parent - for item in items: - if Path(item.fspath).is_relative_to(test_dir): - item.add_marker(pytest.mark.example) diff --git a/tests/fuzz/conftest.py b/tests/fuzz/conftest.py deleted file mode 100644 index 18c9efa..0000000 --- a/tests/fuzz/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - test_dir = Path(__file__).parent - for item in items: - if Path(item.fspath).is_relative_to(test_dir): - item.add_marker(pytest.mark.fuzz) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py deleted file mode 100644 index d029721..0000000 --- a/tests/integration/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - test_dir = Path(__file__).parent - for item in items: - if Path(item.fspath).is_relative_to(test_dir): - item.add_marker(pytest.mark.integration) diff --git a/tests/properties/conftest.py b/tests/properties/conftest.py deleted file mode 100644 index 7206912..0000000 --- a/tests/properties/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -from pathlib import Path - -import pytest - - -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - test_dir = Path(__file__).parent - for item in items: - if Path(item.fspath).is_relative_to(test_dir): - item.add_marker(pytest.mark.property) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 5986dab..1e85ec6 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,10 +1,164 @@ -from pathlib import Path +"""Shared fixtures for unit tests.""" + +from typing import TYPE_CHECKING import pytest +from jsonlt import Table + +from tests.fakes.fake_filesystem import FakeFileSystem + +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + + from jsonlt._json import JSONObject + + +@pytest.fixture +def fake_fs() -> FakeFileSystem: + """Provide a fresh FakeFileSystem instance for each test. + + The FakeFileSystem provides an in-memory filesystem implementation + that can be injected into Table for isolated testing without disk I/O. + + Returns: + A new FakeFileSystem instance with no files and no failure modes. + + Example: + def test_table_with_fake_fs(fake_fs: FakeFileSystem, tmp_path: Path) -> None: + table = Table(tmp_path / "test.jsonlt", key="id", _fs=fake_fs) + fake_fs.set_content(tmp_path / "test.jsonlt", b'{"id":"alice"}\\n') + assert table.count() == 1 + """ + return FakeFileSystem() + + +@pytest.fixture +def fake_fs_with_file( + fake_fs: FakeFileSystem, + tmp_path: "Path", +) -> "Callable[[bytes], Path]": + """Factory fixture to create a fake file with content. + + Returns a callable that creates a file in the fake filesystem + and returns its path. + + Args: + fake_fs: The FakeFileSystem fixture. + tmp_path: Pytest's temporary directory fixture. + + Returns: + A callable that takes bytes content and returns the file path. + + Example: + def test_with_content(fake_fs_with_file, fake_fs) -> None: + path = fake_fs_with_file(b'{"id":"alice"}\\n') + content = fake_fs.get_content(path) + assert b"alice" in content + """ + counter = 0 + + def create_file(content: bytes) -> "Path": + nonlocal counter + counter += 1 + path = tmp_path / f"test_{counter}.jsonlt" + fake_fs.set_content(path, content) + return path + + return create_file + + +@pytest.fixture +def make_table( + tmp_path: "Path", +) -> "Callable[..., Table]": + """Factory fixture for creating Table instances. + + Provides a convenient way to create tables with sensible defaults + while allowing full customization through keyword arguments. + + Returns: + A callable that creates Table instances. + + Example: + def test_table_operations(make_table) -> None: + table = make_table() # Creates table with key="id" + table.put({"id": "alice", "role": "admin"}) + assert table.get("alice") is not None + + def test_with_custom_key(make_table) -> None: + table = make_table(key=("org", "id")) + table.put({"org": "acme", "id": 1, "name": "alice"}) + """ + counter = 0 + + def create_table( + *, + key: str | tuple[str, ...] = "id", + content: str | None = None, + auto_reload: bool = True, + max_file_size: int | None = None, + lock_timeout: float | None = None, + _fs: FakeFileSystem | None = None, + ) -> Table: + nonlocal counter + counter += 1 + path = tmp_path / f"table_{counter}.jsonlt" + + if content is not None: + _ = path.write_text(content) + + return Table( + path, + key=key, + auto_reload=auto_reload, + max_file_size=max_file_size, + lock_timeout=lock_timeout, + _fs=_fs, + ) + + return create_table + + +@pytest.fixture +def make_table_with_records( + make_table: "Callable[..., Table]", +) -> "Callable[..., Table]": + """Factory fixture for creating pre-populated Table instances. + + Builds on make_table to provide convenient record seeding. + + Returns: + A callable that creates Table instances with initial records. + + Example: + def test_populated_table(make_table_with_records) -> None: + table = make_table_with_records([ + {"id": "alice", "role": "admin"}, + {"id": "bob", "role": "user"}, + ]) + assert table.count() == 2 + """ + + def create_table_with_records( + records: "list[JSONObject]", + *, + key: str | tuple[str, ...] = "id", + auto_reload: bool = True, + max_file_size: int | None = None, + lock_timeout: float | None = None, + _fs: FakeFileSystem | None = None, + ) -> Table: + table = make_table( + key=key, + auto_reload=auto_reload, + max_file_size=max_file_size, + lock_timeout=lock_timeout, + _fs=_fs, + ) + for record in records: + table.put(record) + return table -def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: - test_dir = Path(__file__).parent - for item in items: - if Path(item.fspath).is_relative_to(test_dir): - item.add_marker(pytest.mark.unit) + return create_table_with_records diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index 71d40d4..d9c9348 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -243,20 +243,6 @@ def test_complex_nested_structure(self) -> None: class TestSerializationDeterminism: - def test_consistent_output_across_calls(self) -> None: - value = {"zebra": 1, "apple": 2, "Banana": 3} - result1 = serialize_json(value) - result2 = serialize_json(value) - assert result1 == result2 - - def test_consistent_for_identical_data(self) -> None: - # Same data constructed differently should serialize identically - value1 = {"b": 2, "a": 1} - value2 = {"a": 1, "b": 2} - result1 = serialize_json(value1) - result2 = serialize_json(value2) - assert result1 == result2 - def test_preserves_value_types(self) -> None: value = { "null": None, diff --git a/tests/unit/test_keys.py b/tests/unit/test_keys.py index 074ec57..943799a 100644 --- a/tests/unit/test_keys.py +++ b/tests/unit/test_keys.py @@ -240,10 +240,6 @@ class TestCompareKeys: @pytest.mark.parametrize( ("a", "b", "expected"), [ - # Equal values - (42, 42, 0), - ("alice", "alice", 0), - (("a", 1), ("a", 1), 0), # Integer comparisons (1, 2, -1), (2, 1, 1), @@ -255,13 +251,6 @@ class TestCompareKeys: # Unicode code point ordering: uppercase before lowercase ("Alice", "alice", -1), ("Zebra", "apple", -1), - # Cross-type ordering: int < str < tuple - (42, "42", -1), - ("42", 42, 1), - ("alice", ("alice",), -1), - (("alice",), "alice", 1), - (42, ("a", 1), -1), - (("a", 1), 42, 1), # Tuple element ordering (("a", 1), ("a", 2), -1), (("a", 2), ("b", 1), -1), @@ -271,9 +260,6 @@ class TestCompareKeys: ((1, "a"), ("a", 1), -1), ], ids=[ - "equal_integers", - "equal_strings", - "equal_tuples", "less_integer", "greater_integer", "negative_less", @@ -282,12 +268,6 @@ class TestCompareKeys: "greater_string", "uppercase_before_lowercase", "code_point_ordering", - "int_before_string", - "string_after_int", - "string_before_tuple", - "tuple_after_string", - "int_before_tuple", - "tuple_after_int", "tuple_element_ordering", "tuple_first_element_wins", "shorter_tuple_first", diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py index 4f6c4c9..51059d1 100644 --- a/tests/unit/test_state.py +++ b/tests/unit/test_state.py @@ -10,16 +10,6 @@ class TestComputeLogicalState: - def test_empty_operations_returns_empty_state(self) -> None: - operations: list[JSONObject] = [] - state = compute_logical_state(operations, "id") - assert state == {} - - def test_single_record(self) -> None: - operations: list[JSONObject] = [{"id": "alice", "role": "admin"}] - state = compute_logical_state(operations, "id") - assert state == {"alice": {"id": "alice", "role": "admin"}} - def test_multiple_records_distinct_keys(self) -> None: operations: list[JSONObject] = [ {"id": "alice", "role": "admin"}, @@ -32,42 +22,6 @@ def test_multiple_records_distinct_keys(self) -> None: assert state["bob"] == {"id": "bob", "role": "user"} assert state["carol"] == {"id": "carol", "role": "user"} - def test_upsert_overwrites(self) -> None: - operations: list[JSONObject] = [ - {"id": "alice", "role": "user"}, - {"id": "alice", "role": "admin"}, - ] - state = compute_logical_state(operations, "id") - assert len(state) == 1 - assert state["alice"] == {"id": "alice", "role": "admin"} - - def test_tombstone_removes(self) -> None: - operations: list[JSONObject] = [ - {"id": "alice", "role": "admin"}, - {"$deleted": True, "id": "alice"}, - ] - state = compute_logical_state(operations, "id") - assert state == {} - - def test_tombstone_nonexistent_key(self) -> None: - operations: list[JSONObject] = [ - {"id": "alice", "role": "admin"}, - {"$deleted": True, "id": "bob"}, - ] - state = compute_logical_state(operations, "id") - assert len(state) == 1 - assert state["alice"] == {"id": "alice", "role": "admin"} - - def test_reinsert_after_delete(self) -> None: - operations: list[JSONObject] = [ - {"id": "alice", "role": "admin"}, - {"$deleted": True, "id": "alice"}, - {"id": "alice", "role": "user"}, - ] - state = compute_logical_state(operations, "id") - assert len(state) == 1 - assert state["alice"] == {"id": "alice", "role": "user"} - def test_integer_key(self) -> None: operations: list[JSONObject] = [ {"id": 1, "name": "first"}, diff --git a/tests/unit/test_table.py b/tests/unit/test_table.py index 24fba9d..7e33cf6 100644 --- a/tests/unit/test_table.py +++ b/tests/unit/test_table.py @@ -5,11 +5,12 @@ from jsonlt import FileError, InvalidKeyError, LimitError, Table -from tests.fakes.fake_filesystem import FakeFileSystem - if TYPE_CHECKING: + from collections.abc import Callable from pathlib import Path + from tests.fakes.fake_filesystem import FakeFileSystem + class TestTableConstruction: def test_new_file_with_key_specifier(self, tmp_path: "Path") -> None: @@ -115,10 +116,8 @@ def test_get_nonexistent_key(self, tmp_path: "Path") -> None: assert table.get("bob") is None - def test_get_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - - table = Table(table_path, key="id") + def test_get_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() assert table.get("alice") is None @@ -156,19 +155,15 @@ def test_has_nonexistent_key(self, tmp_path: "Path") -> None: assert table.has("bob") is False - def test_has_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - - table = Table(table_path, key="id") + def test_has_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() assert table.has("alice") is False class TestTableAll: - def test_all_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - - table = Table(table_path, key="id") + def test_all_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() assert table.all() == [] @@ -219,10 +214,8 @@ def test_all_sorted_mixed_types(self, tmp_path: "Path") -> None: class TestTableKeys: - def test_keys_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - - table = Table(table_path, key="id") + def test_keys_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() assert table.keys() == [] @@ -236,10 +229,8 @@ def test_keys_sorted(self, tmp_path: "Path") -> None: class TestTableCount: - def test_count_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - - table = Table(table_path, key="id") + def test_count_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() assert table.count() == 0 @@ -445,9 +436,10 @@ def test_put_appends_to_file(self, tmp_path: "Path") -> None: content = table_path.read_text() assert content.count("\n") == 2 - def test_put_updates_existing_record(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_updates_existing_record( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() table.put({"id": "alice", "role": "user"}) table.put({"id": "alice", "role": "admin"}) @@ -455,9 +447,8 @@ def test_put_updates_existing_record(self, tmp_path: "Path") -> None: assert table.count() == 1 assert table.get("alice") == {"id": "alice", "role": "admin"} - def test_put_with_integer_key(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_with_integer_key(self, make_table: "Callable[..., Table]") -> None: + table = make_table() table.put({"id": 1, "name": "First"}) @@ -478,30 +469,30 @@ def test_put_without_key_specifier_raises(self, tmp_path: "Path") -> None: with pytest.raises(InvalidKeyError, match="key specifier is required"): table.put({"id": "alice"}) - def test_put_missing_key_field_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_missing_key_field_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with pytest.raises(InvalidKeyError, match="missing required key field"): table.put({"name": "Alice"}) - def test_put_dollar_field_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_dollar_field_raises(self, make_table: "Callable[..., Table]") -> None: + table = make_table() with pytest.raises(InvalidKeyError, match="reserved field name"): table.put({"id": "alice", "$custom": "value"}) - def test_put_invalid_key_type_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_invalid_key_type_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with pytest.raises(InvalidKeyError, match="boolean"): table.put({"id": True, "name": "Alice"}) - def test_put_key_length_limit(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_key_length_limit(self, make_table: "Callable[..., Table]") -> None: + table = make_table() # Key with > 1024 bytes when serialized (string with quotes) long_key = "x" * 1030 @@ -531,9 +522,10 @@ def test_delete_existing_record(self, tmp_path: "Path") -> None: assert table.get("alice") is None assert table.count() == 0 - def test_delete_nonexistent_record(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_delete_nonexistent_record( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() result = table.delete("bob") @@ -591,9 +583,10 @@ def test_delete_tuple_key_arity_mismatch_raises(self, tmp_path: "Path") -> None: with pytest.raises(InvalidKeyError, match="key arity mismatch"): _ = table.delete(("acme", 1, "extra")) # 3 elements, specifier has 2 - def test_delete_key_length_limit_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_delete_key_length_limit_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() # 1030 characters + quotes = 1032 bytes > 1024 long_key = "x" * 1030 @@ -631,9 +624,8 @@ def test_clear_preserves_header(self, tmp_path: "Path") -> None: assert len(lines) == 1 assert "$jsonlt" in lines[0] - def test_clear_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_clear_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() table.clear() # Should not raise @@ -683,9 +675,8 @@ def test_clear_reloads_header_inside_lock(self, tmp_path: "Path") -> None: class TestTableCompact: - def test_compact_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_compact_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() table.compact() # Should not raise @@ -849,9 +840,10 @@ def test_compact_mixed_key_types_sorted(self, tmp_path: "Path") -> None: class TestTableWriteReload: - def test_put_updates_state_immediately(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id", auto_reload=False) + def test_put_updates_state_immediately( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table(auto_reload=False) table.put({"id": "alice", "name": "Alice"}) @@ -987,9 +979,8 @@ def test_iter_yields_records_in_key_order(self, tmp_path: "Path") -> None: assert records[1] == {"id": "b"} assert records[2] == {"id": "c"} - def test_iter_on_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_iter_on_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() records = list(table) @@ -1044,9 +1035,8 @@ def test_items_in_key_order(self, tmp_path: "Path") -> None: assert [k for k, _ in items] == ["a", "b", "c"] - def test_items_empty_table(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_items_empty_table(self, make_table: "Callable[..., Table]") -> None: + table = make_table() items = table.items() @@ -1107,8 +1097,9 @@ def test_reload_clears_sorted_keys_cache(self, tmp_path: "Path") -> None: class TestFileSystemEdgeCases: - def test_load_empty_file_with_header_but_no_ops(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_load_empty_file_with_header_but_no_ops( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create real file on disk for _load() _ = table_path.write_bytes(b'{"$jsonlt":{"version":1,"key":"id"}}\n') @@ -1120,8 +1111,9 @@ def test_load_empty_file_with_header_but_no_ops(self, tmp_path: "Path") -> None: assert table.count() == 0 assert table.keys() == [] - def test_load_from_content_empty(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_load_from_content_empty( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create real file on disk for _load() _ = table_path.write_bytes(b'{"id":"alice"}\n') @@ -1135,8 +1127,9 @@ def test_load_from_content_empty(self, tmp_path: "Path") -> None: table._load_from_content(b"") # noqa: SLF001 # pyright: ignore[reportPrivateUsage] assert table._state == {} # noqa: SLF001 # pyright: ignore[reportPrivateUsage] - def test_resolve_key_specifier_empty_no_key(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_resolve_key_specifier_empty_no_key( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # File does not exist - table should be empty @@ -1145,8 +1138,9 @@ def test_resolve_key_specifier_empty_no_key(self, tmp_path: "Path") -> None: assert table.key_specifier is None assert table.count() == 0 - def test_reload_if_changed_stat_fails(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_reload_if_changed_stat_fails( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create real file on disk for _load() _ = table_path.write_bytes(b'{"id":"alice"}\n') @@ -1165,8 +1159,9 @@ def test_reload_if_changed_stat_fails(self, tmp_path: "Path") -> None: # Testing internal method table._reload_if_changed(0.0, 0) # noqa: SLF001 # pyright: ignore[reportPrivateUsage] - def test_write_file_not_found_then_exists(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_write_file_not_found_then_exists( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" table = Table(table_path, key="id", _fs=fake_fs) @@ -1176,8 +1171,9 @@ def test_write_file_not_found_then_exists(self, tmp_path: "Path") -> None: assert table.get("alice") == {"id": "alice"} - def test_try_update_stats_ignores_file_error(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_try_update_stats_ignores_file_error( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create real file on disk for _load() _ = table_path.write_bytes(b'{"id":"alice"}\n') @@ -1199,8 +1195,9 @@ def test_try_update_stats_ignores_file_error(self, tmp_path: "Path") -> None: assert table._file_mtime == old_mtime # noqa: SLF001 # pyright: ignore[reportPrivateUsage] assert table._file_size == old_size # noqa: SLF001 # pyright: ignore[reportPrivateUsage] - def test_auto_reload_disabled_uses_cache(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_auto_reload_disabled_uses_cache( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create real file on disk for _load() _ = table_path.write_bytes(b'{"id":"alice"}\n') @@ -1216,8 +1213,9 @@ def test_auto_reload_disabled_uses_cache(self, tmp_path: "Path") -> None: # Should still be able to read from cache since auto_reload is disabled assert table.get("alice") == {"id": "alice"} - def test_clear_on_file_with_header(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_clear_on_file_with_header( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create real file on disk for _load() _ = table_path.write_bytes( @@ -1243,8 +1241,9 @@ def test_clear_on_file_with_header(self, tmp_path: "Path") -> None: assert b'"$jsonlt"' in content assert b'"alice"' not in content - def test_compact_recreates_deleted_file(self, tmp_path: "Path") -> None: - fake_fs = FakeFileSystem() + def test_compact_recreates_deleted_file( + self, tmp_path: "Path", fake_fs: "FakeFileSystem" + ) -> None: table_path = tmp_path / "test.jsonlt" # Create table and add record (uses fake_fs for write) diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index a388743..89cc08d 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -13,15 +13,17 @@ ) if TYPE_CHECKING: + from collections.abc import Callable from os import stat_result from jsonlt._json import JSONObject class TestTransactionCreation: - def test_transaction_returns_transaction_object(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_transaction_returns_transaction_object( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() @@ -35,9 +37,10 @@ def test_transaction_requires_key_specifier(self, tmp_path: "Path") -> None: with pytest.raises(InvalidKeyError, match="key specifier is required"): _ = table.transaction() - def test_nested_transaction_rejected(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_nested_transaction_rejected( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() try: @@ -56,9 +59,10 @@ def test_transaction_sees_initial_state(self, tmp_path: "Path") -> None: with table.transaction() as tx: assert tx.get("alice") == {"id": "alice", "v": 1} - def test_transaction_sees_own_writes(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_transaction_sees_own_writes( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: tx.put({"id": "alice", "v": 1}) @@ -78,16 +82,18 @@ def test_transaction_snapshot_is_isolated(self, tmp_path: "Path") -> None: class TestTransactionReadOperations: - def test_get_returns_none_for_nonexistent_key(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_get_returns_none_for_nonexistent_key( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: assert tx.get("nonexistent") is None - def test_has_returns_false_for_nonexistent_key(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_has_returns_false_for_nonexistent_key( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: assert tx.has("nonexistent") is False @@ -165,9 +171,8 @@ def test_find_one_returns_none_when_no_match(self, tmp_path: "Path") -> None: class TestTransactionWriteOperations: - def test_put_updates_snapshot(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_updates_snapshot(self, make_table: "Callable[..., Table]") -> None: + table = make_table() with table.transaction() as tx: tx.put({"id": "alice", "v": 1}) @@ -183,9 +188,10 @@ def test_put_overwrites_existing(self, tmp_path: "Path") -> None: tx.put({"id": "alice", "v": 2}) assert tx.get("alice") == {"id": "alice", "v": 2} - def test_put_isolates_from_caller_mutations(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_isolates_from_caller_mutations( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: record: JSONObject = {"id": "alice", "items": [1, 2, 3]} @@ -211,17 +217,17 @@ def test_delete_updates_snapshot(self, tmp_path: "Path") -> None: assert tx.has("alice") is False assert tx.count() == 0 - def test_delete_nonexistent_returns_false(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_delete_nonexistent_returns_false( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: result = tx.delete("nonexistent") assert result is False - def test_put_validates_record(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_validates_record(self, make_table: "Callable[..., Table]") -> None: + table = make_table() with ( table.transaction() as tx, @@ -229,9 +235,10 @@ def test_put_validates_record(self, tmp_path: "Path") -> None: ): tx.put({"name": "alice"}) - def test_put_rejects_dollar_fields(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_rejects_dollar_fields( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with ( table.transaction() as tx, @@ -249,9 +256,10 @@ def test_delete_validates_key_arity(self, tmp_path: "Path") -> None: ): _ = tx.delete("alice") - def test_put_key_length_limit_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_key_length_limit_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() long_key = "x" * 1030 # > 1024 bytes when serialized @@ -261,9 +269,10 @@ def test_put_key_length_limit_raises(self, tmp_path: "Path") -> None: ): tx.put({"id": long_key}) - def test_put_record_size_limit_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_put_record_size_limit_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() large_data = "x" * (1024 * 1024 + 1000) @@ -273,9 +282,10 @@ def test_put_record_size_limit_raises(self, tmp_path: "Path") -> None: ): tx.put({"id": "test", "data": large_data}) - def test_delete_key_length_limit_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_delete_key_length_limit_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() # 1030 characters + quotes = 1032 bytes > 1024 long_key = "x" * 1030 @@ -288,9 +298,8 @@ def test_delete_key_length_limit_raises(self, tmp_path: "Path") -> None: class TestTransactionCommit: - def test_commit_persists_writes(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_commit_persists_writes(self, make_table: "Callable[..., Table]") -> None: + table = make_table() with table.transaction() as tx: tx.put({"id": "alice", "v": 1}) @@ -330,9 +339,10 @@ def test_empty_buffer_commit_succeeds(self, tmp_path: "Path") -> None: # Should not raise, table unchanged assert table.get("alice") == {"id": "alice", "v": 1} - def test_multiple_writes_committed_together(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_multiple_writes_committed_together( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: tx.put({"id": "alice", "v": 1}) @@ -401,9 +411,10 @@ def test_abort_does_not_write_to_file(self, tmp_path: "Path") -> None: class TestTransactionContextManager: - def test_context_manager_commits_on_success(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_context_manager_commits_on_success( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: tx.put({"id": "alice", "v": 1}) @@ -432,10 +443,9 @@ def trigger_error() -> None: assert table.get("alice") == {"id": "alice", "v": 1} def test_context_manager_does_not_suppress_exceptions( - self, tmp_path: "Path" + self, make_table: "Callable[..., Table]" ) -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + table = make_table() class PropagateError(Exception): pass @@ -451,9 +461,10 @@ def trigger_error() -> None: class TestTransactionAfterCommitOrAbort: - def test_operations_fail_after_commit(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_operations_fail_after_commit( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() tx.put({"id": "alice", "v": 1}) @@ -462,9 +473,10 @@ def test_operations_fail_after_commit(self, tmp_path: "Path") -> None: with pytest.raises(TransactionError, match="already been committed"): tx.put({"id": "bob", "v": 2}) - def test_operations_fail_after_abort(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_operations_fail_after_abort( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() tx.put({"id": "alice", "v": 1}) @@ -473,9 +485,8 @@ def test_operations_fail_after_abort(self, tmp_path: "Path") -> None: with pytest.raises(TransactionError, match="already been committed"): _ = tx.get("alice") - def test_double_commit_fails(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_double_commit_fails(self, make_table: "Callable[..., Table]") -> None: + table = make_table() tx = table.transaction() tx.commit() @@ -483,9 +494,8 @@ def test_double_commit_fails(self, tmp_path: "Path") -> None: with pytest.raises(TransactionError, match="already been committed"): tx.commit() - def test_double_abort_fails(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_double_abort_fails(self, make_table: "Callable[..., Table]") -> None: + table = make_table() tx = table.transaction() tx.abort() @@ -493,9 +503,10 @@ def test_double_abort_fails(self, tmp_path: "Path") -> None: with pytest.raises(TransactionError, match="already been committed"): tx.abort() - def test_can_start_new_transaction_after_commit(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_can_start_new_transaction_after_commit( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx1: tx1.put({"id": "alice", "v": 1}) @@ -506,9 +517,10 @@ def test_can_start_new_transaction_after_commit(self, tmp_path: "Path") -> None: assert table.count() == 2 - def test_exit_when_already_finalized_returns_false(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_exit_when_already_finalized_returns_false( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() tx.commit() @@ -674,9 +686,10 @@ def test_multiple_puts_same_key_produces_single_line( assert '"id":"alice"' in lines[0] assert '"v":3' in lines[0] - def test_multiple_puts_same_key_final_value_correct(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_multiple_puts_same_key_final_value_correct( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: tx.put({"id": "alice", "v": 1}) @@ -821,40 +834,37 @@ def test_iter_yields_records_in_key_order(self, tmp_path: "Path") -> None: assert records[1] == {"id": "b"} assert records[2] == {"id": "c"} - def test_iter_on_empty_transaction(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_iter_on_empty_transaction( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() with table.transaction() as tx: records = list(tx) assert records == [] - def test_repr_active_transaction(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_repr_active_transaction(self, make_table: "Callable[..., Table]") -> None: + table = make_table() tx = table.transaction() try: result = repr(tx) assert "Transaction(" in result - # Use name to avoid Windows path separator issues - assert table_path.name in result assert "key='id'" in result assert "active" in result finally: tx.abort() - def test_repr_finalized_transaction(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_repr_finalized_transaction( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() tx.commit() result = repr(tx) assert "Transaction(" in result - # Use name to avoid Windows path separator issues - assert table_path.name in result assert "key='id'" in result assert "finalized" in result @@ -894,9 +904,8 @@ def test_items_in_key_order(self, tmp_path: "Path") -> None: items = tx.items() assert [k for k, _ in items] == ["a", "b", "c"] - def test_items_empty_transaction(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_items_empty_transaction(self, make_table: "Callable[..., Table]") -> None: + table = make_table() with table.transaction() as tx: items = tx.items() @@ -914,9 +923,10 @@ def test_items_reflects_transaction_changes(self, tmp_path: "Path") -> None: assert len(items) == 2 assert ("bob", {"id": "bob", "v": 2}) in items - def test_items_on_finalized_transaction_raises(self, tmp_path: "Path") -> None: - table_path = tmp_path / "test.jsonlt" - table = Table(table_path, key="id") + def test_items_on_finalized_transaction_raises( + self, make_table: "Callable[..., Table]" + ) -> None: + table = make_table() tx = table.transaction() tx.commit()