138/138 symbols across 53 modules. New: IFSelect, Interface, XSControl, IGESData, STEPControl, IGESControl, RWStl, Graphic3d, NCollection, StdPrs. **The full app suite passes against this wheel: 1797 passed, 1 skipped — the same result as the stock wheel**, run from the parity venv. Getting there needed six methods that no static analysis could have found: `inventory --check` only sees symbols reached through an import, so a method called on an instance is invisible to it. The suite found them in one pass, and one of them (gp_Vec.Reverse, which every outward-normal probe calls) accounted for 311 of the 255 failing tests on its own. The others: gp_Trsf.SetMirror over a plane and a point, Geom_Surface.D0, BRep_Builder.MakeFace from a triangulation, MakePipeShell.SetMode with a fixed binormal, and MakeFace from a surface plus tolerance. Open question **S5 is settled: no**. The wheel does not need OCCT's share/ resources. test_inc3_io.py asserts no CSF_* variable is set and then round-trips STEP and IGES, reading the declared units back off both — which is exactly the resource-less container the question was about. The XSTEP readers keep the GIL, amending the blanket "file readers and writers" line in design.md's GIL policy. STEP and IGES traffic in process-global Interface_Static state, the IGES reader is documented as not thread-safe, and the app already serialises imports behind a lock — so holding it costs nothing and removes a class of question. RWStl, which touches no global state, releases. XSControl_Reader and IGESData are registered although the app imports neither: they are the reader base both concrete readers inherit their transfer surface from, and the model-to-global-section chain the IGES unit probe walks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
235 lines
8.4 KiB
Python
235 lines
8.4 KiB
Python
"""Inc 3 + 4 gate: file I/O and the text tail.
|
|
|
|
Round-trips rather than digests: STEP and IGES payloads carry timestamps and a
|
|
generator banner, so byte comparison is meaningless — what has to survive is
|
|
the geometry, which these check by measuring the shape that comes back.
|
|
|
|
This file also settles open question **S5** from docs/design.md: whether the
|
|
wheel has to ship OCCT's share/ resources for the XSTEP layer to resolve units
|
|
when no CSF_* variable is set. These tests run in exactly that situation — a
|
|
container with the wheel and nothing else — so a pass means the answer is no.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox
|
|
from OCP.IFSelect import IFSelect_RetDone
|
|
from OCP.IGESControl import IGESControl_Controller, IGESControl_Reader, IGESControl_Writer
|
|
from OCP.Interface import Interface_Static
|
|
from OCP.STEPControl import (
|
|
STEPControl_AsIs,
|
|
STEPControl_Controller,
|
|
STEPControl_Reader,
|
|
STEPControl_Writer,
|
|
)
|
|
from OCP.TColStd import TColStd_SequenceOfAsciiString
|
|
|
|
from .test_inc1_modeling import assert_close, measure
|
|
|
|
BOX = (10.0, 20.0, 30.0)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def initialized():
|
|
"""The app's own import bootstrap, and the S5 check in one.
|
|
|
|
If the controllers could not initialise without OCCT's resource files, this
|
|
is where it would fail — there is no CSF_* in this environment.
|
|
"""
|
|
assert not any(k.startswith("CSF_") for k in os.environ), (
|
|
"a CSF_* variable is set, so this run cannot answer S5"
|
|
)
|
|
assert STEPControl_Controller.Init_s()
|
|
assert IGESControl_Controller.Init_s()
|
|
assert Interface_Static.SetCVal_s("xstep.cascade.unit", "MM")
|
|
return True
|
|
|
|
|
|
def _box():
|
|
return BRepPrimAPI_MakeBox(*BOX).Shape()
|
|
|
|
|
|
def _same_geometry(got, expected_volume: float, what: str) -> None:
|
|
m = measure(got)
|
|
assert m["n_solids"] == 1, f"{what}: solid count"
|
|
assert m["n_faces"] == 6, f"{what}: face count"
|
|
assert_close(m["volume"], expected_volume, f"{what}: volume")
|
|
|
|
|
|
def test_step_round_trip(initialized, tmp_path):
|
|
"""Write a box to STEP and read it back with the geometry intact."""
|
|
path = tmp_path / "roundtrip.step"
|
|
writer = STEPControl_Writer()
|
|
assert writer.Transfer(_box(), STEPControl_AsIs) == IFSelect_RetDone
|
|
assert writer.Write(str(path)) == IFSelect_RetDone
|
|
assert path.stat().st_size > 0
|
|
|
|
reader = STEPControl_Reader()
|
|
assert reader.ReadFile(str(path)) == IFSelect_RetDone
|
|
assert reader.NbRootsForTransfer() >= 1
|
|
assert reader.TransferRoots() >= 1
|
|
_same_geometry(reader.OneShape(), BOX[0] * BOX[1] * BOX[2], "STEP round trip")
|
|
|
|
|
|
def test_step_read_stream_takes_a_python_file_like(initialized, tmp_path):
|
|
"""ReadStream is how the app imports an uploaded payload without touching
|
|
the filesystem. It goes through the same slurp bridge BinTools uses."""
|
|
import io
|
|
|
|
path = tmp_path / "stream.step"
|
|
writer = STEPControl_Writer()
|
|
writer.Transfer(_box(), STEPControl_AsIs)
|
|
writer.Write(str(path))
|
|
|
|
reader = STEPControl_Reader()
|
|
assert (
|
|
reader.ReadStream("stream.step", io.BytesIO(path.read_bytes()))
|
|
== IFSelect_RetDone
|
|
)
|
|
assert reader.TransferRoots() >= 1
|
|
_same_geometry(reader.OneShape(), BOX[0] * BOX[1] * BOX[2], "STEP stream")
|
|
|
|
|
|
def test_step_reports_its_file_units(initialized, tmp_path):
|
|
"""FileUnits fills three sequences by reference — the out-parameter shape
|
|
the app relies on to decide whether to rescale the transfer."""
|
|
path = tmp_path / "units.step"
|
|
writer = STEPControl_Writer()
|
|
writer.Transfer(_box(), STEPControl_AsIs)
|
|
writer.Write(str(path))
|
|
|
|
reader = STEPControl_Reader()
|
|
reader.ReadFile(str(path))
|
|
|
|
length, angle, solid_angle = (
|
|
TColStd_SequenceOfAsciiString(),
|
|
TColStd_SequenceOfAsciiString(),
|
|
TColStd_SequenceOfAsciiString(),
|
|
)
|
|
reader.FileUnits(length, angle, solid_angle)
|
|
assert length.Length() >= 1, "no length unit reported — S5 would be live"
|
|
names = [length.Value(i).ToCString() for i in range(1, length.Length() + 1)]
|
|
assert any("MM" in n.upper() or "MILLI" in n.upper() for n in names), names
|
|
|
|
|
|
def test_interface_static_round_trips_a_setting(initialized):
|
|
"""The unit override the IGES import brackets its read with."""
|
|
previous = Interface_Static.CVal_s("xstep.cascade.unit")
|
|
try:
|
|
assert Interface_Static.SetCVal_s("xstep.cascade.unit", "INCH")
|
|
assert Interface_Static.CVal_s("xstep.cascade.unit") == "INCH"
|
|
finally:
|
|
Interface_Static.SetCVal_s("xstep.cascade.unit", previous or "MM")
|
|
assert Interface_Static.CVal_s("xstep.cascade.unit") == (previous or "MM")
|
|
|
|
|
|
def test_iges_round_trip_and_unit_probe(initialized, tmp_path):
|
|
"""The IGES half, including the model→global-section→unit-name chain that
|
|
forces IGESData to be registered."""
|
|
path = tmp_path / "roundtrip.igs"
|
|
writer = IGESControl_Writer("MM", 0)
|
|
assert writer.AddShape(_box())
|
|
writer.ComputeModel()
|
|
assert writer.Write(str(path))
|
|
|
|
reader = IGESControl_Reader()
|
|
assert reader.ReadFile(str(path)) == IFSelect_RetDone
|
|
assert reader.NbRootsForTransfer() >= 1
|
|
|
|
section = reader.IGESModel().GlobalSection()
|
|
unit_name = section.UnitName()
|
|
assert unit_name is not None
|
|
assert "MM" in unit_name.ToCString().upper()
|
|
assert section.UnitValue() > 0
|
|
|
|
assert reader.TransferRoots() >= 1
|
|
shape = reader.OneShape()
|
|
assert not shape.IsNull()
|
|
# IGES carries surfaces, not solids, so only the face count is meaningful.
|
|
assert measure(shape)["n_faces"] == 6
|
|
|
|
|
|
def test_stl_read_returns_a_triangulation(tmp_path):
|
|
"""RWStl hands back a bare Poly_Triangulation; the app wraps it into a
|
|
shell itself, which is why nothing here builds a shape."""
|
|
from OCP.RWStl import RWStl
|
|
|
|
path = tmp_path / "cube.stl"
|
|
# A minimal ASCII STL: one triangle is enough to prove the reader.
|
|
path.write_text(
|
|
"solid t\n"
|
|
"facet normal 0 0 1\n outer loop\n"
|
|
" vertex 0 0 0\n vertex 1 0 0\n vertex 0 1 0\n"
|
|
" endloop\nendfacet\n"
|
|
"endsolid t\n"
|
|
)
|
|
triangulation = RWStl.ReadFile_s(str(path))
|
|
assert triangulation is not None
|
|
assert triangulation.NbTriangles() == 1
|
|
assert triangulation.NbNodes() == 3
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Inc 4 — text
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def _a_font_path() -> str | None:
|
|
"""Any TrueType face on the box, since no font is committed here."""
|
|
roots = ("/usr/share/fonts", "/usr/local/share/fonts")
|
|
for root in roots:
|
|
for dirpath, _dirs, files in os.walk(root):
|
|
for name in sorted(files):
|
|
if name.lower().endswith((".ttf", ".otf")):
|
|
return os.path.join(dirpath, name)
|
|
return None
|
|
|
|
|
|
def test_text_builder_renders_glyphs_as_faces():
|
|
"""Text emboss builds its glyph outlines this way. The whole chain crosses
|
|
four modules: NCollection carries the strings, Graphic3d the alignment,
|
|
StdPrs does the work, gp positions the pen."""
|
|
from OCP.Graphic3d import Graphic3d_HTA_CENTER, Graphic3d_VTA_CENTER
|
|
from OCP.NCollection import NCollection_Utf8String
|
|
from OCP.StdPrs import StdPrs_BRepFont, StdPrs_BRepTextBuilder
|
|
from OCP.gp import gp_Ax3
|
|
from OCP.TopAbs import TopAbs_FACE
|
|
|
|
from .test_inc1_modeling import sub_shapes
|
|
|
|
path = _a_font_path()
|
|
if path is None:
|
|
pytest.skip("no TrueType font in the image to render with")
|
|
|
|
font = StdPrs_BRepFont()
|
|
assert font.Init(NCollection_Utf8String(path), 10.0, 0), path
|
|
|
|
builder = StdPrs_BRepTextBuilder()
|
|
shape = builder.Perform(
|
|
font,
|
|
NCollection_Utf8String("Ab"),
|
|
gp_Ax3(),
|
|
Graphic3d_HTA_CENTER,
|
|
Graphic3d_VTA_CENTER,
|
|
)
|
|
assert not shape.IsNull()
|
|
# Two glyphs, each at least one face; 'A' contributes a counter too.
|
|
assert len(sub_shapes(shape, TopAbs_FACE)) >= 2
|
|
|
|
|
|
def test_font_loading_reports_failure_rather_than_raising():
|
|
"""The app treats a bad font as a user error, so Init must return False."""
|
|
from OCP.NCollection import NCollection_Utf8String
|
|
from OCP.StdPrs import StdPrs_BRepFont
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".ttf") as bogus:
|
|
bogus.write(b"not a font")
|
|
bogus.flush()
|
|
font = StdPrs_BRepFont()
|
|
assert font.Init(NCollection_Utf8String(bogus.name), 10.0, 0) is False
|