- ruff profile in pyproject (E/W/F/I/B/RUF); clean src/ + tests/ - remove dead code: threaded_len/s/d/coat/first_rec_for_size, self.cat in gui - 59 public re-exports added to __all__ (sorted) - renamed l->ln (E741), zip(strict=True), split long lines, [*list, x] (RUF005) - CI: quality job (ruff), test runs tests/ -k 'not gen_all' w/ coverage - README rewritten (commands, API modules, tests/lint/coverage, CI) - docs/tasks.md: P4 recorded - 136 passed, 3 deselected (regen); ruff: all checks passed
311 lines
11 KiB
Python
311 lines
11 KiB
Python
"""Standalone-тесты fastengen — ПСЕВДОНЕЗАВИСИМЫЕ от cadquery.
|
||
|
||
Проверяют логику спецификаций (standard.py) и схему каталога/product_params,
|
||
не импортируя model.py (который тянет cadquery). Отдельно — проверка сгенерированных
|
||
STEP-файлов лежит в test_geometry.py, который падает «skip» при отсутствии cadquery.
|
||
|
||
Нужны для того, чтобы `pytest` проходился на хостах без cadquery (тесты спецификаций),
|
||
а тяжёлый экспорт модели — только когда cadquery доступен.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||
|
||
import pytest
|
||
|
||
import fastengen.standard as fastengen
|
||
from fastengen.standard import (
|
||
BOLT_ALLOWED_LENGTHS,
|
||
BOLT_SIZES,
|
||
NUT_SIZES,
|
||
WASHER_SIZES,
|
||
BoltSpec,
|
||
bolt_designation,
|
||
get_bolt,
|
||
get_bolt_high,
|
||
get_nut,
|
||
get_nut_baran,
|
||
get_nut_high,
|
||
get_nut_specing,
|
||
get_screw_potay,
|
||
get_self_tacker_gost,
|
||
get_stud_long,
|
||
get_washer,
|
||
get_washer_grever,
|
||
get_washer_stop,
|
||
iter_bolts,
|
||
iter_nuts,
|
||
iter_washers,
|
||
model_filename,
|
||
product_params,
|
||
washer_designation,
|
||
)
|
||
|
||
# =====Стандартные размерные данные ======
|
||
|
||
|
||
def test_size_tables_present_and_consistent():
|
||
for kind in (BOLT_SIZES, NUT_SIZES, WASHER_SIZES):
|
||
assert len(kind) >= 7
|
||
# диаметры совпадают между таблицами болт/гайка/шайба по размеру
|
||
for s in BOLT_SIZES:
|
||
assert BOLT_SIZES[s]["d"] == NUT_SIZES[s]["d"]
|
||
assert s in WASHER_SIZES
|
||
|
||
|
||
def test_bolt_geometry_monotonic():
|
||
# размер под ключ и высота головки растут с размером
|
||
sizes = sorted(BOLT_SIZES, key=lambda s: int(s[1:]))
|
||
prev_s = prev_k = 0.0
|
||
for s in sizes:
|
||
cur_s = BOLT_SIZES[s]["S"]
|
||
cur_k = BOLT_SIZES[s]["k"]
|
||
assert cur_s > prev_s or cur_s == prev_s
|
||
assert cur_k >= prev_k
|
||
prev_s, prev_k = cur_s, cur_k
|
||
|
||
|
||
def test_nut_washer_geometry_reasonable():
|
||
for n in NUT_SIZES.values():
|
||
# высота гайки меньше размера под ключ
|
||
assert n["k"] < n["S"]
|
||
for w in WASHER_SIZES.values():
|
||
# внутренний диаметр < внешнего, толщина положительная
|
||
assert w["d"] < w["D"]
|
||
assert w["S"] > 0
|
||
|
||
|
||
def test_m6_short_lengths():
|
||
# M6 по ГОСТ 7805-70 только до 60
|
||
lengths = [spec.length for spec in iter_bolts() if spec.size == "M6"]
|
||
assert lengths == sorted(lengths)
|
||
assert all(ln <= 60 for ln in lengths)
|
||
assert all(ln in BOLT_ALLOWED_LENGTHS for ln in lengths)
|
||
|
||
|
||
# =====Спецификации и валидация ======
|
||
|
||
|
||
def test_get_bolt_roundtrip():
|
||
spec = get_bolt("M12", 45)
|
||
assert isinstance(spec, BoltSpec)
|
||
assert spec.size == "M12"
|
||
assert spec.length == 45
|
||
|
||
|
||
def test_get_bolt_invalid_size():
|
||
with pytest.raises(ValueError):
|
||
get_bolt("M99", 40)
|
||
|
||
|
||
def test_get_bolt_invalid_length():
|
||
with pytest.raises(ValueError):
|
||
get_bolt("M10", 33) # 33 не в BOLT_ALLOWED_LENGTHS
|
||
|
||
|
||
def test_get_nut_washer_invalid():
|
||
with pytest.raises(ValueError):
|
||
get_nut("M99")
|
||
with pytest.raises(ValueError):
|
||
get_washer("M99")
|
||
|
||
|
||
# =====Итераторы (полные семейства) ======
|
||
|
||
|
||
def test_iter_counts():
|
||
bolt_count = sum(1 for _ in iter_bolts())
|
||
nut_count = sum(1 for _ in iter_nuts())
|
||
washer_count = sum(1 for _ in iter_washers())
|
||
# M6×7 + M8,M10,M12,M16,M20,M24,M27,M30 × 20 = 7 + 160 = 167 болтов + 9 гаек + 9 шайб
|
||
assert bolt_count == 167
|
||
assert nut_count == 9
|
||
assert washer_count == 9
|
||
assert bolt_count + nut_count + washer_count == 185
|
||
|
||
|
||
def test_iter_unique_bolt_ids():
|
||
ids = {(b.size, b.length) for b in iter_bolts()}
|
||
assert len(ids) == sum(1 for _ in iter_bolts()) # без дубликатov
|
||
|
||
|
||
# =====Дизигнации и model_filename ======
|
||
|
||
|
||
def test_bolt_designation_format():
|
||
# bolt_designation/nut_designation/washer_designation — базовое наименование;
|
||
# составное обозначение по ГОСТ 1759.0-87 (класс, стандарты, покрытие) собирает _*_params.
|
||
assert bolt_designation("M12", 45) == "Болт М12×45"
|
||
assert bolt_designation("M10", 20) == "Болт М10×20"
|
||
|
||
|
||
def test_washer_designation_format():
|
||
# шайба по номиналу резьбы: M12 -> 13
|
||
assert washer_designation("M12") == "Шайба 13"
|
||
|
||
|
||
def test_model_filename_large_washer_distinguished():
|
||
"""Увеличенная шайба получает отдельное имя модели (не затирает плоскую)."""
|
||
assert model_filename(get_washer("M12")) == "washer_M12.step"
|
||
assert model_filename(get_washer("M12", large=True)) == "washer_M12_large.step"
|
||
|
||
|
||
def test_catalog_ids_unique_for_large_washer():
|
||
"""id увеличенной шайбы отличается от плоской (no collision)."""
|
||
from fastengen.catalog import ProductRecord
|
||
flat = ProductRecord.from_spec(get_washer("M12"))
|
||
large = ProductRecord.from_spec(get_washer("M12", large=True))
|
||
assert flat.id != large.id
|
||
assert flat.id == "washer_M12"
|
||
assert large.id == "washer_M12_large"
|
||
|
||
|
||
def test_model_filename_types():
|
||
assert model_filename(get_bolt("M12", 45)) == "bolt_M12_45.step"
|
||
assert model_filename(get_nut("M12")) == "nut_M12.step"
|
||
assert model_filename(get_washer("M12")) == "washer_M12.step"
|
||
|
||
|
||
# =====product_params / схема каталога ======
|
||
|
||
|
||
def test_bolt_product_params():
|
||
p = product_params(get_bolt("M12", 45))
|
||
assert p["type"] == "bolt"
|
||
assert p["category"] == "Крепёж"
|
||
assert p["standard"] == "ГОСТ 7805-70"
|
||
assert p["material"] == "Сталь 8.8"
|
||
assert p["attributes"]["диаметр"] == "12"
|
||
assert p["attributes"]["длина"] == 45
|
||
assert p["attributes"]["размер_под_ключ"] == 18.0
|
||
|
||
|
||
def test_nut_product_params():
|
||
p = product_params(get_nut("M12"))
|
||
assert p["type"] == "nut"
|
||
assert p["standard"] == "ГОСТ 5915-70"
|
||
assert p["material"] == "Сталь 8"
|
||
|
||
|
||
def test_washer_product_params():
|
||
p = product_params(get_washer("M12"))
|
||
assert p["type"] == "washer"
|
||
assert p["standard"] == "ГОСТ 11371-78"
|
||
assert p["attributes"]["диаметр_наружный"] == 24.0
|
||
|
||
|
||
def test_all_bolt_params_have_required_keys():
|
||
required = {"designation", "type", "category", "standard", "material", "attributes"}
|
||
for spec in iter_bolts():
|
||
p = product_params(spec)
|
||
assert required.issubset(p.keys())
|
||
assert p["designation"].startswith("Болт ")
|
||
|
||
|
||
# =====Критические гостовские допуски ======
|
||
|
||
|
||
def test_bolt_key_size_upper_bound():
|
||
# размер под ключ не превышает диаметр + ~25 (защита от дурака в данных)
|
||
for s, b in BOLT_SIZES.items():
|
||
assert b["S"] <= float(s[1:]) + 25
|
||
|
||
|
||
def test_washer_hole_vs_outer_clearance_ratio():
|
||
# внутреннее отверстие заметно меньше внешнего (плоская шайба)
|
||
for w in WASHER_SIZES.values():
|
||
ratio = w["d"] / w["D"]
|
||
assert 0.3 < ratio < 0.75
|
||
|
||
|
||
# =====Дополнительные ГОСТ (расширение каталога) ======
|
||
|
||
|
||
def test_new_product_params_standards():
|
||
"""Каждый новый тип спецификации отдаёт свой ГОСТ в product_params."""
|
||
checks = {
|
||
get_bolt_high("M12", 45): "ГОСТ 7802-70",
|
||
get_screw_potay("M10", 40): "ГОСТ 1149-80",
|
||
get_nut_high("M12"): "ГОСТ 5916-70",
|
||
get_nut_baran("M12"): "ГОСТ 11889-76",
|
||
get_nut_specing("M12"): "ГОСТ 11394-74",
|
||
get_washer_grever("M12"): "ГОСТ 6402-70",
|
||
get_washer_stop("M12"): "ГОСТ 13463-77",
|
||
get_stud_long("M12", 80): "ГОСТ 25709-84",
|
||
get_self_tacker_gost("4.2", 30): "ГОСТ Р 51472-99",
|
||
}
|
||
for spec, expected in checks.items():
|
||
p = product_params(spec)
|
||
assert p["standard"] == expected, f"{type(spec).__name__}: {p['standard']} != {expected}"
|
||
|
||
|
||
def test_new_product_params_required_keys():
|
||
"""У новых типов есть все обязательные ключи и тип относится к Крепёжу."""
|
||
specs = [
|
||
get_bolt_high("M12", 45),
|
||
get_screw_potay("M10", 40),
|
||
get_nut_high("M12"),
|
||
get_nut_baran("M12"),
|
||
get_nut_specing("M12"),
|
||
get_washer_grever("M12"),
|
||
get_washer_stop("M12"),
|
||
get_stud_long("M12", 80),
|
||
get_self_tacker_gost("4.2", 30),
|
||
]
|
||
required = {"designation", "type", "category", "standard", "material", "attributes"}
|
||
for spec in specs:
|
||
p = product_params(spec)
|
||
assert required.issubset(p.keys())
|
||
assert p["category"] == "Крепёж"
|
||
|
||
|
||
def test_new_model_filename():
|
||
assert model_filename(get_bolt_high("M12", 45)) == "bolt_a_M12_45.step"
|
||
assert model_filename(get_screw_potay("M10", 40)) == "screw_potay_M10_40.step"
|
||
assert model_filename(get_nut_high("M12")) == "nut_high_M12.step"
|
||
assert model_filename(get_nut_baran("M12")) == "nut_baran_M12.step"
|
||
assert model_filename(get_nut_specing("M12")) == "nut_specing_M12.step"
|
||
assert model_filename(get_washer_grever("M12")) == "washer_grever_M12.step"
|
||
assert model_filename(get_washer_stop("M12")) == "washer_stop_M12.step"
|
||
assert model_filename(get_stud_long("M12", 80)) == "stud_long_M12_80.step"
|
||
assert model_filename(get_self_tacker_gost("4.2", 30)) == "selftacker_gost_4.2_30.step"
|
||
|
||
|
||
def test_new_specs_validation():
|
||
# Недействительные размеры отклоняются
|
||
with pytest.raises(ValueError):
|
||
get_nut_high("M99")
|
||
with pytest.raises(ValueError):
|
||
get_washer_grever("M99")
|
||
with pytest.raises(ValueError):
|
||
get_screw_potay("M99", 40)
|
||
with pytest.raises(ValueError):
|
||
get_nut_baran("M99")
|
||
with pytest.raises(ValueError):
|
||
get_nut_specing("M99")
|
||
with pytest.raises(ValueError):
|
||
get_washer_stop("M99")
|
||
with pytest.raises(ValueError):
|
||
get_stud_long("M99", 80)
|
||
|
||
|
||
def test_new_specs_iterators_list_return():
|
||
# Итераторы новых семейств возвращают итерируемый
|
||
for it in (
|
||
fastengen.iter_bolt_highs,
|
||
fastengen.iter_screws_potay,
|
||
fastengen.iter_nut_highs,
|
||
fastengen.iter_nut_barans,
|
||
fastengen.iter_nut_specings,
|
||
fastengen.iter_washer_grevers,
|
||
fastengen.iter_washer_stops,
|
||
fastengen.iter_studs_long,
|
||
fastengen.iter_self_tackers_gost,
|
||
):
|
||
items = list(it())
|
||
assert isinstance(items, list)
|