Technician tool v0.7
Imported from the Google Drive folder 004-technician_tool/v0.7.
This commit is contained in:
1 parent
22bd853ecf
commit
dd915b5a74
29 files changed
+8429
-3981
No files matched your search
+104
-131
@@ -1,5 +1,6 @@
|
||||
"""Flashing: image validation on the real official image, plan building,
|
||||
and the runner against a fake esptool (no hardware)."""
|
||||
"""Flashing: image validation on the real image in firmware/, the folder
|
||||
catalogue (compile-date order), plan building, and the runner against a fake
|
||||
esptool (no hardware)."""
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
@@ -8,31 +9,39 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from kota_tech.flash.image import ImageError, inspect_image, official_images
|
||||
from kota_tech.flash.image import ImageError, app_image_length, folder_images, inspect_image
|
||||
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"
|
||||
FW_DIR = Path(__file__).resolve().parents[1] / "firmware"
|
||||
FW = sorted(FW_DIR.glob("*.bin"))[0]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def image():
|
||||
imgs = official_images()
|
||||
assert len(imgs) == 1
|
||||
imgs, problems = folder_images()
|
||||
assert imgs and problems == []
|
||||
return imgs[0]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ image
|
||||
|
||||
def test_official_image_is_the_deployed_v1_build(image):
|
||||
def test_folder_image_is_a_kota_build(image):
|
||||
assert image.app.project == "kota-sensorboard"
|
||||
assert image.app.elf_sha.startswith("45fa9d5b9") # same as the live board
|
||||
assert image.kind == "merged"
|
||||
assert image.app.built_at is not None # compile date is readable
|
||||
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_label_starts_with_compile_date(image):
|
||||
label = image.label()
|
||||
assert label.startswith(f"Compiled {image.app.built_at:%Y-%m-%d %H:%M}")
|
||||
assert image.app.version in label and image.name in label
|
||||
|
||||
|
||||
def test_app_bytes_are_a_complete_app_image(image):
|
||||
app = image.app_bytes()
|
||||
assert app[0] == 0xE9
|
||||
@@ -40,17 +49,6 @@ def test_app_bytes_are_a_complete_app_image(image):
|
||||
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 an ESP32 firmware"),
|
||||
((12, 0x05), "not ESP32-S3"),
|
||||
@@ -64,6 +62,93 @@ def test_wrong_images_are_rejected(tmp_path, patch, msg):
|
||||
inspect_image(p)
|
||||
|
||||
|
||||
def test_integrity_check_on_folder_image(image):
|
||||
d = image.data
|
||||
assert app_image_length(d, 0) > 0 # bootloader
|
||||
assert app_image_length(d, 0x40000) == len(d) - 0x40000 # app ends exactly at file end
|
||||
|
||||
|
||||
@pytest.mark.parametrize("damage,msg", [
|
||||
(lambda b: b[:len(b) // 2], "truncated"),
|
||||
(lambda b: b[:0x40000 + 5000] + bytes([b[0x40000 + 5000] ^ 1]) + b[0x40000 + 5001:], "corrupted"),
|
||||
])
|
||||
def test_corrupted_or_truncated_images_refused(tmp_path, damage, msg):
|
||||
p = tmp_path / "broken.bin"
|
||||
p.write_bytes(damage(FW.read_bytes()))
|
||||
with pytest.raises(ImageError, match=msg):
|
||||
inspect_image(p)
|
||||
|
||||
|
||||
# ------------------------------------------------------- firmware folder
|
||||
|
||||
def _with_compile_date(data: bytes, date: str, time: str, off: int = 0x40000) -> bytes:
|
||||
"""Copy of a merged image whose app claims another compile date, with the
|
||||
segment checksum and appended SHA-256 recomputed (a valid, different build)."""
|
||||
d = bytearray(data)
|
||||
desc = off + 0x20
|
||||
old = bytes(d[desc + 80:desc + 112])
|
||||
new = time.encode().ljust(16, b"\0") + date.encode().ljust(16, b"\0")
|
||||
d[desc + 80:desc + 112] = new
|
||||
end = off + app_image_length(data, off) - 32 # SHA-256 follows the checksum byte
|
||||
xor = 0
|
||||
for a, b in zip(old, new):
|
||||
xor ^= a ^ b
|
||||
d[end - 1] ^= xor
|
||||
d[end:end + 32] = hashlib.sha256(bytes(d[off:end])).digest()
|
||||
return bytes(d)
|
||||
|
||||
|
||||
def test_folder_lists_builds_newest_compile_date_first(tmp_path):
|
||||
data = FW.read_bytes()
|
||||
(tmp_path / "a_old.bin").write_bytes(_with_compile_date(data, "Jan 5 2026", "08:00:00"))
|
||||
(tmp_path / "z_new.bin").write_bytes(_with_compile_date(data, "Dec 24 2026", "18:30:00"))
|
||||
(tmp_path / "m_mid.bin").write_bytes(_with_compile_date(data, "Jun 15 2026", "12:00:00"))
|
||||
imgs, problems = folder_images(tmp_path)
|
||||
assert problems == []
|
||||
assert [i.name for i in imgs] == ["z_new.bin", "m_mid.bin", "a_old.bin"]
|
||||
assert imgs[0].label().startswith("Compiled 2026-12-24 18:30")
|
||||
assert imgs[2].label().startswith("Compiled 2026-01-05 08:00")
|
||||
|
||||
|
||||
def test_folder_reports_bad_files_and_duplicates(tmp_path):
|
||||
data = FW.read_bytes()
|
||||
(tmp_path / "good.bin").write_bytes(data)
|
||||
(tmp_path / "copy.bin").write_bytes(data)
|
||||
(tmp_path / "notes.bin").write_bytes(b"hello firmware team")
|
||||
(tmp_path / "readme.txt").write_text("ignored: not a .bin")
|
||||
imgs, problems = folder_images(tmp_path)
|
||||
assert len(imgs) == 1
|
||||
assert any("notes.bin" in p and "not an ESP32 firmware" in p for p in problems)
|
||||
assert any("same file as" in p for p in problems)
|
||||
assert not any("readme" in p for p in problems)
|
||||
|
||||
|
||||
def test_app_only_file_uses_the_merged_layout(tmp_path, image):
|
||||
(tmp_path / "merged.bin").write_bytes(image.data)
|
||||
(tmp_path / "kota-sensorboard.bin").write_bytes(
|
||||
_with_compile_date(image.data, "Dec 1 2026", "10:00:00")[0x40000:])
|
||||
imgs, problems = folder_images(tmp_path)
|
||||
assert problems == []
|
||||
app = next(i for i in imgs if i.kind == "app")
|
||||
assert imgs[0] is app # newer compile date first
|
||||
assert app.partition("ota_0").offset == 0x40000 and "app only" in app.label()
|
||||
assert [s.kind for s in build_plan(app, Mode.APP_ONLY, "COM5", tmp_path / "w").steps] == ["read_pt", "write"]
|
||||
with pytest.raises(ValueError, match="app only"):
|
||||
build_plan(app, Mode.FULL, "COM5", tmp_path / "w")
|
||||
|
||||
|
||||
def test_app_only_file_without_merged_image_is_not_offered(tmp_path, image):
|
||||
(tmp_path / "kota-sensorboard.bin").write_bytes(image.app_bytes())
|
||||
imgs, problems = folder_images(tmp_path)
|
||||
assert imgs == [] and "no complete (merged) image" in problems[0]
|
||||
|
||||
|
||||
def test_empty_or_missing_folder(tmp_path):
|
||||
assert folder_images(tmp_path) == ([], [])
|
||||
imgs, problems = folder_images(tmp_path / "nope")
|
||||
assert imgs == [] and "not found" in problems[0]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- plan
|
||||
|
||||
def test_app_only_plan_keeps_nvs(image, tmp_path):
|
||||
@@ -160,115 +245,3 @@ def test_runner_reports_connect_failure_with_hint(image, tmp_path):
|
||||
res, *_ = _run(plan, cmd)
|
||||
assert not res.ok and res.failed_step == 0
|
||||
assert "BOOT" in res.hint
|
||||
|
||||
|
||||
# ------------------------------------------------- developer builds (v0.5)
|
||||
|
||||
from kota_tech.flash.image import FirmwareLibrary, app_image_length # noqa: E402
|
||||
|
||||
|
||||
def _app_only_file(image, tmp_path, name="kota-sensorboard.bin"):
|
||||
p = tmp_path / name
|
||||
p.write_bytes(image.app_bytes()) # what a developer build/ folder contains
|
||||
return p
|
||||
|
||||
|
||||
def test_integrity_check_on_factory_image(image):
|
||||
d = image.data
|
||||
assert app_image_length(d, 0) > 0 # bootloader
|
||||
assert app_image_length(d, 0x40000) == len(d) - 0x40000 # app ends exactly at file end
|
||||
|
||||
|
||||
@pytest.mark.parametrize("damage,msg", [
|
||||
(lambda b: b[:len(b) // 2], "truncated"),
|
||||
(lambda b: b[:0x40000 + 5000] + bytes([b[0x40000 + 5000] ^ 1]) + b[0x40000 + 5001:], "corrupted"),
|
||||
])
|
||||
def test_corrupted_or_truncated_images_refused(tmp_path, damage, msg):
|
||||
p = tmp_path / "broken.bin"
|
||||
p.write_bytes(damage(FW.read_bytes()))
|
||||
with pytest.raises(ImageError, match=msg):
|
||||
inspect_image(p, origin="developer")
|
||||
|
||||
|
||||
def test_import_app_only_developer_build(image, tmp_path):
|
||||
lib = FirmwareLibrary(tmp_path / "lib")
|
||||
src = _app_only_file(image, tmp_path)
|
||||
img = lib.import_file(src, reference=image)
|
||||
assert img.is_developer and img.kind == "app"
|
||||
assert img.partition("ota_0").offset == 0x40000 # layout from the factory image
|
||||
assert img.app_bytes() == image.app_bytes()
|
||||
listed, problems = lib.images(image)
|
||||
assert [i.sha256 for i in listed] == [img.sha256] and problems == []
|
||||
assert lib.import_file(src, reference=image).sha256 == img.sha256 # re-import: no duplicate
|
||||
assert len(lib.images(image)[0]) == 1
|
||||
|
||||
|
||||
def test_app_only_build_cannot_be_flashed_full(image, tmp_path):
|
||||
lib = FirmwareLibrary(tmp_path / "lib")
|
||||
img = lib.import_file(_app_only_file(image, tmp_path), reference=image)
|
||||
plan = build_plan(img, Mode.APP_ONLY, "COM5", tmp_path / "w")
|
||||
assert [s.kind for s in plan.steps] == ["read_pt", "write"]
|
||||
with pytest.raises(ValueError, match="app only"):
|
||||
build_plan(img, Mode.FULL, "COM5", tmp_path / "w")
|
||||
|
||||
|
||||
def test_import_merged_developer_build(image, tmp_path):
|
||||
lib = FirmwareLibrary(tmp_path / "lib")
|
||||
src = tmp_path / "dev_full.bin"
|
||||
src.write_bytes(image.data)
|
||||
img = lib.import_file(src, reference=image)
|
||||
assert img.kind == "merged" and img.is_developer
|
||||
assert [s.kind for s in build_plan(img, Mode.FULL, "COM5", tmp_path / "w").steps] == ["write"]
|
||||
|
||||
|
||||
def test_library_detects_file_changed_after_import(image, tmp_path):
|
||||
lib = FirmwareLibrary(tmp_path / "lib")
|
||||
img = lib.import_file(_app_only_file(image, tmp_path), reference=image)
|
||||
stored = tmp_path / "lib" / img.path.name
|
||||
data = bytearray(stored.read_bytes())
|
||||
data[-40] ^= 1
|
||||
stored.write_bytes(data)
|
||||
listed, problems = lib.images(image)
|
||||
assert listed == [] and problems
|
||||
|
||||
|
||||
def test_library_remove(image, tmp_path):
|
||||
lib = FirmwareLibrary(tmp_path / "lib")
|
||||
img = lib.import_file(_app_only_file(image, tmp_path), reference=image)
|
||||
lib.remove(img.sha256)
|
||||
assert lib.images(image) == ([], []) and not img.path.exists()
|
||||
|
||||
|
||||
def test_non_firmware_file_refused(tmp_path, image):
|
||||
p = tmp_path / "notes.bin"
|
||||
p.write_bytes(b"hello firmware team")
|
||||
with pytest.raises(ImageError, match="not an ESP32 firmware"):
|
||||
FirmwareLibrary(tmp_path / "lib").import_file(p, reference=image)
|
||||
|
||||
|
||||
# ------------------------------------------- bundled developer build (v0.6)
|
||||
|
||||
from kota_tech.flash.image import bundled_images # noqa: E402
|
||||
|
||||
|
||||
def test_bundled_developer_build_is_labelled_and_not_default():
|
||||
imgs = bundled_images()
|
||||
assert [i.origin for i in imgs] == ["factory", "developer"] # factory first
|
||||
dev = imgs[1]
|
||||
assert dev.built_by == "Krzysztof Kallas" and "built by Krzysztof Kallas" in dev.label()
|
||||
assert dev.kind == "merged" and dev.app.elf_sha.startswith("c94ba0e9b")
|
||||
assert dev.partition_table == imgs[0].partition_table # App-only safe on v1.0 boxes
|
||||
assert [i.origin for i in factory_images_only()] == ["factory"]
|
||||
|
||||
|
||||
def factory_images_only():
|
||||
from kota_tech.flash.image import factory_images
|
||||
return factory_images()
|
||||
|
||||
|
||||
def test_import_records_who_built_it(image, tmp_path):
|
||||
lib = FirmwareLibrary(tmp_path / "lib")
|
||||
img = lib.import_file(_app_only_file(image, tmp_path), reference=image, built_by=" Anna Nowak ")
|
||||
assert img.built_by == "Anna Nowak" and "built by Anna Nowak" in img.label()
|
||||
again, _ = lib.images(image)
|
||||
assert again[0].built_by == "Anna Nowak"
|
||||
Reference in new issue
Block a user