test: relocate service-layer tests to tests/services/
Mirrors the source layout (core/services split) so unit/ holds core-only tests and services get their own home.
This commit is contained in:
parent
dd9fa26b52
commit
7093164f75
9 changed files with 144 additions and 79 deletions
107
tests/services/test_persistence.py
Normal file
107
tests/services/test_persistence.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""Unit tests for the persistence service — JSON round-trip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from otko.core import (
|
||||
Concrete02,
|
||||
ElasticBeamColumn,
|
||||
ElasticSection,
|
||||
LinearTimeSeries,
|
||||
NodalLoad,
|
||||
Node,
|
||||
PlainLoadPattern,
|
||||
Project,
|
||||
ProjectMeta,
|
||||
Steel01,
|
||||
TrussElement,
|
||||
UnitSystem,
|
||||
)
|
||||
from otko.services import PROJECT_FILE_SUFFIX, load_project, save_project
|
||||
|
||||
|
||||
def _sample_project() -> Project:
|
||||
return Project(
|
||||
meta=ProjectMeta(name="Sample", author="Ozan", units=UnitSystem.SI_M_N),
|
||||
ndm=3,
|
||||
ndf=6,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6),
|
||||
Node(id=2, coords=(0, 0, 3.0)),
|
||||
],
|
||||
materials=[
|
||||
Steel01(id=1, Fy=420e6, E0=200e9, b=0.01),
|
||||
Concrete02(
|
||||
id=2,
|
||||
fpc=-30e6,
|
||||
epsc0=-0.002,
|
||||
fpcu=-15e6,
|
||||
epsU=-0.005,
|
||||
ft=3e6,
|
||||
Ets=2e9,
|
||||
**{"lambda": 0.1},
|
||||
),
|
||||
],
|
||||
sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=8.33e-6, Iy=8.33e-6, G=80e9, J=1e-6)],
|
||||
elements=[
|
||||
ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1, geom_transf="PDelta"),
|
||||
TrussElement(id=2, nodes=(1, 2), area=1e-3, material_id=1),
|
||||
],
|
||||
time_series=[LinearTimeSeries(id=1, factor=1.0)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(0, 0, -10e3, 0, 0, 0))],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_round_trip_preserves_everything(tmp_path: Path) -> None:
|
||||
original = _sample_project()
|
||||
target = save_project(original, tmp_path / "model")
|
||||
assert target.suffix == PROJECT_FILE_SUFFIX
|
||||
assert target.exists()
|
||||
|
||||
restored = load_project(target)
|
||||
assert restored.model_dump(by_alias=True) == original.model_dump(by_alias=True)
|
||||
|
||||
|
||||
def test_save_appends_suffix_when_missing(tmp_path: Path) -> None:
|
||||
p = Project()
|
||||
out = save_project(p, tmp_path / "no_extension")
|
||||
assert out.suffix == PROJECT_FILE_SUFFIX
|
||||
|
||||
|
||||
def test_save_keeps_existing_suffix(tmp_path: Path) -> None:
|
||||
p = Project()
|
||||
out = save_project(p, tmp_path / "with_ext.osmodel")
|
||||
assert out.name == "with_ext.osmodel"
|
||||
|
||||
|
||||
def test_load_missing_file_raises() -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_project("/no/such/path/x.osmodel")
|
||||
|
||||
|
||||
def test_load_corrupt_file_raises_validation_error(tmp_path: Path) -> None:
|
||||
bad = tmp_path / "bad.osmodel"
|
||||
bad.write_text('{"ndm": 2, "ndf": 6}', encoding="utf-8") # invalid combo
|
||||
with pytest.raises(ValidationError):
|
||||
load_project(bad)
|
||||
|
||||
|
||||
def test_polymorphic_collection_dispatches_correctly(tmp_path: Path) -> None:
|
||||
"""Materials list must restore as Steel01 + Concrete02 — not generic Entity."""
|
||||
original = _sample_project()
|
||||
out = save_project(original, tmp_path / "x")
|
||||
restored = load_project(out)
|
||||
assert isinstance(restored.materials[0], Steel01)
|
||||
assert isinstance(restored.materials[1], Concrete02)
|
||||
assert isinstance(restored.elements[0], ElasticBeamColumn)
|
||||
assert isinstance(restored.elements[1], TrussElement)
|
||||
Loading…
Reference in a new issue