81 lines
3.3 KiB
Python
81 lines
3.3 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_unknown_boards_share_the_default_pair(self):
|
|
"""An unrecognised FQBN resolves to the default (esp32, esp32) pair.
|
|
|
|
That is not a hole: those compiles genuinely land in the same build dir,
|
|
because the compiler derives the dir from the same two functions. Sharing
|
|
a lock here is the safe direction — serialising more than necessary only
|
|
costs throughput, while serialising less corrupts a build.
|
|
"""
|
|
a = compile_module._build_identity('definitely:not:a:board')
|
|
b = compile_module._build_identity('also:not:a:board')
|
|
self.assertEqual(a, b)
|
|
self.assertIs(
|
|
compile_module._target_lock('definitely:not:a:board'),
|
|
compile_module._target_lock('also:not:a:board'),
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|