feat: named case-result load combinations with full GUI support
Snapshots the current development tree, headlined by proper load combinations (user request): a reusable LoadCombination entity of weighted completed static-case results (e.g. 1.2xDead + 1.6xLive). - core: LoadCombination/LoadCombinationItem entities, Project integration (lookup, unique ids, reference validation) - services: combinations.py (linear superposition + envelope), exported via services __init__ - commands: undoable Add/Delete/Update for combinations - GUI: Load Combinations manager dialog, Run-dialog evaluation, envelope display in Results panel, Combinations tab in Table dock - tests: unit coverage (validation, math, error paths) + integration superposition check vs a single factored run
This commit is contained in:
commit
f361fee969
560 changed files with 178701 additions and 0 deletions
53
tests/unit/test_node.py
Normal file
53
tests/unit/test_node.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Unit tests for the Node entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from otko.core import Node
|
||||
|
||||
|
||||
class TestNodeConstruction:
|
||||
def test_minimum_required_fields(self) -> None:
|
||||
n = Node(id=1, coords=(0.0, 0.0, 0.0))
|
||||
assert n.id == 1
|
||||
assert n.coords == (0.0, 0.0, 0.0)
|
||||
assert n.mass == (0.0,) * 6
|
||||
assert n.restraint == (False,) * 6
|
||||
assert n.is_free
|
||||
assert not n.is_restrained
|
||||
|
||||
def test_with_name_and_mass(self) -> None:
|
||||
n = Node(
|
||||
id=42, name="A1", coords=(1.0, 2.0, 3.0), mass=(100.0, 100.0, 100.0, 0.0, 0.0, 0.0)
|
||||
)
|
||||
assert n.name == "A1"
|
||||
assert n.mass[0] == 100.0
|
||||
|
||||
def test_pin_restraint(self) -> None:
|
||||
pin = Node(id=1, coords=(0, 0, 0), restraint=(True, True, True, False, False, False))
|
||||
assert pin.is_restrained
|
||||
assert sum(pin.restraint) == 3
|
||||
|
||||
|
||||
class TestNodeValidation:
|
||||
def test_id_must_be_positive(self) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Node(id=0, coords=(0, 0, 0))
|
||||
with pytest.raises(ValidationError):
|
||||
Node(id=-1, coords=(0, 0, 0))
|
||||
|
||||
def test_coords_must_be_three_floats(self) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Node(id=1, coords=(0.0, 0.0)) # type: ignore[arg-type]
|
||||
with pytest.raises(ValidationError):
|
||||
Node(id=1, coords=(0.0, 0.0, 0.0, 0.0)) # type: ignore[arg-type]
|
||||
|
||||
def test_mass_must_be_six_components(self) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Node(id=1, coords=(0, 0, 0), mass=(1.0, 2.0, 3.0)) # type: ignore[arg-type]
|
||||
|
||||
def test_extra_fields_rejected(self) -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Node(id=1, coords=(0, 0, 0), unknown="oops") # type: ignore[call-arg]
|
||||
Loading…
Reference in a new issue