ContentsFoundations to FrontierBezhig

eng-lab/labs/el01_hover/tests/test_requirements.py

All lab code. Download this file.

"""EL01 lab tests for YOUR design/requirements.toml (step 7).

These check that your requirements are verifiable, not whether the design meets
them: a requirement the design fails is still a good requirement (the report
shows it as "fail", which is information, not a bug).
"""
import unittest

from _impl import REPO, artifact
from hwe import requirements as reqs
from hwe.units import Q_

# The results the EL01 pipeline computes, with their units (what `result = ...` may name).
KNOWN = {
    "mass.basic": Q_(1, "g"), "mass.predicted": Q_(1, "g"), "mass.allowable": Q_(1, "g"), "mass.margin": Q_(1, ""),
    "hover.max_thrust_per_motor": Q_(1, "gf"), "hover.weight": Q_(1, "N"), "hover.thrust_per_motor": Q_(1, "gf"),
    "hover.thrust_to_weight": Q_(1, ""), "hover.hover_fraction": Q_(1, ""), "hover.max_climb_accel": Q_(1, "m/s^2"),
    "energy.battery_energy": Q_(1, "W*h"), "energy.hover_efficiency": Q_(1, "gf/W"), "energy.hover_power": Q_(1, "W"),
    "energy.flight_time": Q_(1, "min"), "energy.hover_current": Q_(1, "A"),
    "arm.volume": Q_(1, "mm^3"), "arm.volume_by_hand": Q_(1, "mm^3"), "arm.mass": Q_(1, "g"),
}

PATH = artifact("requirements.toml", REPO / "design" / "requirements.toml")


class Step7Requirements(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.records = reqs.load(PATH)

    def test_at_least_five_requirements(self):
        self.assertGreaterEqual(len(self.records), 5, f"write at least five requirements in {PATH}")

    def test_no_problems_in_the_file(self):
        problems = reqs.validate(self.records, KNOWN)
        self.assertEqual(problems, [], "\n".join(problems))

    def test_uses_analysis_test_and_inspection(self):
        methods = {r.verification for r in self.records}
        for m in ("Analysis", "Test", "Inspection"):
            self.assertIn(m, methods, f"no requirement is verified by {m}")

    def test_at_least_one_derived_requirement(self):
        ids = {r.id for r in self.records}
        derived = [r for r in self.records if r.parent]
        self.assertTrue(derived, "no requirement names a parent: derive at least one (section 2.11)")
        for r in derived:
            self.assertIn(r.parent, ids, f"{r.id}: parent {r.parent} is not in the file")

    def test_every_requirement_has_a_rationale(self):
        for r in self.records:
            self.assertTrue(r.rationale.strip(), f"{r.id}: write a rationale (why this number?)")

    def test_op_agrees_with_the_words(self):
        # "at least" is a floor (>=); "at most", "no more than", "not exceed" are ceilings (<=).
        for r in self.records:
            text = r.text.lower()
            if "at least" in text:
                self.assertEqual(r.op, ">=", f"{r.id}: 'at least' needs op '>='")
            if any(w in text for w in ("at most", "no more than", "not exceed")):
                self.assertEqual(r.op, "<=", f"{r.id}: a ceiling needs op '<='")

    def test_physical_thresholds_carry_units(self):
        for r in self.records:
            value = KNOWN.get(r.result)
            if value is not None and not value.dimensionless:
                self.assertFalse(r.threshold.dimensionless, f"{r.id}: threshold needs a unit")


if __name__ == "__main__":
    unittest.main()