102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""Tests for the cut list / board-feet / shopping estimate."""
|
||
import pytest
|
||
|
||
from woodshop.cutlist import board_feet, cut_rows, nominal_dims, shopping
|
||
from woodshop.scene import Scene
|
||
|
||
|
||
def test_nominal_dims():
|
||
assert nominal_dims("2x4") == (2.0, 4.0)
|
||
assert nominal_dims("4x4") == (4.0, 4.0)
|
||
|
||
|
||
def test_board_feet_uses_nominal():
|
||
# 2x4 at 96in = (2*4*96)/144 = 5.333 bd-ft
|
||
assert board_feet("2x4", 96) == pytest.approx(5.3333, abs=1e-3)
|
||
|
||
|
||
def test_cut_rows_groups_and_counts():
|
||
s = Scene()
|
||
s.place("2x4", 48)
|
||
s.place("2x4", 48)
|
||
s.place("2x4", 29)
|
||
rows = cut_rows(s)
|
||
by_len = {r["length_in"]: r for r in rows}
|
||
assert by_len[48.0]["count"] == 2
|
||
assert by_len[29.0]["count"] == 1
|
||
assert by_len[48.0]["board_feet"] == pytest.approx(board_feet("2x4", 48) * 2)
|
||
|
||
|
||
def test_shopping_rounds_up_with_waste():
|
||
s = Scene()
|
||
s.place("2x4", 48)
|
||
s.place("2x4", 48) # 96in total -> with 10% waste = 105.6 -> 2 sticks of 96in
|
||
assert shopping(s) == {"2x4": 2}
|
||
|
||
|
||
def test_empty_shopping():
|
||
assert shopping(Scene()) == {}
|
||
|
||
|
||
def test_end_tenon_extends_cut_length():
|
||
from woodshop.cutlist import cut_length
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
assert cut_length(s.get_part("p1")) == 24
|
||
s.add_feature("p1", "tenon", face="end_b", depth_in=1.5) # protrudes 1.5"
|
||
assert cut_length(s.get_part("p1")) == 25.5
|
||
# the cut row and board-feet reflect the longer piece
|
||
assert cut_rows(s)[0]["length_in"] == 25.5
|
||
assert cut_rows(s)[0]["board_feet"] == pytest.approx(board_feet("2x4", 25.5))
|
||
|
||
|
||
def test_plywood_uses_sqft_and_sheets():
|
||
from woodshop.cutlist import cut_rows, shopping, format_cutlist
|
||
s = Scene()
|
||
s.place("ply-3/4", 48, width_in=24) # 48 × 24 = 8 sq ft
|
||
row = cut_rows(s)[0]
|
||
assert row["plywood"] and row["sq_ft"] == pytest.approx(8.0)
|
||
assert shopping(s)["ply-3/4"] == 1 # well under a 32 sq-ft sheet
|
||
assert "sq ft" in format_cutlist(s) and "sheet" in format_cutlist(s)
|
||
|
||
|
||
def test_cut_feature_does_not_change_cut_length():
|
||
from woodshop.cutlist import cut_length
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
s.add_feature("p1", "mortise", face="top", width_in=1, height_in=1, depth_in=0.5)
|
||
assert cut_length(s.get_part("p1")) == 24 # cuts don't reduce stock you buy
|
||
|
||
|
||
def test_cli_cutlist_matches_bom_shopping_counts():
|
||
"""The CLI cut list now renders from the same CutPlan the BOM window uses,
|
||
so kerf-aware shopping counts agree."""
|
||
from collections import Counter
|
||
|
||
from woodshop.cutlist import format_cutlist
|
||
from woodshop.cutplan import build_cut_plan
|
||
s = Scene()
|
||
for _ in range(5):
|
||
s.place("2x4", 50) # 50" pieces: kerf prevents 2 per 96" stick
|
||
plan = build_cut_plan(s)
|
||
counts = Counter(sp.stock for sp in plan.stock_pieces)
|
||
assert f"{counts['2x4']} × 2x4 stick(s)" in format_cutlist(s)
|
||
|
||
|
||
def test_cli_cutlist_shows_species_and_sanding():
|
||
from woodshop.cutlist import format_cutlist
|
||
s = Scene()
|
||
s.place("1x4", 24)
|
||
s.set_material("p1", "oak")
|
||
s.set_finish("p1", "sanded")
|
||
text = format_cutlist(s)
|
||
assert "oak 1x4" in text # species surfaced
|
||
assert "sand to final" in text # sanding allowance surfaced
|
||
|
||
|
||
def test_cli_cutlist_flags_unplaced():
|
||
from woodshop.cutlist import format_cutlist
|
||
s = Scene()
|
||
s.place("2x4", 200) # longer than a stick
|
||
assert "WON'T FIT" in format_cutlist(s)
|