boltgen: property-based geometry tests + CI (cadquery)
This commit is contained in:
1 parent
6d614ac9ae
commit
ec0540ad38
3 files changed
+186
No files matched your search
@@ -0,0 +1,52 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[dev]"
|
||||
|
||||
- name: Run specification tests (no cadquery needed)
|
||||
run: python -m pytest tests/test_specs.py -v
|
||||
|
||||
- name: Run geometry tests (requires cadquery)
|
||||
run: python -m pytest tests/test_geometry.py -v
|
||||
|
||||
build-check:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Build package
|
||||
run: |
|
||||
pip install --upgrade pip build
|
||||
python -m build
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: boltgen-dist-${{ matrix.python-version }}
|
||||
path: dist/
|
||||
@@ -0,0 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Property-based / фаззер-тесты геометрии boltgen (требуют cadquery).
|
||||
|
||||
Случайные валидные спецификации -> генерация STEP -> проверка, что модель
|
||||
невырожденная (объем > 0, все размеры в разумных пределах, файл валиден).
|
||||
Ловит вырожденные модели: нулевая длина, отверстие > внешнего диаметра и т.п.
|
||||
|
||||
Зависит от cadquery -> pytest собирает этот файл в «skip», если cadquery недоступен.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
import pytest
|
||||
|
||||
# cadquery опционален: без него весь этот файл пропускается.
|
||||
try:
|
||||
import cadquery as cq # noqa: F401
|
||||
from boltgen.model import (
|
||||
generate_bolt,
|
||||
generate_nut,
|
||||
generate_washer,
|
||||
)
|
||||
from boltgen.standard import (
|
||||
BOLT_ALLOWED_LENGTHS,
|
||||
BOLT_SIZES,
|
||||
NUT_SIZES,
|
||||
WASHER_SIZES,
|
||||
BoltSpec,
|
||||
NutSpec,
|
||||
WasherSpec,
|
||||
get_bolt,
|
||||
get_nut,
|
||||
get_washer,
|
||||
iter_bolts,
|
||||
)
|
||||
HAS_CQ = True
|
||||
except ImportError:
|
||||
HAS_CQ = False
|
||||
|
||||
pytestmark = pytest.mark.skipif(not HAS_CQ, reason="requires cadquery")
|
||||
|
||||
_RANDOM = random.Random(20261107) # детерминированный seed
|
||||
|
||||
|
||||
def _random_bolt() -> BoltSpec:
|
||||
size = _RANDOM.choice(list(BOLT_SIZES))
|
||||
lengths = (
|
||||
[l for l in BOLT_ALLOWED_LENGTHS if l <= 60]
|
||||
if size == "M6"
|
||||
else list(BOLT_ALLOWED_LENGTHS)
|
||||
)
|
||||
return get_bolt(size, _RANDOM.choice(lengths))
|
||||
|
||||
|
||||
def _read_step_volume(path: str) -> float:
|
||||
"""Объем STEP-модели через OCP.STEPReader + BRepGProp. Проверка > 0."""
|
||||
from OCP.STEPControl import STEPControl_Reader
|
||||
|
||||
reader = STEPControl_Reader()
|
||||
reader.ReadFile(path)
|
||||
ok = reader.TransferRoots()
|
||||
assert ok, f"STEP не загрузился: {path}"
|
||||
shape = reader.Shape()
|
||||
|
||||
from OCP.BRepGProp import BRepGProp
|
||||
from OCP.GProp import GProp_GProps
|
||||
|
||||
props = GProp_GProps()
|
||||
BRepGProp.VolumeProperties_s(shape, props)
|
||||
volume = props.Mass()
|
||||
assert volume > 0, f"Модель {path} имеет нулевой/отрицательный объем {volume}"
|
||||
return volume
|
||||
|
||||
|
||||
# =====Болты ======
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", [25])
|
||||
def test_random_bolts_have_positive_volume(count):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
for _ in range(count):
|
||||
spec = _random_bolt()
|
||||
out = os.path.join(d, "b.step")
|
||||
generate_bolt(spec, out)
|
||||
assert os.path.exists(out) and os.path.getsize(out) > 1000
|
||||
vol = _read_step_volume(out)
|
||||
d_mm = float(spec.size[1:])
|
||||
# тело болта: стержень d×L + головка — минимум объем стержня
|
||||
assert vol > 0.5 * d_mm * d_mm * spec.length
|
||||
|
||||
|
||||
# =====Гайки/шайбы по всем размерам ======
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", list(NUT_SIZES))
|
||||
def test_random_nuts_valid(size):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
out = os.path.join(d, "n.step")
|
||||
generate_nut(get_nut(size), out)
|
||||
assert os.path.getsize(out) > 1000
|
||||
_read_step_volume(out)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", list(WASHER_SIZES))
|
||||
def test_random_washers_valid(size):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
out = os.path.join(d, "w.step")
|
||||
generate_washer(get_washer(size), out)
|
||||
assert os.path.getsize(out) > 1000
|
||||
_read_step_volume(out)
|
||||
|
||||
|
||||
# =====Границы валидации (ни одна модель не должна порождаться некорректной) ======
|
||||
|
||||
|
||||
def test_iter_bolts_all_generate(tmp_path):
|
||||
"""Полное семейство генерируется без исключений (регресс по всей сетке)."""
|
||||
count = 0
|
||||
for spec in iter_bolts():
|
||||
out = str(tmp_path / ("b_" + spec.size + "_" + str(spec.length) + ".step"))
|
||||
generate_bolt(spec, out)
|
||||
assert os.path.getsize(out) > 1000
|
||||
count += 1
|
||||
assert count == 79
|
||||
Reference in new issue
Block a user