Skip to content

Commit b5eb795

Browse files
test(units): add test suite for QUDT units integration
- Add unit tests - Add E2E CLI tests - Add fixtures for units sync functionality. - Includes mocking for external QUDT API calls. Signed-off-by: Mustafa Kaptan <[email protected]>
1 parent 479bffa commit b5eb795

File tree

4 files changed

+360
-55
lines changed

4 files changed

+360
-55
lines changed

tests/conftest.py

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from collections import UserDict
21
from collections.abc import Callable
32
from dataclasses import dataclass, field
3+
from pathlib import Path
44
from typing import Any
55

66
import pytest
@@ -17,22 +17,11 @@
1717
from s2dm.exporters.utils.extraction import get_all_named_types
1818
from s2dm.exporters.utils.schema_loader import ensure_query
1919
from s2dm.idgen.models import IDGenerationSpec
20+
from s2dm.units.sync import UnitRow, _uri_to_enum_symbol
2021

2122
SCALAR_TYPES = ["String", "Int", "Float", "Boolean"]
2223

2324

24-
class EchoDict(UserDict[str, str]):
25-
"""A dictionary that echoes the key as the value."""
26-
27-
def __missing__(self, key: str) -> str:
28-
return key
29-
30-
31-
@pytest.fixture(scope="session")
32-
def mock_unit_lookup() -> EchoDict:
33-
return EchoDict()
34-
35-
3625
@dataclass
3726
class MockFieldData:
3827
name: str
@@ -58,7 +47,7 @@ def enum_name(self) -> str:
5847
def enum_schema_str(self) -> str:
5948
return f"""
6049
enum {self.enum_name if self.is_enum_field else self.unit_enum_name} {{
61-
{' '.join(self.allowed or self.unit_allowed_values)}
50+
{" ".join(self.allowed or self.unit_allowed_values)}
6251
}}
6352
"""
6453

@@ -143,10 +132,10 @@ def mock_graphql_schema_strategy(
143132
vehicle: Vehicle
144133
}}
145134
146-
{' '.join(enums)}
135+
{" ".join(enums)}
147136
148137
type Vehicle {{
149-
{' '.join(map(lambda f: f.to_field_str(), fields))}
138+
{" ".join(map(lambda f: f.to_field_str(), fields))}
150139
}}
151140
"""
152141
)
@@ -160,3 +149,96 @@ def mock_named_types_strategy(
160149
) -> tuple[list[GraphQLNamedType], list[MockFieldData]]:
161150
schema, fields = draw(mock_graphql_schema_strategy())
162151
return get_all_named_types(schema), fields
152+
153+
154+
# Constants for common test values
155+
MOCK_QUDT_VERSION = "3.1.5"
156+
MOCK_ENUM_CONTENT = "enum TestEnum { TEST }"
157+
158+
# QUDT constants for unit testing
159+
QUDT_UNIT_BASE = "http://qudt.org/vocab/unit"
160+
QUDT_QK_BASE = "http://qudt.org/vocab/quantitykind"
161+
162+
# Standard test paths for units sync
163+
STANDARD_UNIT_PATHS = [
164+
"velocity/VelocityUnitEnum.graphql",
165+
"mass/MassUnitEnum.graphql",
166+
"length/LengthUnitEnum.graphql",
167+
]
168+
169+
170+
@pytest.fixture
171+
def mock_qudt_version() -> str:
172+
"""Standard QUDT version for testing."""
173+
return MOCK_QUDT_VERSION
174+
175+
176+
@pytest.fixture
177+
def mock_check_latest_qudt_version(mock_qudt_version: str) -> Callable[[], str]:
178+
"""Mock function for checking latest QUDT version."""
179+
180+
def _mock() -> str:
181+
return mock_qudt_version
182+
183+
return _mock
184+
185+
186+
@pytest.fixture
187+
def mock_sync_qudt_units() -> Callable[..., list[Path]]:
188+
"""Mock function for sync_qudt_units with configurable behavior."""
189+
190+
def _mock(
191+
units_root: Path,
192+
version: str | None = None,
193+
*,
194+
dry_run: bool = False,
195+
num_paths: int = 3,
196+
create_files: bool = True,
197+
) -> list[Path]:
198+
# Generate test paths based on the number requested
199+
test_paths = [units_root / path for path in STANDARD_UNIT_PATHS[:num_paths]]
200+
201+
# Simulate creating files only when not in dry-run mode and create_files is True
202+
if not dry_run and create_files:
203+
for path in test_paths:
204+
path.parent.mkdir(parents=True, exist_ok=True)
205+
path.write_text(MOCK_ENUM_CONTENT)
206+
207+
return test_paths
208+
209+
return _mock
210+
211+
212+
@pytest.fixture
213+
def units_sync_mocks(
214+
monkeypatch: pytest.MonkeyPatch,
215+
mock_sync_qudt_units: Callable[..., list[Path]],
216+
mock_check_latest_qudt_version: Callable[[], str],
217+
) -> tuple[Callable[..., list[Path]], Callable[[], str]]:
218+
"""Fixture that applies all units sync mocks at once."""
219+
monkeypatch.setattr("s2dm.cli.sync_qudt_units", mock_sync_qudt_units)
220+
monkeypatch.setattr("s2dm.cli.check_latest_qudt_version", mock_check_latest_qudt_version)
221+
return mock_sync_qudt_units, mock_check_latest_qudt_version
222+
223+
224+
def create_test_unit_row(
225+
unit_segment: str,
226+
qk_segment: str,
227+
label: str | None = None,
228+
ucum: str | None = None,
229+
) -> "UnitRow":
230+
"""Create a test UnitRow with minimal required data.
231+
232+
Note: This imports UnitRow and _uri_to_enum_symbol locally to avoid circular imports.
233+
"""
234+
unit_label = label or unit_segment.lower().replace("-", " ")
235+
symbol = _uri_to_enum_symbol(f"{QUDT_UNIT_BASE}/{unit_segment}")
236+
237+
return UnitRow(
238+
unit_iri=f"{QUDT_UNIT_BASE}/{unit_segment}",
239+
unit_label=unit_label,
240+
quantity_kind_iri=f"{QUDT_QK_BASE}/{qk_segment}",
241+
quantity_kind_label=qk_segment,
242+
symbol=symbol,
243+
ucum_code=ucum or unit_segment.lower(),
244+
)

tests/test_e2e_cli.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections.abc import Callable
23
from pathlib import Path
34
from typing import Any
45

@@ -11,7 +12,6 @@
1112
SAMPLE1 = TESTS_DATA / "schema1.graphql"
1213
SAMPLE2 = TESTS_DATA / "schema2.graphql"
1314
SAMPLE3 = TESTS_DATA / "schema3.graphql"
14-
UNITS = TESTS_DATA / "test_units.yaml"
1515

1616
# Version bump test schemas
1717
BASE_SCHEMA = TESTS_DATA / "base.graphql"
@@ -192,19 +192,7 @@ def test_registry_export_concept_uri(runner: CliRunner, tmp_outputs: Path) -> No
192192

193193
def test_registry_export_id(runner: CliRunner, tmp_outputs: Path) -> None:
194194
out = tmp_outputs / "ids.json"
195-
result = runner.invoke(
196-
cli,
197-
[
198-
"registry",
199-
"id",
200-
"-s",
201-
str(SAMPLE1),
202-
"-u",
203-
str(UNITS),
204-
"-o",
205-
str(out),
206-
],
207-
)
195+
result = runner.invoke(cli, ["registry", "id", "-s", str(SAMPLE1), "-o", str(out)])
208196
assert result.exit_code == 0, result.output
209197
assert out.exists()
210198
with open(out) as f:
@@ -215,7 +203,7 @@ def test_registry_export_id(runner: CliRunner, tmp_outputs: Path) -> None:
215203

216204
def test_registry_init(runner: CliRunner, tmp_outputs: Path) -> None:
217205
out = tmp_outputs / "spec_history.json"
218-
result = runner.invoke(cli, ["registry", "init", "-s", str(SAMPLE1), "-u", str(UNITS), "-o", str(out)])
206+
result = runner.invoke(cli, ["registry", "init", "-s", str(SAMPLE1), "-o", str(out)])
219207
assert result.exit_code == 0, result.output
220208
assert out.exists()
221209
with open(out) as f:
@@ -230,21 +218,19 @@ def test_registry_init(runner: CliRunner, tmp_outputs: Path) -> None:
230218
spec_history
231219
and isinstance(spec_history, list)
232220
and isinstance(spec_history[0], dict)
233-
and spec_history[0].get("@id") == "0xEC20D822"
221+
and spec_history[0].get("@id") == "0x32E14E88"
234222
):
235223
found = True
236224
break
237-
assert found, 'Expected entry with "@id": "ns:Vehicle.averageSpeed" and specHistory id "0xEC20D822" not found.'
225+
assert found, 'Expected entry with "@id": "ns:Vehicle.averageSpeed" and specHistory id "0x32E14E88" not found.'
238226

239227

240228
def test_registry_update(runner: CliRunner, tmp_outputs: Path) -> None:
241229
out = tmp_outputs / "spec_history_update.json"
242230
# First, create a spec history file
243231
init_out = tmp_outputs / "spec_history.json"
244-
runner.invoke(cli, ["registry", "init", "-s", str(SAMPLE1), "-u", str(UNITS), "-o", str(init_out)])
245-
runner.invoke(
246-
cli, ["registry", "update", "-s", str(SAMPLE2), "-u", str(UNITS), "-sh", str(init_out), "-o", str(out)]
247-
)
232+
runner.invoke(cli, ["registry", "init", "-s", str(SAMPLE1), "-o", str(init_out)])
233+
runner.invoke(cli, ["registry", "update", "-s", str(SAMPLE2), "-sh", str(init_out), "-o", str(out)])
248234
assert out.exists()
249235
with open(out) as f:
250236
data = json.load(f)
@@ -256,13 +242,13 @@ def test_registry_update(runner: CliRunner, tmp_outputs: Path) -> None:
256242
if isinstance(entry, dict) and entry.get("@id") == "ns:Vehicle.averageSpeed":
257243
spec_history = entry.get("specHistory", [])
258244
ids = [h.get("@id") for h in spec_history if isinstance(h, dict)]
259-
if "0xEC20D822" in ids:
245+
if "0x32E14E88" in ids:
260246
found_old = True
261-
if "0xB86BF561" in ids:
247+
if "0xE47AA673" in ids:
262248
found_new = True
263249
break
264-
assert found_old, 'Expected old specHistory id "0xEC20D822" not found.'
265-
assert found_new, 'Expected new specHistory id "0xB86BF561" not found.'
250+
assert found_old, 'Expected old specHistory id "0x32E14E88" not found.'
251+
assert found_new, 'Expected new specHistory id "0xE47AA673" not found.'
266252

267253

268254
@pytest.mark.parametrize(
@@ -397,3 +383,33 @@ def test_stats_graphql(runner: CliRunner) -> None:
397383
print(f"{result.output=}")
398384
assert result.exit_code == 0, result.output
399385
assert "'UInt32': 1" in result.output
386+
387+
388+
def test_units_sync_cli(
389+
runner: CliRunner,
390+
tmp_outputs: Path,
391+
monkeypatch: pytest.MonkeyPatch,
392+
mock_sync_qudt_units: Callable[..., list[Path]],
393+
mock_check_latest_qudt_version: Callable[[], str],
394+
) -> None:
395+
"""Test that the units sync CLI command works end-to-end."""
396+
397+
# Simple mock that just returns a few paths
398+
def simple_mock(units_root: Path, version: str | None = None, *, dry_run: bool = False) -> list[Path]:
399+
return mock_sync_qudt_units(units_root, version, dry_run=dry_run, num_paths=2)
400+
401+
# Apply mocks to avoid network calls
402+
monkeypatch.setattr("s2dm.cli.sync_qudt_units", simple_mock)
403+
monkeypatch.setattr("s2dm.cli.check_latest_qudt_version", mock_check_latest_qudt_version)
404+
405+
output_dir = tmp_outputs / "units_cli_test"
406+
407+
# Test basic CLI functionality
408+
result = runner.invoke(cli, ["units", "sync", "--output-dir", str(output_dir)])
409+
assert result.exit_code == 0, result.output
410+
assert "Generated" in result.output or "enum files" in result.output
411+
412+
# Test that --dry-run flag is accepted (detailed behavior tested in unit tests)
413+
result = runner.invoke(cli, ["units", "sync", "--output-dir", str(output_dir), "--dry-run"])
414+
assert result.exit_code == 0, result.output
415+
assert "Would generate" in result.output or "dry" in result.output.lower()

tests/test_idgen.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from s2dm.idgen.idgen import fnv1_32_wrapper
1111
from s2dm.idgen.models import IDGenerationSpec
1212
from tests.conftest import (
13-
EchoDict,
1413
MockFieldData,
1514
mock_named_types_strategy,
1615
)
@@ -19,14 +18,13 @@
1918
@given(named_types_and_fields=mock_named_types_strategy())
2019
def test_id_spec_generation_of_all_fields_from_graphql_schema(
2120
named_types_and_fields: tuple[list[GraphQLNamedType], list[MockFieldData]],
22-
mock_unit_lookup: EchoDict,
2321
) -> None:
2422
"""Test that ID generation spec can be generated from a GraphQL schema."""
2523
named_types, fields = named_types_and_fields
2624
expected_id_specs = {field.expected_id_spec() for field in fields}
2725

28-
exporter = IDExporter(schema=None, units_file=None, output=None, strict_mode=False, dry_run=True) # type: ignore [arg-type]
29-
all_id_specs = set(exporter.iter_all_id_specs(named_types, mock_unit_lookup)) # type: ignore [arg-type]
26+
exporter = IDExporter(schema=None, output=None, strict_mode=False, dry_run=True) # type: ignore [arg-type]
27+
all_id_specs = set(exporter.iter_all_id_specs(named_types))
3028

3129
for expected_id_spec in expected_id_specs:
3230
assert expected_id_spec in all_id_specs
@@ -35,16 +33,14 @@ def test_id_spec_generation_of_all_fields_from_graphql_schema(
3533
@given(named_types_and_fields=mock_named_types_strategy())
3634
@pytest.mark.parametrize("strict_mode", [True, False])
3735
def test_id_generation_is_deterministic_across_iterations(
38-
named_types_and_fields: tuple[list[GraphQLNamedType], list[MockFieldData]],
39-
strict_mode: bool,
40-
mock_unit_lookup: EchoDict,
36+
named_types_and_fields: tuple[list[GraphQLNamedType], list[MockFieldData]], strict_mode: bool
4137
) -> None:
4238
"""Test that ID generation is deterministic across iterations."""
4339

4440
named_types, _ = named_types_and_fields
4541

46-
exporter = IDExporter(schema=None, units_file=None, output=None, strict_mode=strict_mode, dry_run=True) # type: ignore [arg-type]
47-
all_id_specs = set(exporter.iter_all_id_specs(named_types, mock_unit_lookup)) # type: ignore [arg-type]
42+
exporter = IDExporter(schema=None, output=None, strict_mode=strict_mode, dry_run=True) # type: ignore [arg-type]
43+
all_id_specs = set(exporter.iter_all_id_specs(named_types))
4844

4945
first_iteration_ids = {}
5046
for id_spec in all_id_specs:
@@ -62,17 +58,15 @@ def test_id_generation_is_deterministic_across_iterations(
6258
@given(named_types_and_fields=mock_named_types_strategy())
6359
@pytest.mark.parametrize("strict_mode", [True, False])
6460
def test_id_generation_is_unique_accros_schema(
65-
named_types_and_fields: tuple[list[GraphQLNamedType], list[MockFieldData]],
66-
strict_mode: bool,
67-
mock_unit_lookup: EchoDict,
61+
named_types_and_fields: tuple[list[GraphQLNamedType], list[MockFieldData]], strict_mode: bool
6862
) -> None:
6963
"""Test that ID generation produces unique IDs across the schema fields."""
7064

7165
named_types, _ = named_types_and_fields
7266

73-
exporter = IDExporter(schema=None, units_file=None, output=None, strict_mode=strict_mode, dry_run=True) # type: ignore [arg-type]
67+
exporter = IDExporter(schema=None, output=None, strict_mode=strict_mode, dry_run=True) # type: ignore [arg-type]
7468
ids = {}
75-
for id_spec in exporter.iter_all_id_specs(named_types, mock_unit_lookup): # type: ignore [arg-type]
69+
for id_spec in exporter.iter_all_id_specs(named_types):
7670
field_id = fnv1_32_wrapper(id_spec, strict_mode=strict_mode)
7771
ids[id_spec.name] = field_id
7872

0 commit comments

Comments
 (0)