from pathlib import Path import importlib.util import json import subprocess import sys import pytest import yaml HERE = Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location("concentric_footing_calc", HERE / "calc.py") assert spec is not None and spec.loader is not None calc_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(calc_module) compute = calc_module.compute def load_input(): with (HERE / "input.yaml").open(encoding="utf-8") as handle: return yaml.safe_load(handle) @pytest.fixture def result(): return compute(load_input()) def test_example_pressures_and_geometry(result): v = result["values"] assert v["Af_ft2"] == pytest.approx(9.0) assert v["Bf_ft"] == pytest.approx(3.0) assert v["Bf_in"] == pytest.approx(36.0) assert v["d_in"] == pytest.approx(9.0) assert v["q_psf"] == pytest.approx(2044.44, rel=0.01) assert v["qu_psf"] == pytest.approx(2911.11, rel=0.01) assert v["qa_psf"] == 2500.0 def test_example_one_way_shear(result): v = result["values"] assert v["L1_in"] == pytest.approx(2.0, abs=0.01) assert v["Vu_one_way_kip"] == pytest.approx(1.455, abs=0.05) assert v["Vc_one_way_kip"] == pytest.approx(35.45, rel=0.01) assert v["phiVc_one_way_kip"] == pytest.approx(26.59, rel=0.01) assert result["checks"]["one_way_shear"]["ok"] is True dc = v["Vu_one_way_kip"] / v["phiVc_one_way_kip"] assert dc == pytest.approx(0.055, abs=0.001) def test_example_two_way_shear(result): v = result["values"] # Reference PDF used 68 in; ACI-correct is 92 in (4*(c+d) for interior square column). # PDF's 68 in perimeter yields ~134 kip Vc; ACI-correct 92 in yields ~181.4 kip Vc (phiVn ~136 kip). assert v["bo_in"] == pytest.approx(92.0) assert v["vc_psi"] == pytest.approx(219.089, rel=1e-3) assert v["Vc_two_way_kip"] == pytest.approx(181.4, rel=0.02) assert v["phiVn_two_way_kip"] == pytest.approx(136.0, rel=0.02) assert v["Vu_two_way_kip"] == pytest.approx(15.51, abs=0.1) assert result["checks"]["two_way_shear"]["ok"] is True dc = v["Vu_two_way_kip"] / v["phiVn_two_way_kip"] assert dc == pytest.approx(0.114, abs=0.002) def test_example_flexure(result): v = result["values"] assert v["Lc_in"] == pytest.approx(11.0) assert v["Mu_kipft"] == pytest.approx(3.68, abs=0.1) assert v["a_in"] == pytest.approx(0.524, abs=0.02) assert v["As_in2"] == pytest.approx(0.785, abs=0.02) assert v["Mn_kipft"] == pytest.approx(34.27, abs=0.5) assert v["phiMn_kipft"] == pytest.approx(30.84, abs=0.5) assert v["rho"] == pytest.approx(0.00242, rel=0.02) assert result["checks"]["flexure"]["ok"] is True assert result["checks"]["minimum_steel"]["ok"] is True dc = v["Mu_kipft"] / v["phiMn_kipft"] assert dc == pytest.approx(0.12, abs=0.01) def test_example_bearing(result): v = result["values"] assert v["A1_in2"] == pytest.approx(36.0) assert v["A2_in2"] == pytest.approx(1296.0) # Raw ratio stored; Bn uses capped 2.0. assert v["sqrt_ratio"] == pytest.approx(6.0, abs=0.01) assert v["Bn_kip"] == pytest.approx(183.6, abs=0.5) assert v["phiBn_kip"] == pytest.approx(119.34, abs=0.5) assert result["checks"]["bearing"]["ok"] is True dc = v["Pu_kip"] / v["phiBn_kip"] assert dc == pytest.approx(0.22, abs=0.01) def test_all_checks_pass(result): for k in ("soil_bearing", "one_way_shear", "two_way_shear", "flexure", "minimum_steel", "bearing"): assert result["checks"][k]["ok"] is True def test_alternate_units_match_default(result): alt = load_input() alt.update({ "Bf": "36 in", "fc": "3 ksi", "fy": "60000 psi", "Ps": "18400 lbf", "Pu": "26200 lbf", }) converted = compute(alt) for key in ("Af_ft2", "q_psf", "Mu_kipft", "Vu_one_way_kip", "phiBn_kip"): assert converted["values"][key] == pytest.approx(result["values"][key], rel=1e-6) def test_wrong_dimension_is_rejected(): bad = load_input() bad["Bf"] = "3 kip" with pytest.raises(ValueError, match="Bf"): compute(bad) def test_effective_depth_validation(): bad = load_input() bad["cover"] = "13 in" bad["Df"] = "12 in" with pytest.raises(ValueError, match="d"): compute(bad) def test_zero_footing_size_rejected(): bad = load_input() bad["Bf"] = "0 ft" with pytest.raises(ValueError, match="Bf"): compute(bad) def test_typst_compiles_and_presents_python_numbers(result): subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1]) pdf = HERE / "generated" / "footing.pdf" pdf.parent.mkdir(parents=True, exist_ok=True) subprocess.run( ["typst", "compile", "--root", ".", "calcs/concentric-footing/footing.typ", str(pdf)], check=True, cwd=HERE.parents[1], ) assert pdf.exists() query = subprocess.run( ["typst", "eval", "query()", "--root", ".", "--in", "calcs/concentric-footing/footing.typ", "--format", "json"], check=True, capture_output=True, text=True, cwd=HERE.parents[1], ) published = json.loads(query.stdout) assert len(published) == 1 meta = published[0]["value"] values = result["values"] assert meta["soil_util"] == pytest.approx(values["q_psf"] / values["qa_psf"], abs=0.001) assert meta["one_way_util"] == pytest.approx(values["Vu_one_way_kip"] / values["phiVc_one_way_kip"], abs=0.001) assert meta["two_way_util"] == pytest.approx(values["Vu_two_way_kip"] / values["phiVn_two_way_kip"], abs=0.001) assert meta["flexure_util"] == pytest.approx(values["Mu_kipft"] / values["phiMn_kipft"], abs=0.001) assert meta["bearing_util"] == pytest.approx(values["Pu_kip"] / values["phiBn_kip"], abs=0.001) def test_typst_load_demands_match_checked_inputs(result): subprocess.run([sys.executable, str(HERE / "calc.py")], check=True, cwd=HERE.parents[1]) query = subprocess.run( ["typst", "eval", "query()", "--root", ".", "--in", "calcs/concentric-footing/footing.typ", "--format", "json"], check=True, capture_output=True, text=True, cwd=HERE.parents[1], ) published = json.loads(query.stdout) assert len(published) == 1 derived = published[0]["value"] values = result["values"] assert derived["Ps_kip"] == pytest.approx(values["Ps_kip"], abs=0.1) assert derived["Pu_kip"] == pytest.approx(values["Pu_kip"], abs=0.1)