111 lines
4.7 KiB
Python
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()
|