Some checks failed
CI / lint (pull_request) Has been cancelled
CI / type (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.12) (pull_request) Has been cancelled
CI / test-gui (pull_request) Has been cancelled
CI / test-integration (macos-latest) (pull_request) Has been cancelled
CI / test-integration (ubuntu-latest) (pull_request) Has been cancelled
CI / test-integration (windows-latest) (pull_request) Has been cancelled
Auto-fixes (UP037, UP038, F401, I001, SIM105, RUF100) plus hand-fixes: drop the now-unused `Union` imports, annotate mutable class attributes with ClassVar, raise ValidationError instead of blind Exception in the zero-length test, delete two dead locals, and bind the per-iteration grid-builder closures to their loop variables (B023).
391 lines
14 KiB
Python
391 lines
14 KiB
Python
"""
|
|
Tests for tools/gidopensees_import/parse_schemas.py.
|
|
|
|
Fast tests run without gidopensees present (snapshot + unit tests).
|
|
Integration tests that read the real .mat/.cnd files are skipped when the
|
|
files cannot be found, so CI still passes in environments without the
|
|
upstream repo checked out.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tools.gidopensees_import.parse_schemas import (
|
|
ParseError,
|
|
_parse_dependencies,
|
|
_parse_question,
|
|
_parse_value,
|
|
build_catalog,
|
|
parse_mat,
|
|
)
|
|
from tools.gidopensees_import.schema_model import CatalogSpec
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Paths
|
|
# ---------------------------------------------------------------------------
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
MINIMAL_MAT = FIXTURES / "minimal.mat"
|
|
MINIMAL_EXPECTED = FIXTURES / "minimal_expected.json"
|
|
|
|
GIDOPENSEES = Path("d:/GitHub/gidopensees")
|
|
REAL_MAT = GIDOPENSEES / "OpenSees.mat"
|
|
REAL_CND = GIDOPENSEES / "OpenSees.cnd"
|
|
|
|
_have_real = REAL_MAT.exists() and REAL_CND.exists()
|
|
real_files = pytest.mark.skipif(not _have_real, reason="gidopensees repo not present")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit tests — _parse_question
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseQuestion:
|
|
def test_cb_with_options(self) -> None:
|
|
name, wtype, opts = _parse_question("Formulation#CB#(Stress-Strain,Force-Deformation)")
|
|
assert name == "Formulation"
|
|
assert wtype == "CB"
|
|
assert opts == ["Stress-Strain", "Force-Deformation"]
|
|
|
|
def test_cb_single_underscore(self) -> None:
|
|
name, wtype, opts = _parse_question("_#CB#(_)")
|
|
assert name == "_"
|
|
assert wtype == "CB"
|
|
assert opts == ["_"]
|
|
|
|
def test_units(self) -> None:
|
|
name, wtype, opts = _parse_question("Elastic_modulus_E#UNITS#")
|
|
assert name == "Elastic_modulus_E"
|
|
assert wtype == "UNITS"
|
|
assert opts == []
|
|
|
|
def test_mat_with_books(self) -> None:
|
|
name, wtype, opts = _parse_question(
|
|
"Ux_material#MAT#(Standard_Uniaxial_Materials,Uniaxial_Steel_Materials)"
|
|
)
|
|
assert name == "Ux_material"
|
|
assert wtype == "MAT"
|
|
assert "Standard_Uniaxial_Materials" in opts
|
|
|
|
def test_tuple(self) -> None:
|
|
name, wtype, opts = _parse_question(
|
|
"Cyclic_data(Max_compressive_strain,Max_tensile_strain,Number_of_cycles)"
|
|
)
|
|
assert name == "Cyclic_data"
|
|
assert wtype == "TUPLE"
|
|
assert opts == ["Max_compressive_strain", "Max_tensile_strain", "Number_of_cycles"]
|
|
|
|
def test_plain_scalar(self) -> None:
|
|
name, wtype, opts = _parse_question("Parameter_R0")
|
|
assert name == "Parameter_R0"
|
|
assert wtype == "SCALAR"
|
|
assert opts == []
|
|
|
|
def test_label_with_trailing_colon(self) -> None:
|
|
name, wtype, opts = _parse_question("Material:#CB#(Steel02)")
|
|
assert name == "Material"
|
|
assert wtype == "CB"
|
|
assert opts == ["Steel02"]
|
|
|
|
def test_plain_label_colon(self) -> None:
|
|
name, wtype, _opts = _parse_question("Layer_name:")
|
|
assert name == "Layer_name"
|
|
assert wtype == "SCALAR"
|
|
|
|
def test_cb_no_options(self) -> None:
|
|
name, wtype, opts = _parse_question("Section:#CB#(Fiber)")
|
|
assert name == "Section"
|
|
assert wtype == "CB"
|
|
assert opts == ["Fiber"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit tests — _parse_value
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseValue:
|
|
def test_simple(self) -> None:
|
|
val, width = _parse_value("30 GPa")
|
|
assert val == "30 GPa"
|
|
assert width is None
|
|
|
|
def test_width_suffix(self) -> None:
|
|
val, width = _parse_value("0.0#WIDTH#(10)")
|
|
assert val == "0.0"
|
|
assert width == 10
|
|
|
|
def test_number_width(self) -> None:
|
|
val, width = _parse_value("20#WIDTH#(10)")
|
|
assert val == "20"
|
|
assert width == 10
|
|
|
|
def test_n_prefix(self) -> None:
|
|
val, width = _parse_value("#N# 3 -0.005 0.005 5")
|
|
assert val == "#N# 3 -0.005 0.005 5"
|
|
assert width is None
|
|
|
|
def test_empty(self) -> None:
|
|
val, width = _parse_value("")
|
|
assert val == ""
|
|
assert width is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unit tests — _parse_dependencies
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestParseDependencies:
|
|
_path = Path("test.mat")
|
|
|
|
def test_single_group_restore_hide(self) -> None:
|
|
s = "(Stress-Strain,RESTORE,Mat_type,#CURRENT#,HIDE,Stiffness_K,#CURRENT#)"
|
|
rules = _parse_dependencies(s, self._path, 1)
|
|
assert len(rules) == 1
|
|
rule = rules[0]
|
|
assert rule.trigger == "Stress-Strain"
|
|
assert len(rule.actions) == 2
|
|
assert rule.actions[0].action == "RESTORE"
|
|
assert rule.actions[0].field == "Mat_type"
|
|
assert rule.actions[1].action == "HIDE"
|
|
assert rule.actions[1].field == "Stiffness_K"
|
|
|
|
def test_two_groups_same_line(self) -> None:
|
|
s = "(0,SET,Rz_material,#CURRENT#) (1,RESTORE,Rz_material,#CURRENT#)"
|
|
rules = _parse_dependencies(s, self._path, 1)
|
|
assert len(rules) == 2
|
|
assert rules[0].trigger == "0"
|
|
assert rules[0].actions[0].action == "SET"
|
|
assert rules[1].trigger == "1"
|
|
assert rules[1].actions[0].action == "RESTORE"
|
|
|
|
def test_set_with_literal_value(self) -> None:
|
|
s = "(1,SET,Void_width_dv,0.0m,SET,Number_of_voids,0)"
|
|
rules = _parse_dependencies(s, self._path, 1)
|
|
assert len(rules) == 1
|
|
assert rules[0].actions[0].target == "0.0m"
|
|
assert rules[0].actions[1].target == "0"
|
|
|
|
def test_no_groups_raises(self) -> None:
|
|
with pytest.raises(ParseError):
|
|
_parse_dependencies("no parentheses here", self._path, 5)
|
|
|
|
def test_literal_integer_trigger(self) -> None:
|
|
s = "(1,RESTORE,Gap_length,#CURRENT#)"
|
|
rules = _parse_dependencies(s, self._path, 1)
|
|
assert rules[0].trigger == "1"
|
|
|
|
def test_fix_xyz_multi_action(self) -> None:
|
|
s = "(Fix_XYZ,RESTORE,X-Translation,1,RESTORE,Y-Translation,1,RESTORE,Z-Translation,1)"
|
|
rules = _parse_dependencies(s, self._path, 1)
|
|
assert len(rules) == 1
|
|
assert rules[0].trigger == "Fix_XYZ"
|
|
assert len(rules[0].actions) == 3
|
|
for act in rules[0].actions:
|
|
assert act.target == "1"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Snapshot test — minimal fixture
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSnapshotMinimal:
|
|
def test_minimal_mat_matches_golden(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
cat = CatalogSpec(mat_books=books)
|
|
actual = json.loads(cat.model_dump_json())
|
|
expected = json.loads(MINIMAL_EXPECTED.read_text(encoding="utf-8"))
|
|
assert actual == expected
|
|
|
|
def test_minimal_has_one_book(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
assert len(books) == 1
|
|
assert books[0].name == "Test_Book"
|
|
|
|
def test_minimal_entry_fields(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
entry = books[0].entries[0]
|
|
assert entry.name == "TestMaterial"
|
|
assert entry.comment.startswith("A minimal test material")
|
|
field_names = [f.name for f in entry.fields]
|
|
assert "Elastic_modulus_E" in field_names
|
|
assert "Stiffness_K" in field_names
|
|
assert "Gap_size" in field_names
|
|
assert "Combo_sub" in field_names
|
|
|
|
def test_formulation_two_deps(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
entry = books[0].entries[0]
|
|
form = next(f for f in entry.fields if f.name == "Formulation")
|
|
assert len(form.dependencies) == 2
|
|
triggers = {d.trigger for d in form.dependencies}
|
|
assert "Stress-Strain" in triggers
|
|
assert "Force-Deformation" in triggers
|
|
|
|
def test_gap_size_width_hint(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
entry = books[0].entries[0]
|
|
gap = next(f for f in entry.fields if f.name == "Gap_size")
|
|
assert gap.default == "0.0"
|
|
assert gap.width_hint == 10
|
|
|
|
def test_hidden_button_field(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
entry = books[0].entries[0]
|
|
hidden = [f for f in entry.fields if f.state == "HIDDEN"]
|
|
assert any(f.name == "_" and "TK_MaterialTester" in f.tkwidgets for f in hidden)
|
|
|
|
def test_tkwidget_on_first_field(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
entry = books[0].entries[0]
|
|
mat_field = entry.fields[0]
|
|
assert mat_field.name == "Material"
|
|
assert "TK_UpdateInfoBar" in mat_field.tkwidgets
|
|
assert mat_field.image == "img/test.png"
|
|
|
|
def test_tuple_field(self) -> None:
|
|
books = parse_mat(MINIMAL_MAT)
|
|
entry = books[0].entries[0]
|
|
combo = next(f for f in entry.fields if f.name == "Combo_sub")
|
|
assert combo.widget_type == "TUPLE"
|
|
assert combo.options == ["Val_A", "Val_B", "Val_C"]
|
|
assert combo.default == "#N# 3 1.0 2.0 3.0"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration tests — real gidopensees files
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@real_files
|
|
class TestRealFiles:
|
|
def test_mat_book_count(self, catalog: CatalogSpec) -> None:
|
|
assert len(catalog.mat_books) == 14
|
|
|
|
def test_cnd_book_count(self, catalog: CatalogSpec) -> None:
|
|
assert len(catalog.cnd_books) == 6
|
|
|
|
def test_mat_material_count(self, catalog: CatalogSpec) -> None:
|
|
total = sum(len(b.entries) for b in catalog.mat_books)
|
|
assert total == 58
|
|
|
|
def test_cnd_condition_count(self, catalog: CatalogSpec) -> None:
|
|
total = sum(len(b.entries) for b in catalog.cnd_books)
|
|
assert total >= 39
|
|
|
|
def test_steel02_field_names(self, catalog: CatalogSpec) -> None:
|
|
steel2 = _find_entry(catalog, "Steel02")
|
|
assert steel2 is not None
|
|
fnames = {f.name for f in steel2.fields}
|
|
required = {
|
|
"Steel_grade",
|
|
"Yield_Stress_Fy",
|
|
"Strain-hardening_ratio_b",
|
|
"Parameter_R0",
|
|
"Parameter_cR1",
|
|
"Parameter_cR2",
|
|
}
|
|
assert required.issubset(fnames), f"Missing: {required - fnames}"
|
|
|
|
def test_steel02_formulation_three_deps(self, catalog: CatalogSpec) -> None:
|
|
steel2 = _find_entry(catalog, "Steel02")
|
|
assert steel2 is not None
|
|
form = next(f for f in steel2.fields if f.name == "Formulation")
|
|
assert (
|
|
len(form.dependencies) == 3
|
|
), f"Expected 3 dependency rules on Formulation, got {len(form.dependencies)}"
|
|
|
|
def test_steel02_formulation_dep_triggers(self, catalog: CatalogSpec) -> None:
|
|
steel2 = _find_entry(catalog, "Steel02")
|
|
assert steel2 is not None
|
|
form = next(f for f in steel2.fields if f.name == "Formulation")
|
|
triggers = {d.trigger for d in form.dependencies}
|
|
assert triggers == {"Stress-Strain", "Force-Deformation", "Moment-Rotation"}
|
|
|
|
def test_concrete04_exists(self, catalog: CatalogSpec) -> None:
|
|
entry = _find_entry(catalog, "Concrete04_(Popovics_concrete)")
|
|
assert entry is not None
|
|
assert entry.book == "Uniaxial_Concrete_Materials"
|
|
|
|
def test_all_mat_entries_have_book(self, catalog: CatalogSpec) -> None:
|
|
for book in catalog.mat_books:
|
|
for entry in book.entries:
|
|
assert (
|
|
entry.book == book.name
|
|
), f"{entry.name!r} has book={entry.book!r}, expected {book.name!r}"
|
|
|
|
def test_all_fields_have_valid_widget_type(self, catalog: CatalogSpec) -> None:
|
|
valid = {"CB", "UNITS", "MAT", "SCALAR", "TUPLE"}
|
|
for book in catalog.mat_books:
|
|
for entry in book.entries:
|
|
for f in entry.fields:
|
|
assert (
|
|
f.widget_type in valid
|
|
), f"{entry.name}.{f.name}: unknown widget_type={f.widget_type!r}"
|
|
|
|
def test_point_restraints_condtype(self, catalog: CatalogSpec) -> None:
|
|
entry = _find_cnd_entry(catalog, "Point_Restraints")
|
|
assert entry is not None
|
|
assert "point" in entry.condtype.lower()
|
|
assert "node" in entry.condmeshtype.lower()
|
|
|
|
def test_zerolength_mat_fields(self, catalog: CatalogSpec) -> None:
|
|
entry = _find_cnd_entry(catalog, "Point_ZeroLength")
|
|
assert entry is not None
|
|
mat_fields = [f for f in entry.fields if f.widget_type == "MAT"]
|
|
assert len(mat_fields) >= 6 # Ux,Uy,Uz,Rx,Ry,Rz materials
|
|
|
|
def test_no_empty_field_names(self, catalog: CatalogSpec) -> None:
|
|
for book in catalog.mat_books + catalog.cnd_books:
|
|
for entry in book.entries:
|
|
for f in entry.fields:
|
|
assert f.name, f"{entry.name} has a field with empty name"
|
|
|
|
def test_dependencies_actions_are_valid(self, catalog: CatalogSpec) -> None:
|
|
valid_actions = {"RESTORE", "HIDE", "SET"}
|
|
for book in catalog.mat_books + catalog.cnd_books:
|
|
for entry in book.entries:
|
|
for fspec in entry.fields:
|
|
for rule in fspec.dependencies:
|
|
for act in rule.actions:
|
|
assert (
|
|
act.action in valid_actions
|
|
), f"{entry.name}.{fspec.name}: invalid action {act.action!r}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures (pytest)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def catalog() -> CatalogSpec:
|
|
return build_catalog(REAL_MAT, REAL_CND)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _find_entry(catalog: CatalogSpec, name: str) -> object | None:
|
|
for book in catalog.mat_books:
|
|
for entry in book.entries:
|
|
if entry.name == name:
|
|
return entry
|
|
return None
|
|
|
|
|
|
def _find_cnd_entry(catalog: CatalogSpec, name: str) -> object | None:
|
|
for book in catalog.cnd_books:
|
|
for entry in book.entries:
|
|
if entry.name == name:
|
|
return entry
|
|
return None
|