Skip to content

Commit 48ed64a

Browse files
authored
test: consolidate conftest files and add shared fixtures (#12)
- 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
1 parent 9aa28a7 commit 48ed64a

12 files changed

Lines changed: 373 additions & 306 deletions

File tree

tests/benchmarks/conftest.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/conftest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Pytest configuration and shared fixtures for the test suite."""
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
# Directory-to-marker mapping
8+
_DIRECTORY_MARKERS: dict[str, str] = {
9+
"unit": "unit",
10+
"integration": "integration",
11+
"properties": "property",
12+
"benchmarks": "benchmark",
13+
"examples": "example",
14+
"fuzz": "fuzz",
15+
}
16+
17+
18+
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
19+
"""Automatically apply markers based on test directory."""
20+
tests_dir = Path(__file__).parent
21+
22+
for item in items:
23+
item_path = Path(item.fspath)
24+
25+
try:
26+
relative = item_path.relative_to(tests_dir)
27+
if relative.parts:
28+
subdir = relative.parts[0]
29+
if marker_name := _DIRECTORY_MARKERS.get(subdir):
30+
# Dynamic marker access returns Any
31+
marker = getattr(pytest.mark, marker_name) # pyright: ignore[reportAny]
32+
item.add_marker(marker) # pyright: ignore[reportAny]
33+
except ValueError:
34+
pass

tests/examples/conftest.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/fuzz/conftest.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/integration/conftest.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/properties/conftest.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/unit/conftest.py

Lines changed: 160 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,164 @@
1-
from pathlib import Path
1+
"""Shared fixtures for unit tests."""
2+
3+
from typing import TYPE_CHECKING
24

35
import pytest
46

7+
from jsonlt import Table
8+
9+
from tests.fakes.fake_filesystem import FakeFileSystem
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Callable
13+
from pathlib import Path
14+
15+
from jsonlt._json import JSONObject
16+
17+
18+
@pytest.fixture
19+
def fake_fs() -> FakeFileSystem:
20+
"""Provide a fresh FakeFileSystem instance for each test.
21+
22+
The FakeFileSystem provides an in-memory filesystem implementation
23+
that can be injected into Table for isolated testing without disk I/O.
24+
25+
Returns:
26+
A new FakeFileSystem instance with no files and no failure modes.
27+
28+
Example:
29+
def test_table_with_fake_fs(fake_fs: FakeFileSystem, tmp_path: Path) -> None:
30+
table = Table(tmp_path / "test.jsonlt", key="id", _fs=fake_fs)
31+
fake_fs.set_content(tmp_path / "test.jsonlt", b'{"id":"alice"}\\n')
32+
assert table.count() == 1
33+
"""
34+
return FakeFileSystem()
35+
36+
37+
@pytest.fixture
38+
def fake_fs_with_file(
39+
fake_fs: FakeFileSystem,
40+
tmp_path: "Path",
41+
) -> "Callable[[bytes], Path]":
42+
"""Factory fixture to create a fake file with content.
43+
44+
Returns a callable that creates a file in the fake filesystem
45+
and returns its path.
46+
47+
Args:
48+
fake_fs: The FakeFileSystem fixture.
49+
tmp_path: Pytest's temporary directory fixture.
50+
51+
Returns:
52+
A callable that takes bytes content and returns the file path.
53+
54+
Example:
55+
def test_with_content(fake_fs_with_file, fake_fs) -> None:
56+
path = fake_fs_with_file(b'{"id":"alice"}\\n')
57+
content = fake_fs.get_content(path)
58+
assert b"alice" in content
59+
"""
60+
counter = 0
61+
62+
def create_file(content: bytes) -> "Path":
63+
nonlocal counter
64+
counter += 1
65+
path = tmp_path / f"test_{counter}.jsonlt"
66+
fake_fs.set_content(path, content)
67+
return path
68+
69+
return create_file
70+
71+
72+
@pytest.fixture
73+
def make_table(
74+
tmp_path: "Path",
75+
) -> "Callable[..., Table]":
76+
"""Factory fixture for creating Table instances.
77+
78+
Provides a convenient way to create tables with sensible defaults
79+
while allowing full customization through keyword arguments.
80+
81+
Returns:
82+
A callable that creates Table instances.
83+
84+
Example:
85+
def test_table_operations(make_table) -> None:
86+
table = make_table() # Creates table with key="id"
87+
table.put({"id": "alice", "role": "admin"})
88+
assert table.get("alice") is not None
89+
90+
def test_with_custom_key(make_table) -> None:
91+
table = make_table(key=("org", "id"))
92+
table.put({"org": "acme", "id": 1, "name": "alice"})
93+
"""
94+
counter = 0
95+
96+
def create_table(
97+
*,
98+
key: str | tuple[str, ...] = "id",
99+
content: str | None = None,
100+
auto_reload: bool = True,
101+
max_file_size: int | None = None,
102+
lock_timeout: float | None = None,
103+
_fs: FakeFileSystem | None = None,
104+
) -> Table:
105+
nonlocal counter
106+
counter += 1
107+
path = tmp_path / f"table_{counter}.jsonlt"
108+
109+
if content is not None:
110+
_ = path.write_text(content)
111+
112+
return Table(
113+
path,
114+
key=key,
115+
auto_reload=auto_reload,
116+
max_file_size=max_file_size,
117+
lock_timeout=lock_timeout,
118+
_fs=_fs,
119+
)
120+
121+
return create_table
122+
123+
124+
@pytest.fixture
125+
def make_table_with_records(
126+
make_table: "Callable[..., Table]",
127+
) -> "Callable[..., Table]":
128+
"""Factory fixture for creating pre-populated Table instances.
129+
130+
Builds on make_table to provide convenient record seeding.
131+
132+
Returns:
133+
A callable that creates Table instances with initial records.
134+
135+
Example:
136+
def test_populated_table(make_table_with_records) -> None:
137+
table = make_table_with_records([
138+
{"id": "alice", "role": "admin"},
139+
{"id": "bob", "role": "user"},
140+
])
141+
assert table.count() == 2
142+
"""
143+
144+
def create_table_with_records(
145+
records: "list[JSONObject]",
146+
*,
147+
key: str | tuple[str, ...] = "id",
148+
auto_reload: bool = True,
149+
max_file_size: int | None = None,
150+
lock_timeout: float | None = None,
151+
_fs: FakeFileSystem | None = None,
152+
) -> Table:
153+
table = make_table(
154+
key=key,
155+
auto_reload=auto_reload,
156+
max_file_size=max_file_size,
157+
lock_timeout=lock_timeout,
158+
_fs=_fs,
159+
)
160+
for record in records:
161+
table.put(record)
162+
return table
5163

6-
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
7-
test_dir = Path(__file__).parent
8-
for item in items:
9-
if Path(item.fspath).is_relative_to(test_dir):
10-
item.add_marker(pytest.mark.unit)
164+
return create_table_with_records

tests/unit/test_json.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -243,20 +243,6 @@ def test_complex_nested_structure(self) -> None:
243243

244244

245245
class TestSerializationDeterminism:
246-
def test_consistent_output_across_calls(self) -> None:
247-
value = {"zebra": 1, "apple": 2, "Banana": 3}
248-
result1 = serialize_json(value)
249-
result2 = serialize_json(value)
250-
assert result1 == result2
251-
252-
def test_consistent_for_identical_data(self) -> None:
253-
# Same data constructed differently should serialize identically
254-
value1 = {"b": 2, "a": 1}
255-
value2 = {"a": 1, "b": 2}
256-
result1 = serialize_json(value1)
257-
result2 = serialize_json(value2)
258-
assert result1 == result2
259-
260246
def test_preserves_value_types(self) -> None:
261247
value = {
262248
"null": None,

tests/unit/test_keys.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,6 @@ class TestCompareKeys:
240240
@pytest.mark.parametrize(
241241
("a", "b", "expected"),
242242
[
243-
# Equal values
244-
(42, 42, 0),
245-
("alice", "alice", 0),
246-
(("a", 1), ("a", 1), 0),
247243
# Integer comparisons
248244
(1, 2, -1),
249245
(2, 1, 1),
@@ -255,13 +251,6 @@ class TestCompareKeys:
255251
# Unicode code point ordering: uppercase before lowercase
256252
("Alice", "alice", -1),
257253
("Zebra", "apple", -1),
258-
# Cross-type ordering: int < str < tuple
259-
(42, "42", -1),
260-
("42", 42, 1),
261-
("alice", ("alice",), -1),
262-
(("alice",), "alice", 1),
263-
(42, ("a", 1), -1),
264-
(("a", 1), 42, 1),
265254
# Tuple element ordering
266255
(("a", 1), ("a", 2), -1),
267256
(("a", 2), ("b", 1), -1),
@@ -271,9 +260,6 @@ class TestCompareKeys:
271260
((1, "a"), ("a", 1), -1),
272261
],
273262
ids=[
274-
"equal_integers",
275-
"equal_strings",
276-
"equal_tuples",
277263
"less_integer",
278264
"greater_integer",
279265
"negative_less",
@@ -282,12 +268,6 @@ class TestCompareKeys:
282268
"greater_string",
283269
"uppercase_before_lowercase",
284270
"code_point_ordering",
285-
"int_before_string",
286-
"string_after_int",
287-
"string_before_tuple",
288-
"tuple_after_string",
289-
"int_before_tuple",
290-
"tuple_after_int",
291271
"tuple_element_ordering",
292272
"tuple_first_element_wins",
293273
"shorter_tuple_first",

tests/unit/test_state.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@
1010

1111

1212
class TestComputeLogicalState:
13-
def test_empty_operations_returns_empty_state(self) -> None:
14-
operations: list[JSONObject] = []
15-
state = compute_logical_state(operations, "id")
16-
assert state == {}
17-
18-
def test_single_record(self) -> None:
19-
operations: list[JSONObject] = [{"id": "alice", "role": "admin"}]
20-
state = compute_logical_state(operations, "id")
21-
assert state == {"alice": {"id": "alice", "role": "admin"}}
22-
2313
def test_multiple_records_distinct_keys(self) -> None:
2414
operations: list[JSONObject] = [
2515
{"id": "alice", "role": "admin"},
@@ -32,42 +22,6 @@ def test_multiple_records_distinct_keys(self) -> None:
3222
assert state["bob"] == {"id": "bob", "role": "user"}
3323
assert state["carol"] == {"id": "carol", "role": "user"}
3424

35-
def test_upsert_overwrites(self) -> None:
36-
operations: list[JSONObject] = [
37-
{"id": "alice", "role": "user"},
38-
{"id": "alice", "role": "admin"},
39-
]
40-
state = compute_logical_state(operations, "id")
41-
assert len(state) == 1
42-
assert state["alice"] == {"id": "alice", "role": "admin"}
43-
44-
def test_tombstone_removes(self) -> None:
45-
operations: list[JSONObject] = [
46-
{"id": "alice", "role": "admin"},
47-
{"$deleted": True, "id": "alice"},
48-
]
49-
state = compute_logical_state(operations, "id")
50-
assert state == {}
51-
52-
def test_tombstone_nonexistent_key(self) -> None:
53-
operations: list[JSONObject] = [
54-
{"id": "alice", "role": "admin"},
55-
{"$deleted": True, "id": "bob"},
56-
]
57-
state = compute_logical_state(operations, "id")
58-
assert len(state) == 1
59-
assert state["alice"] == {"id": "alice", "role": "admin"}
60-
61-
def test_reinsert_after_delete(self) -> None:
62-
operations: list[JSONObject] = [
63-
{"id": "alice", "role": "admin"},
64-
{"$deleted": True, "id": "alice"},
65-
{"id": "alice", "role": "user"},
66-
]
67-
state = compute_logical_state(operations, "id")
68-
assert len(state) == 1
69-
assert state["alice"] == {"id": "alice", "role": "user"}
70-
7125
def test_integer_key(self) -> None:
7226
operations: list[JSONObject] = [
7327
{"id": 1, "name": "first"},

0 commit comments

Comments
 (0)