226 lines
7.7 KiB
Python
226 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import contextlib
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
import runpy
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPT = ROOT / "scripts" / "esp32_full_partition.py"
|
|
INDICATOR_PROFILE = (
|
|
ROOT / "variants" / "sensecap_indicator-espnow" / "platformio.ini"
|
|
)
|
|
INDICATOR_PARTITIONS = (
|
|
"variants/sensecap_indicator-espnow/"
|
|
"dual_ota_2560k_preserve_spiffs.csv"
|
|
)
|
|
INDICATOR_N16R2_PARTITIONS = (
|
|
"variants/sensecap_indicator-espnow/"
|
|
"dual_ota_6400k_preserve_spiffs.csv"
|
|
)
|
|
|
|
|
|
class FakeBoardConfig:
|
|
def __init__(self, values):
|
|
self.values = dict(values)
|
|
self.updates = []
|
|
|
|
def get(self, key, default=None):
|
|
return self.values.get(key, default)
|
|
|
|
def update(self, key, value):
|
|
self.values[key] = value
|
|
self.updates.append((key, value))
|
|
|
|
|
|
class FakeEnvironment:
|
|
def __init__(self, board):
|
|
self.board = board
|
|
|
|
def BoardConfig(self):
|
|
return self.board
|
|
|
|
|
|
def apply_policy(
|
|
values, *, full=True, companion=True, required_partitions=""
|
|
):
|
|
board = FakeBoardConfig(values)
|
|
environment = {
|
|
"MESHCORE_ESP32_FULL_BUILD": "1" if full else "0",
|
|
"MESHCORE_COMPANION_RADIO_FULL": "1" if companion else "0",
|
|
"MESHCORE_ESP32_FULL_PARTITION_TABLE": required_partitions,
|
|
}
|
|
output = io.StringIO()
|
|
with mock.patch.dict(os.environ, environment, clear=False):
|
|
with contextlib.redirect_stdout(output):
|
|
runpy.run_path(
|
|
str(SCRIPT),
|
|
init_globals={
|
|
"Import": lambda _name: None,
|
|
"env": FakeEnvironment(board),
|
|
},
|
|
)
|
|
return board, output.getvalue()
|
|
|
|
|
|
class Esp32FullPartitionTest(unittest.TestCase):
|
|
def test_indicator_full_retains_preserve_spiffs_layout(self):
|
|
profile = INDICATOR_PROFILE.read_text()
|
|
self.assertIn(
|
|
f"board_build.partitions = {INDICATOR_PARTITIONS}", profile
|
|
)
|
|
|
|
table = (ROOT / INDICATOR_PARTITIONS).read_text()
|
|
self.assertIn("app0, app, ota_0, 0x10000, 0x280000", table)
|
|
self.assertIn("spiffs, data, spiffs, 0x290000, 0x160000", table)
|
|
self.assertIn("app1, app, ota_1, 0x400000, 0x280000", table)
|
|
|
|
board, output = apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.variant": "esp32s3",
|
|
"build.flash_mode": "qio",
|
|
"build.partitions": INDICATOR_PARTITIONS,
|
|
}
|
|
)
|
|
|
|
self.assertEqual(board.values["build.partitions"], INDICATOR_PARTITIONS)
|
|
self.assertIn(("build.partitions", INDICATOR_PARTITIONS), board.updates)
|
|
self.assertEqual(board.values["build.flash_mode"], "dio")
|
|
self.assertIn(INDICATOR_PARTITIONS, output)
|
|
|
|
def test_n16r2_retains_factory_six_megabyte_ota_slots(self):
|
|
profile = INDICATOR_PROFILE.read_text()
|
|
self.assertIn(
|
|
f"board_build.partitions = {INDICATOR_N16R2_PARTITIONS}", profile
|
|
)
|
|
table = (ROOT / INDICATOR_N16R2_PARTITIONS).read_text()
|
|
self.assertIn("app0, app, ota_0, 0x10000, 0x640000", table)
|
|
self.assertIn("app1, app, ota_1, 0x650000, 0x640000", table)
|
|
self.assertIn("spiffs, data, spiffs, 0xc90000, 0x360000", table)
|
|
board, output = apply_policy(
|
|
{
|
|
"upload.flash_size": "16MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.partitions": INDICATOR_N16R2_PARTITIONS,
|
|
}
|
|
)
|
|
self.assertEqual(
|
|
board.values["build.partitions"], INDICATOR_N16R2_PARTITIONS
|
|
)
|
|
self.assertIn(INDICATOR_N16R2_PARTITIONS, output)
|
|
|
|
def test_absolute_indicator_partition_path_is_also_preserved(self):
|
|
absolute = str(ROOT / INDICATOR_PARTITIONS)
|
|
board, _output = apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.partitions": absolute,
|
|
}
|
|
)
|
|
self.assertEqual(board.values["build.partitions"], INDICATOR_PARTITIONS)
|
|
|
|
def test_indicator_target_override_preserves_espnow_base_data_layout(self):
|
|
board, _output = apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.partitions": "default.csv",
|
|
},
|
|
required_partitions=INDICATOR_PARTITIONS,
|
|
)
|
|
self.assertEqual(board.values["build.partitions"], INDICATOR_PARTITIONS)
|
|
|
|
def test_unknown_required_partition_table_fails_closed(self):
|
|
with self.assertRaisesRegex(
|
|
ValueError, "unsupported required Full partition table"
|
|
):
|
|
apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.partitions": "default.csv",
|
|
},
|
|
required_partitions="variants/not-a-real-layout.csv",
|
|
)
|
|
|
|
def test_generic_eight_megabyte_full_still_expands(self):
|
|
board, _output = apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.partitions": "default.csv",
|
|
}
|
|
)
|
|
self.assertEqual(board.values["build.partitions"], "default_8MB.csv")
|
|
|
|
def test_capacity_only_custom_table_does_not_bypass_full_policy(self):
|
|
board, _output = apply_policy(
|
|
{
|
|
"upload.flash_size": "4MB",
|
|
"build.mcu": "esp32",
|
|
"build.partitions": "variants/dual_ota_1536k.csv",
|
|
}
|
|
)
|
|
self.assertEqual(board.values["build.partitions"], "huge_app.csv")
|
|
|
|
def test_explicit_small_full_trial_uses_dual_ota_with_large_spiffs(self):
|
|
table = "variants/dual_ota_1536k.csv"
|
|
board, _output = apply_policy(
|
|
{
|
|
"upload.flash_size": "4MB",
|
|
"build.mcu": "esp32c3",
|
|
"build.partitions": "huge_app.csv",
|
|
"build.full_partitions": table,
|
|
}
|
|
)
|
|
self.assertEqual(board.values["build.partitions"], table)
|
|
# Retain the huge_app SPIFFS/coredump placement and enough storage for
|
|
# two complete 350-contact snapshots, rather than the 64 KiB table.
|
|
partitions = (ROOT / table).read_text()
|
|
self.assertIn("spiffs, data, spiffs, 0x310000,0xE0000", partitions)
|
|
self.assertIn("app1, app, ota_1, 0x190000,0x180000", partitions)
|
|
|
|
def test_explicit_small_full_trial_rejects_insufficient_flash(self):
|
|
with self.assertRaisesRegex(ValueError, "at least 4 MiB"):
|
|
apply_policy(
|
|
{
|
|
"upload.flash_size": "2MB",
|
|
"build.full_partitions": "variants/dual_ota_1536k.csv",
|
|
}
|
|
)
|
|
|
|
def test_tbeam_1w_factory_layout_keeps_precedence(self):
|
|
board, _output = apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.variant": "lilygo_tbeam_1w",
|
|
"build.partitions": INDICATOR_PARTITIONS,
|
|
}
|
|
)
|
|
self.assertEqual(board.values["build.partitions"], "huge_app.csv")
|
|
|
|
def test_non_full_build_is_untouched(self):
|
|
board, output = apply_policy(
|
|
{
|
|
"upload.flash_size": "8MB",
|
|
"build.mcu": "esp32s3",
|
|
"build.flash_mode": "qio",
|
|
"build.partitions": INDICATOR_PARTITIONS,
|
|
},
|
|
full=False,
|
|
)
|
|
self.assertEqual(board.updates, [])
|
|
self.assertEqual(output, "")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|