velxio/test/backend/unit/test_compile_build_lock.py

111 lines
4.7 KiB
Python

"""
The compile lock must protect the BUILD DIRECTORY, not the FQBN.
`_prepare_persistent_project_dir` keys the persistent build dir on
(idf_target, variant). arduino-esp32's boards.txt gives several boards the same
variant — `esp32` and `esp32cam` are both `build.variant=esp32` — so those FQBNs
share one directory. The lock used to be keyed on the FQBN, which left them free
to build in it at the same time.
That is not a theoretical race. Measured against staging: compiling
`esp32:esp32:esp32` and `esp32:esp32:esp32cam` concurrently returned
BYTE-IDENTICAL firmware (same sha256, 285504 bytes) and the esp32cam caller got
the other sketch's binary — verified by embedding a unique marker string in each
sketch and finding the wrong one in the returned image. In the product that
surfaced as a gallery example running someone else's program: open two ESP32
examples in two tabs, and the first tab shows the second's serial log and does
nothing itself.
Run from the repo root:
python -m pytest test/backend/unit/test_compile_build_lock.py -v
"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'backend'))
from app.api.routes import compile as compile_module
class BuildIdentityTests(unittest.TestCase):
def test_boards_sharing_a_variant_share_one_lock(self):
"""esp32 and esp32cam build in the same dir, so they must serialise."""
plain = compile_module._target_lock('esp32:esp32:esp32')
cam = compile_module._target_lock('esp32:esp32:esp32cam')
self.assertIs(
plain,
cam,
'esp32 and esp32cam share one persistent build dir (both are '
'build.variant=esp32); handing them different locks lets them '
'corrupt each other and swap binaries.',
)
def test_the_same_board_still_serialises(self):
self.assertIs(
compile_module._target_lock('esp32:esp32:esp32'),
compile_module._target_lock('esp32:esp32:esp32'),
)
def test_different_chips_still_run_in_parallel(self):
"""The lock must not become a global one — that would halve throughput."""
xtensa = compile_module._target_lock('esp32:esp32:esp32')
riscv = compile_module._target_lock('esp32:esp32:esp32c3')
self.assertIsNot(
xtensa,
riscv,
'different IDF targets have separate build dirs and should still '
'compile concurrently up to the semaphore cap.',
)
def test_non_esp32_boards_never_share_a_lock(self):
"""Only the ESP-IDF lane has a shared persistent build dir.
This assertion was inverted until now: it required every unrecognised
FQBN to collapse onto the default (esp32, esp32) pair, on the reasoning
that over-serialising is the safe direction. `_build_identity` stopped
doing that deliberately — `_idf_target` defaults ANY unknown board to
'esp32', so an AVR / RP2040 / STM32 compile inherited the ESP32 key and
queued behind unrelated ESP-IDF builds it shares nothing with. There is
no correctness argument for that wait: those boards go through
arduino-cli, which has no shared build dir to corrupt.
"""
a = compile_module._build_identity('definitely:not:a:board')
b = compile_module._build_identity('also:not:a:board')
self.assertNotEqual(
a,
b,
'non-ESP32 FQBNs have no shared build dir, so each keeps its own '
'identity instead of collapsing onto the ESP32 default.',
)
self.assertIsNot(
compile_module._target_lock('arduino:avr:uno'),
compile_module._target_lock('rp2040:rp2040:rpipico'),
'an AVR build must not queue behind an ESP-IDF one.',
)
def test_a_non_esp32_board_still_serialises_with_itself(self):
"""Dropping the shared key must not drop the per-board lock too."""
self.assertIs(
compile_module._target_lock('arduino:avr:uno'),
compile_module._target_lock('arduino:avr:uno'),
)
def test_unknown_esp32_boards_still_share_the_default_pair(self):
"""The over-serialising fallback stays where it protects something.
An unrecognised `esp32:` FQBN still resolves through _idf_target to the
default pair and lands in that build dir, so it must keep sharing the
lock — serialising more than necessary costs throughput, serialising
less corrupts a build.
"""
self.assertIs(
compile_module._target_lock('esp32:esp32:definitely-not-a-board'),
compile_module._target_lock('esp32:esp32:also-not-a-board'),
)
if __name__ == '__main__':
unittest.main()