From 382e13cfe75beb08d3e3cd3d2437508d57b7bd43 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sat, 11 Apr 2026 15:42:38 -0300 Subject: [PATCH] feat: Update components metadata timestamp and mark subproject commits as dirty; add diagnostic test for real library paths --- frontend/public/components-metadata.json | 2 +- test/backend/unit/test_espidf_real_paths.py | 233 ++++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 test/backend/unit/test_espidf_real_paths.py diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 24f9507d..c3ce4d3d 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "generatedAt": "2026-04-11T00:28:26.563Z", + "generatedAt": "2026-04-11T18:35:07.056Z", "components": [ { "id": "arduino-mega", diff --git a/test/backend/unit/test_espidf_real_paths.py b/test/backend/unit/test_espidf_real_paths.py new file mode 100644 index 00000000..8c6f24c1 --- /dev/null +++ b/test/backend/unit/test_espidf_real_paths.py @@ -0,0 +1,233 @@ +""" +Diagnostic test: exercises _resolve_library_components with the REAL library +paths on this machine (C:/Users/David/Documents/Arduino/libraries and +C:/Espressif/components/arduino-esp32/libraries). + +This reproduces the exact SSD1306 compilation failure and shows exactly what +user_libs/ structure the current code produces. + +Run: + python test/backend/unit/test_espidf_real_paths.py +or: + python -m pytest test/backend/unit/test_espidf_real_paths.py -v -s +""" + +import importlib +import sys +import tempfile +import shutil +import unittest +from pathlib import Path + +# Force fresh import — bypass any .pyc cache +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'backend')) + +# Delete cached module if already imported so we get the live .py file +for key in list(sys.modules.keys()): + if 'espidf_compiler' in key: + del sys.modules[key] + +from app.services.espidf_compiler import ESPIDFCompiler + +# ── Real paths on this machine ──────────────────────────────────────────────── +ARDUINO_LIBS = Path.home() / 'Documents' / 'Arduino' / 'libraries' +ESP32_LIBS = Path('C:/Espressif/components/arduino-esp32/libraries') + +SSD1306_SKETCH_HEADERS = ['Wire.h', 'Adafruit_GFX.h', 'Adafruit_SSD1306.h'] + +# ── SSD1306 sketch (same as the example) ───────────────────────────────────── +SSD1306_SKETCH = """\ +// ESP32 — SSD1306 OLED Display (I2C 128×64) +#include +#include +#include + +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 + +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); + +void setup() { + Serial.begin(115200); + Wire.begin(21, 22); + display.begin(SSD1306_SWITCHCAPVCC, 0x3C); + display.clearDisplay(); + display.display(); +} + +void loop() { + delay(1000); +} +""" + + +def make_compiler_with_real_paths() -> ESPIDFCompiler: + comp = ESPIDFCompiler.__new__(ESPIDFCompiler) + comp.idf_path = 'C:/Espressif/frameworks/esp-idf-v4.4.7' + comp.arduino_path = str(ESP32_LIBS.parent) # C:/Espressif/components/arduino-esp32 + comp.has_arduino = ESP32_LIBS.parent.is_dir() + return comp + + +class TestRealPathsSSD1306(unittest.TestCase): + """ + Uses actual library directories on this machine to reproduce the build failure. + """ + + def setUp(self): + self.comp = make_compiler_with_real_paths() + self.tmp = tempfile.mkdtemp(prefix='velxio_diag_') + self.user_libs = Path(self.tmp) / 'user_libs' + self.user_libs.mkdir() + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + # ── Pre-conditions ──────────────────────────────────────────────────────── + + def test_prereq_arduino_libs_exists(self): + self.assertTrue(ARDUINO_LIBS.is_dir(), + f'Arduino libs not found at {ARDUINO_LIBS}') + + def test_prereq_gfx_library_installed(self): + gfx = ARDUINO_LIBS / 'Adafruit_GFX_Library' + self.assertTrue(gfx.is_dir(), + 'Adafruit_GFX_Library not installed — install via Library Manager') + + def test_prereq_ssd1306_library_installed(self): + ssd = ARDUINO_LIBS / 'Adafruit_SSD1306' + self.assertTrue(ssd.is_dir(), + 'Adafruit_SSD1306 not installed — install via Library Manager') + + def test_prereq_gfx_header_exists(self): + h = ARDUINO_LIBS / 'Adafruit_GFX_Library' / 'Adafruit_GFX.h' + self.assertTrue(h.exists(), f'Adafruit_GFX.h not found at {h}') + + def test_prereq_ssd1306_header_exists(self): + h = ARDUINO_LIBS / 'Adafruit_SSD1306' / 'Adafruit_SSD1306.h' + self.assertTrue(h.exists(), f'Adafruit_SSD1306.h not found at {h}') + + # ── Module identity ─────────────────────────────────────────────────────── + + def test_loaded_module_has_user_libs_all(self): + """Verify the LIVE .py file is loaded, not stale .pyc.""" + import inspect + source = inspect.getsource(ESPIDFCompiler._resolve_library_components) + self.assertIn('user_libs_all', source, + 'STALE CODE: _resolve_library_components does not contain ' + '"user_libs_all" — the old bytecode (.pyc) is being used. ' + 'Kill uvicorn, delete __pycache__, restart.') + + def test_module_file_path(self): + """Print the actual file being loaded.""" + import app.services.espidf_compiler as mod + print(f'\n[diag] Loaded module from: {mod.__file__}') + self.assertIn('espidf_compiler', mod.__file__) + + # ── _detect_external_includes ───────────────────────────────────────────── + + def test_detect_finds_gfx_and_ssd1306(self): + """Headers from the SSD1306 sketch must all be detected.""" + result = self.comp._detect_external_includes(SSD1306_SKETCH) + print(f'\n[diag] _detect_external_includes → {result}') + self.assertIn('Adafruit_GFX.h', result, + 'Adafruit_GFX.h not detected — it was silently skipped') + self.assertIn('Adafruit_SSD1306.h', result) + self.assertIn('Wire.h', result) + + # ── _find_library_for_header ────────────────────────────────────────────── + + def test_find_gfx_in_arduino_libs(self): + result = self.comp._find_library_for_header('Adafruit_GFX.h', ARDUINO_LIBS) + print(f'\n[diag] _find_library_for_header(Adafruit_GFX.h) → {result}') + self.assertIsNotNone(result, + f'Adafruit_GFX.h not found in {ARDUINO_LIBS}') + + def test_find_ssd1306_in_arduino_libs(self): + result = self.comp._find_library_for_header('Adafruit_SSD1306.h', ARDUINO_LIBS) + print(f'\n[diag] _find_library_for_header(Adafruit_SSD1306.h) → {result}') + self.assertIsNotNone(result, + f'Adafruit_SSD1306.h not found in {ARDUINO_LIBS}') + + def test_find_arduino_libs_dir(self): + result = self.comp._find_arduino_libraries_dir() + print(f'\n[diag] _find_arduino_libraries_dir() → {result}') + self.assertIsNotNone(result, '_find_arduino_libraries_dir() returned None') + self.assertTrue(result.is_dir()) + + # ── _resolve_library_components ─────────────────────────────────────────── + + def test_resolve_creates_user_libs_all(self): + """After resolution, user_libs/ must contain user_libs_all/, NOT separate dirs.""" + arduino_libs = self.comp._find_arduino_libraries_dir() + esp32_libs = Path(self.comp.arduino_path) / 'libraries' if self.comp.arduino_path else None + + names, h2c = self.comp._resolve_library_components( + SSD1306_SKETCH_HEADERS, + arduino_libs, esp32_libs, + 'arduino-esp32', self.user_libs, + ) + + print(f'\n[diag] component_names = {names}') + print(f'[diag] header_to_comp = {h2c}') + print(f'[diag] user_libs/ contents:') + for p in sorted(self.user_libs.rglob('*')): + print(f' {p.relative_to(self.user_libs)}') + + self.assertEqual(names, ['user_libs_all'], + f'Expected ["user_libs_all"], got {names}.\n' + 'If names contains "Adafruit_SSD1306" etc., the old code is still running.') + + def test_gfx_header_in_user_libs_all(self): + """Adafruit_GFX.h must be inside user_libs_all/, not in a separate dir.""" + arduino_libs = self.comp._find_arduino_libraries_dir() + esp32_libs = Path(self.comp.arduino_path) / 'libraries' if self.comp.arduino_path else None + + self.comp._resolve_library_components( + SSD1306_SKETCH_HEADERS, + arduino_libs, esp32_libs, + 'arduino-esp32', self.user_libs, + ) + + gfx_in_all = self.user_libs / 'user_libs_all' / 'Adafruit_GFX.h' + gfx_separate = self.user_libs / 'Adafruit_GFX_Library' / 'Adafruit_GFX.h' + + print(f'\n[diag] user_libs_all/Adafruit_GFX.h exists: {gfx_in_all.exists()}') + print(f'[diag] separate Adafruit_GFX_Library/Adafruit_GFX.h exists: {gfx_separate.exists()}') + + self.assertTrue(gfx_in_all.exists(), + 'Adafruit_GFX.h NOT in user_libs_all — ' + 'when Adafruit_SSD1306.cpp compiles it cannot find it') + self.assertFalse(gfx_separate.exists(), + 'Separate Adafruit_GFX_Library/ dir found — OLD CODE IS RUNNING') + + def test_cmake_lists_both_cpp_files(self): + """user_libs_all/CMakeLists.txt must list Adafruit_GFX.cpp and Adafruit_SSD1306.cpp.""" + arduino_libs = self.comp._find_arduino_libraries_dir() + esp32_libs = Path(self.comp.arduino_path) / 'libraries' if self.comp.arduino_path else None + + self.comp._resolve_library_components( + SSD1306_SKETCH_HEADERS, + arduino_libs, esp32_libs, + 'arduino-esp32', self.user_libs, + ) + + cmake_path = self.user_libs / 'user_libs_all' / 'CMakeLists.txt' + if not cmake_path.exists(): + self.fail('user_libs_all/CMakeLists.txt does not exist — no component was created') + + cmake_text = cmake_path.read_text(encoding='utf-8') + print(f'\n[diag] user_libs_all/CMakeLists.txt:\n{cmake_text}') + + self.assertIn('Adafruit_GFX.cpp', cmake_text) + self.assertIn('Adafruit_SSD1306.cpp', cmake_text) + self.assertIn('INCLUDE_DIRS "."', cmake_text) + + +if __name__ == '__main__': + print('=' * 70) + print('SSD1306 Real-Path Diagnostic Test') + print(f'Arduino libs : {ARDUINO_LIBS}') + print(f'ESP32 libs : {ESP32_LIBS}') + print('=' * 70) + unittest.main(verbosity=2)