Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions tests/benchmarks/conftest.py

This file was deleted.

34 changes: 34 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 0 additions & 10 deletions tests/examples/conftest.py

This file was deleted.

10 changes: 0 additions & 10 deletions tests/fuzz/conftest.py

This file was deleted.

10 changes: 0 additions & 10 deletions tests/integration/conftest.py

This file was deleted.

10 changes: 0 additions & 10 deletions tests/properties/conftest.py

This file was deleted.

166 changes: 160 additions & 6 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 0 additions & 14 deletions tests/unit/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 0 additions & 20 deletions tests/unit/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -271,9 +260,6 @@ class TestCompareKeys:
((1, "a"), ("a", 1), -1),
],
ids=[
"equal_integers",
"equal_strings",
"equal_tuples",
"less_integer",
"greater_integer",
"negative_less",
Expand All @@ -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",
Expand Down
46 changes: 0 additions & 46 deletions tests/unit/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -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"},
Expand Down
Loading