velxio/test/backend/unit/test_compile_build_lock.py

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()