Technician tool v0.4
Imported from the Google Drive folder 004-technician_tool/v0.4.
This commit is contained in:
1 parent
e161941672
commit
72adfe1cec
19 files changed
+1818
-602
No files matched your search
@@ -0,0 +1,162 @@
|
||||
"""Flashing: image validation on the real official image, plan building,
|
||||
and the runner against a fake esptool (no hardware)."""
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from kota_tech.flash.image import ImageError, inspect_image, official_images
|
||||
from kota_tech.flash.plan import Mode, build_plan, check_partition_table
|
||||
from kota_tech.flash.runner import FlashRunner
|
||||
|
||||
FW = Path(__file__).resolve().parents[1] / "firmware" / "new_system.bin"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def image():
|
||||
imgs = official_images()
|
||||
assert len(imgs) == 1
|
||||
return imgs[0]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ image
|
||||
|
||||
def test_official_image_is_the_deployed_v1_build(image):
|
||||
assert image.app.project == "kota-sensorboard"
|
||||
assert image.app.elf_sha.startswith("45fa9d5b9") # same as the live board
|
||||
assert image.flash_size == "8MB"
|
||||
assert image.partition("ota_0").offset == 0x40000
|
||||
assert image.partition("otadata").offset == 0x31000
|
||||
assert image.partition("nvs").offset == 0x10000
|
||||
|
||||
|
||||
def test_app_bytes_are_a_complete_app_image(image):
|
||||
app = image.app_bytes()
|
||||
assert app[0] == 0xE9
|
||||
assert len(app) == len(image.data) - 0x40000 # ends where the file ends
|
||||
assert len(image.blank_otadata()) == 0x2000 and set(image.blank_otadata()) == {0xFF}
|
||||
|
||||
|
||||
def test_modified_image_is_refused(tmp_path):
|
||||
data = bytearray(FW.read_bytes())
|
||||
data[-1] ^= 0xFF
|
||||
bad = tmp_path / "new_system.bin"
|
||||
bad.write_bytes(data)
|
||||
manifest = tmp_path / "firmware.toml"
|
||||
manifest.write_text(f'[[image]]\nfile = "new_system.bin"\nsha256 = "{hashlib.sha256(FW.read_bytes()).hexdigest()}"\n')
|
||||
with pytest.raises(ImageError, match="SHA-256"):
|
||||
official_images(manifest)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("patch,msg", [
|
||||
((0, 0x00), "not a merged"),
|
||||
((12, 0x05), "not ESP32-S3"),
|
||||
])
|
||||
def test_wrong_images_are_rejected(tmp_path, patch, msg):
|
||||
data = bytearray(FW.read_bytes())
|
||||
data[patch[0]] = patch[1]
|
||||
p = tmp_path / "x.bin"
|
||||
p.write_bytes(data)
|
||||
with pytest.raises(ImageError, match=msg):
|
||||
inspect_image(p)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- plan
|
||||
|
||||
def test_app_only_plan_keeps_nvs(image, tmp_path):
|
||||
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
|
||||
kinds = [s.kind for s in plan.steps]
|
||||
assert kinds == ["read_pt", "write"]
|
||||
write = plan.steps[-1].args
|
||||
offsets = [a for a in write if a.startswith("0x")]
|
||||
assert offsets == ["0x31000", "0x40000"] # otadata + ota_0 only
|
||||
assert "0x10000" not in write and "0x0" not in write # NVS / bootloader untouched
|
||||
assert write[write.index("--after") + 1] == "hard_reset"
|
||||
assert (tmp_path / "app_ota0.bin").read_bytes() == image.app_bytes()
|
||||
|
||||
|
||||
def test_full_plan_with_backup(image, tmp_path):
|
||||
backup = tmp_path / "bk.bin"
|
||||
plan = build_plan(image, Mode.FULL, "COM5", tmp_path, backup_to=backup)
|
||||
assert [s.kind for s in plan.steps] == ["backup", "write"]
|
||||
assert plan.steps[0].args[-3:] == ["0", "0x800000", str(backup)]
|
||||
assert plan.steps[0].args[plan.steps[0].args.index("--after") + 1] == "no_reset"
|
||||
assert plan.steps[1].args[-2:] == ["0x0", str(image.path)]
|
||||
assert "Back up" in plan.summary()[0]
|
||||
|
||||
|
||||
def test_partition_table_check(image):
|
||||
assert check_partition_table(image.partition_table, image) is None
|
||||
assert "blank" in check_partition_table(b"\xff" * 0xC00, image)
|
||||
other = bytearray(image.partition_table)
|
||||
other[40] ^= 1
|
||||
assert "different partition layout" in check_partition_table(bytes(other), image)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- runner
|
||||
|
||||
FAKE = """
|
||||
import sys, pathlib
|
||||
args = sys.argv[1:]
|
||||
if "read_flash" in args:
|
||||
out = pathlib.Path(args[-1])
|
||||
src = pathlib.Path(r"{fw}").read_bytes()
|
||||
off, size = int(args[-3], 0), int(args[-2], 0)
|
||||
{pt_line}
|
||||
print("Read ... (50 %)", end="\\r"); print("Read ... (100 %)")
|
||||
sys.exit(0)
|
||||
if "write_flash" in args:
|
||||
for p in (10, 60, 100):
|
||||
print(f"Writing at 0x00040000... ({{p}} %)", end="\\r", flush=True)
|
||||
print(); print("Hash of data verified.")
|
||||
sys.exit({write_code})
|
||||
"""
|
||||
|
||||
|
||||
def _fake(tmp_path, write_code=0, pt="same", fail_text=""):
|
||||
pt_line = {"same": "out.write_bytes(src[off:off+size])",
|
||||
"blank": "out.write_bytes(b'\\xff' * size)"}[pt]
|
||||
script = FAKE.format(fw=FW, pt_line=pt_line, write_code=write_code)
|
||||
if fail_text:
|
||||
script = script.replace('print("Hash of data verified.")', f'print("{fail_text}")')
|
||||
f = tmp_path / "fake_esptool.py"
|
||||
f.write_text(script)
|
||||
return [sys.executable, str(f)]
|
||||
|
||||
|
||||
def _run(plan, cmd):
|
||||
steps, progress, output, done = [], [], [], threading.Event()
|
||||
result = {}
|
||||
r = FlashRunner(plan, lambda i, t: steps.append(i), progress.append, output.append,
|
||||
lambda res: (result.setdefault("r", res), done.set()), esptool_cmd=cmd)
|
||||
r.start()
|
||||
assert done.wait(30)
|
||||
return result["r"], steps, progress, output
|
||||
|
||||
|
||||
def test_runner_app_only_success(image, tmp_path):
|
||||
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
|
||||
res, steps, progress, output = _run(plan, _fake(tmp_path))
|
||||
assert res.ok, res
|
||||
assert steps == [0, 1]
|
||||
assert 60 in progress and progress[-1] == 100
|
||||
assert any("Hash of data verified" in o for o in output)
|
||||
|
||||
|
||||
def test_runner_refuses_app_only_on_blank_chip(image, tmp_path):
|
||||
plan = build_plan(image, Mode.APP_ONLY, "COM5", tmp_path)
|
||||
res, steps, *_ = _run(plan, _fake(tmp_path, pt="blank"))
|
||||
assert not res.ok and "Full mode" in res.message
|
||||
assert steps == [0] # never reached the write step
|
||||
|
||||
|
||||
def test_runner_reports_connect_failure_with_hint(image, tmp_path):
|
||||
plan = build_plan(image, Mode.FULL, "COM5", tmp_path)
|
||||
cmd = _fake(tmp_path, write_code=2,
|
||||
fail_text="A fatal error occurred: Failed to connect to ESP32-S3: No serial data received.")
|
||||
res, *_ = _run(plan, cmd)
|
||||
assert not res.ok and res.failed_step == 0
|
||||
assert "BOOT" in res.hint
|
||||
Reference in new issue
Block a user