Update MOTA profile and compact logging checks and run them in CI

This commit is contained in:
mikecarper committed 2026-10-23 17:08:36 -07:00
1 parent 9da754b137
commit d0d32d7a31
2 files changed
+82 -24

No files matched your search

+4 -1
View File
@@ -264,10 +264,13 @@ jobs:
- name: Install host identity test dependencies
run: |
python3 -m pip install cryptography
python3 -m pip install cryptography detools
sudo apt-get update
sudo apt-get install -y libssl-dev
- name: Verify MOTA containers and build profile contracts
run: python3 -B tools/mota/test_mota.py
- name: Verify embedded bootloader variant and guarded activation
run: |
python3 -B test/test_nrf52_embedded_variant.py -v
+78 -23
View File
@@ -247,15 +247,21 @@ def test_flash_constrained_stm32_repeaters_pin_the_size_qualified_toolchain():
repeater = (
root / "examples/simple_repeater/MyMesh.cpp"
).read_text(encoding="utf-8")
assert "#if MESH_PACKET_LOGGING_COMPACT" in dispatcher
assert 'line.printf("T");' in dispatcher
assert "line.hex(raw, len);" in dispatcher
assert "line.flush(usbLoggingPort(), false);" in dispatcher
# RX/TX share one bounded formatter to keep these flash-constrained builds
# small. The production-method tests in test_mini_repeater_logging.py also
# compile both callers and verify exact bytes, LF endings and stall recovery.
for source, call in (
(dispatcher, "serialLogCompactPacket(usbLoggingPort(), 'T', raw, len);"),
(repeater, "mesh::serialLogCompactPacket(mesh::usbLoggingPort(), 'R', raw, len);"),
):
compact = source.split("#if MESH_PACKET_LOGGING_COMPACT", 1)[1].split("#else", 1)[0]
assert call in compact
assert "!MESH_PACKET_LOGGING_COMPACT" in dispatcher
assert "#if MESH_PACKET_LOGGING_COMPACT" in repeater
assert 'line.printf("R");' in repeater
assert "line.hex(raw, len);" in repeater
assert "line.flush(mesh::usbLoggingPort(), false);" in repeater
logger = (root / "src/helpers/SerialPacketLog.h").read_text(encoding="utf-8")
formatter = logger.split("inline bool serialLogCompactPacket(", 1)[1].split("\n}", 1)[0]
assert "SerialLogLine<> line(direction);" in formatter
assert "line.hex(raw, len);" in formatter
assert "return line.flush(out, false);" in formatter
def test_flash_constrained_stm32_companions_pin_the_size_qualified_toolchain():
@@ -603,6 +609,10 @@ def test_full_companion_wireless_startup_and_psram_contacts_are_resilient():
def test_esp32_s3_full_profiles_use_arduino2_and_dio_boot_mode():
import configparser
import re
from unittest import TestCase
root = Path(__file__).resolve().parents[2]
project = (root / "platformio.ini").read_text(encoding="utf-8")
esp32_base = project.split("[esp32_base]", 1)[1].split("\n[", 1)[0]
@@ -656,25 +666,70 @@ def test_esp32_s3_full_profiles_use_arduino2_and_dio_boot_mode():
"station_g3_esp32",
"xiao_s3_wio",
}
profile_count = 0
config = configparser.ConfigParser(
interpolation=None, inline_comment_prefixes=(";", "#")
)
config.read(root / "platformio.ini", encoding="utf-8")
def option(section, key, seen=()):
pair = (section, key)
assert pair not in seen, f"cyclic recipe reference: {pair}"
seen = (*seen, pair)
if config.has_option(section, key):
return re.sub(
r"\$\{([^}]+)\}",
lambda match: option(*match[1].split(".", 1), seen),
config.get(section, key),
)
parents = config.get(section, "extends", fallback="").replace(",", " ").split()
# PlatformIO checks the last listed parent first.
for parent in reversed(parents):
value = option(parent, key, seen)
if value is not None:
return value
return None
profiles = []
for variant_dir in sorted(s3_full_variant_dirs):
path = root / "variants" / variant_dir / "platformio.ini"
content = path.read_text(encoding="utf-8")
for section in content.split("\n["):
header = section.split("]", 1)[0].lower()
if "companion_radio_full" not in header:
variant = configparser.ConfigParser(interpolation=None)
variant.read(path, encoding="utf-8")
config.read(path, encoding="utf-8")
for section in variant.sections():
if not section.startswith("env:") or "companion_radio_full" not in section.lower():
continue
profile_count += 1
assert (
"board_build.flash_mode = "
"${esp32_s3_full.board_build.flash_mode}"
) in section, f"ESP32-S3 Full profile lacks shared DIO mode: {path}"
assert "esp32_s3_dual_cdc_full" not in section
if variant_dir != "heltec_rc32":
assert "55.03.311" not in section
assert "esp32-core-3.3.11" not in section
profiles.append((variant_dir, section))
assert profile_count == 18
def check_profile(variant_dir, section):
# Full aliases inherit the same boot policy as their canonical image.
# Check effective settings so local overrides and parent changes count.
assert option(section, "board_build.flash_mode") == "dio", section
if variant_dir == "heltec_rc32":
assert "55.03.311" in option(section, "platform"), section
assert "esp32-core-3.3.11" in option(section, "platform_packages"), section
else:
assert option(section, "platform") == "platformio/espressif32@6.11.0", section
assert option(section, "platform_packages") == option("esp32_base", "platform_packages"), section
assert len(profiles) == 18
for variant_dir, section in profiles:
check_profile(variant_dir, section)
# An alias must not hide a conflicting override behind its DIO parent.
alias = "env:heltec_v4_expansionkit_tft_companion_radio_full_femon"
config.set(alias, "board_build.flash_mode", "qio")
with TestCase().assertRaises(AssertionError):
check_profile("heltec_v4", alias)
config.set(alias, "board_build.flash_mode", "dio")
check_profile("heltec_v4", alias)
# A conflicting later parent must also fail, even when the first uses DIO.
config.remove_option(alias, "board_build.flash_mode")
config.add_section("mota_qio_control")
config.set("mota_qio_control", "board_build.flash_mode", "qio")
config.set(alias, "extends", config.get(alias, "extends") + ", mota_qio_control")
with TestCase().assertRaises(AssertionError):
check_profile("heltec_v4", alias)
def test_tbeam_1w_release_hardware_fixes_are_preserved():