"""Unit tests for edge/tools/fetch_hailo_hef.py — arch detection (both signals) and the --arch / HAILO_ARCH override precedence. No hardware, no network: `run` is a subprocess stand-in, the python-API fallback and detect_arch()/fetch() are injected/monkeypatched, and HAILO_ARCH is patched per test. The override path is what lets an install proceed on a Hailo-8 board that auto-detect cannot read (the user's board reports no serial/identity), so it is the one that must not regress. """ import pytest from edge.tools import fetch_hailo_hef as fh class _Proc: def __init__(self, stdout="", returncode=0): self.stdout, self.returncode = stdout, returncode def _run(stdout="", returncode=0): return lambda cmd, **kw: _Proc(stdout, returncode) # A real serial-less identify: the architecture line is present while every identity field is # . This is the proof in-repo — a missing serial is NOT why detection would fail. SERIAL_LESS = ( "Board Name: Hailo-8\n" "Device Architecture: HAILO8\n" "Serial Number: \n" "Part Number: \n" "Product Name: \n" ) # ---- signal 1: hailortcli text -------------------------------------------------------- def test_detect_arch_reads_hailo8_even_when_serial_is_absent(): assert fh.detect_arch(run=_run(SERIAL_LESS), python_api=lambda: None) == "hailo8" def test_detect_arch_hailo8l_wins_first_match_ordering(): # 'HAILO8L' contains 'HAILO8'; a 8-before-8L alternation would mis-read an AI Kit as a HAT+. assert fh.detect_arch(run=_run("Device Architecture: HAILO8L\n"), python_api=lambda: None) == "hailo8l" def test_detect_arch_none_when_board_silent_and_no_bindings(): assert fh.detect_arch(run=_run("", returncode=1), python_api=lambda: None) is None def test_detect_arch_none_when_hailortcli_missing_and_no_bindings(): def boom(cmd, **kw): raise FileNotFoundError("hailortcli") assert fh.detect_arch(run=boom, python_api=lambda: None) is None # ---- signal 2: python-API fallback wiring --------------------------------------------- def test_fallback_used_when_hailortcli_missing(): def boom(cmd, **kw): raise FileNotFoundError("hailortcli") assert fh.detect_arch(run=boom, python_api=lambda: "hailo8") == "hailo8" def test_fallback_used_when_regex_misses_develop_firmware(): # 'HAILO8_B0' defeats the \b regex ('_' is a word char), so signal 1 -> None; the enum # fallback resolves it. Documents exactly the fragility the 2nd signal exists to cover. assert fh.detect_arch(run=_run("Device Architecture: HAILO8_B0\n"), python_api=lambda: "hailo8") == "hailo8" def test_fallback_not_consulted_when_hailortcli_answers(): def explode(): raise AssertionError("the python fallback must not run when signal 1 answered") assert fh.detect_arch(run=_run("Device Architecture: HAILO8\n"), python_api=explode) == "hailo8" # ---- pure enum-name mapping (no hailo_platform needed) -------------------------------- @pytest.mark.parametrize("name,expected", [ ("HAILO8", "hailo8"), ("HAILO8L", "hailo8l"), ("HAILO8_A0", "hailo8"), ("HAILO8_B0", "hailo8"), ("HAILO15H", None), ("HAILO10H", None), ("", None), (None, None), ]) def test_arch_from_enum_name(name, expected): assert fh._arch_from_enum_name(name) == expected # ---- main() override precedence: --arch > HAILO_ARCH > detected ----------------------- @pytest.fixture def fetched(monkeypatch): """Capture what arch main() would download, without touching the network.""" calls = [] monkeypatch.setattr(fh, "fetch", lambda arch, dest, force=False: calls.append(arch)) return calls def test_env_forces_when_detection_returns_none(monkeypatch, fetched, tmp_path): # THE FIX: serial-less board, detect_arch -> None, operator sets HAILO_ARCH=hailo8. monkeypatch.setattr(fh, "detect_arch", lambda: None) monkeypatch.setenv("HAILO_ARCH", "hailo8") fh.main(["--dest", str(tmp_path / "y.hef")]) assert fetched == ["hailo8"] def test_flag_beats_env(monkeypatch, fetched, tmp_path): monkeypatch.setattr(fh, "detect_arch", lambda: None) monkeypatch.setenv("HAILO_ARCH", "hailo8") fh.main(["--arch", "hailo8l", "--dest", str(tmp_path / "y.hef")]) assert fetched == ["hailo8l"] def test_env_normalized_case_and_whitespace(monkeypatch, fetched, tmp_path): monkeypatch.setattr(fh, "detect_arch", lambda: None) monkeypatch.setenv("HAILO_ARCH", " HAILO8 ") fh.main(["--dest", str(tmp_path / "y.hef")]) assert fetched == ["hailo8"] def test_detection_used_when_no_override(monkeypatch, fetched, tmp_path): monkeypatch.setattr(fh, "detect_arch", lambda: "hailo8l") monkeypatch.delenv("HAILO_ARCH", raising=False) fh.main(["--dest", str(tmp_path / "y.hef")]) assert fetched == ["hailo8l"] def test_bad_env_is_a_clear_error_not_a_fallback(monkeypatch, fetched, tmp_path): monkeypatch.setattr(fh, "detect_arch", lambda: "hailo8") # board present, but env is junk monkeypatch.setenv("HAILO_ARCH", "hailo9000") with pytest.raises(SystemExit) as e: fh.main(["--dest", str(tmp_path / "y.hef")]) assert "HAILO_ARCH" in str(e.value) assert fetched == [] # never downloads on a bad override def test_no_arch_anywhere_refuses_to_guess(monkeypatch, fetched, tmp_path): monkeypatch.setattr(fh, "detect_arch", lambda: None) monkeypatch.delenv("HAILO_ARCH", raising=False) with pytest.raises(SystemExit) as e: fh.main(["--dest", str(tmp_path / "y.hef")]) assert "refusing to guess" in str(e.value) assert fetched == []