"""Preprocessor-aware include scanning for the ESP-IDF library resolver. Reported 2026-08-03: every ESP32 build in a user's project failed with errors from libraries he never installed (SoftwareSerial.h, ArduinoBearSSL.h, Adafruit_GFX.h, AsyncElegantOTA.h) coming out of user_libs_all. Chain: ESPAsyncWebServer guards `#include ` behind `#if defined(ESP8266) || defined(TARGET_RP2040)`. The old scanner was a bare regex with no preprocessor state, so it reported Hash.h anyway; the resolver then searched ~1000 installed libraries, found stemi-hexapod's src/Hash.h, merged that hexapod-robot library whole, and compiled all of its sources. Design rule under test: a branch is skipped ONLY when every identifier in its condition is a platform macro known to be undefined on ESP32. Anything unknown stays LIVE — hiding a genuinely needed header would be a far worse failure than the old over-reporting. No toolchain required: pure scanning logic. """ import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'backend')) from app.services.espidf_compiler import ESPIDFCompiler class TestPreprocessorAwareIncludeScan(unittest.TestCase): def setUp(self) -> None: self.scan = ESPIDFCompiler()._detect_external_includes def test_real_espasyncwebserver_shape(self): """The exact reported break.""" got = self.scan( '#if defined(ESP8266) || defined(TARGET_RP2040)\n' '#include \n' '#include \n' '#else\n' '#include \n' '#endif\n' '#include \n' ) self.assertNotIn('Hash.h', got) self.assertNotIn('ESPAsyncTCP.h', got) self.assertIn('AsyncTCP.h', got) self.assertIn('ArduinoJson.h', got) def test_unknown_macro_stays_live(self): """The safety property: we must never hide a header we cannot rule out. A project-specific macro is not a platform macro.""" self.assertIn('MyLib.h', self.scan( '#if USE_CUSTOM_THING\n#include \n#endif')) self.assertIn('Other.h', self.scan( '#if LIB_VERSION > 2\n#include \n#endif')) def test_esp32_branch_lives_and_foreign_branch_dies(self): got = self.scan( '#ifdef ESP32\n#include \n#endif\n' '#ifdef __AVR__\n#include \n#endif') self.assertIn('WiFi.h', got) self.assertNotIn('SoftwareSerial.h', got) def test_else_of_a_dead_branch_is_live(self): self.assertEqual( self.scan('#ifdef ESP8266\n#include \n' '#else\n#include \n#endif'), ['Alive.h']) def test_elif_selects_the_esp32_arm(self): self.assertEqual( self.scan('#if defined(ESP8266)\n#include \n' '#elif defined(ESP32)\n#include \n' '#else\n#include \n#endif'), ['B.h']) def test_else_of_an_unknown_branch_is_live(self): """The GxEPD2 shape (found by the gallery compile smoke, 2026-08-15): #if ENABLE_GxEPD2_GFX // defaults to 0 — unknown to us #include "GxEPD2_GFX.h" #elif defined(_GFX_H_) #define GxEPD2_GFX_BASE_CLASS GFX #else #include // what the compiler really takes #endif An UNKNOWN #if must not mark the chain as taken: pruning this #else dropped Adafruit_GFX from user_libs_all and broke every GxEPD2 e-paper example with 'fatal error: Adafruit_GFX.h: No such file'.""" got = self.scan( '#if ENABLE_GxEPD2_GFX\n#include "GxEPD2_GFX.h"\n' '#elif defined(_GFX_H_)\n#define GxEPD2_GFX_BASE_CLASS GFX\n' '#else\n#include \n#endif') self.assertIn('Adafruit_GFX.h', got) # The unknown arm itself also stays live — over-reporting is the # accepted cost, hiding a real dependency is not. self.assertIn('GxEPD2_GFX.h', got) def test_else_of_a_provably_true_branch_stays_dead(self): """The counterweight: when the #if IS provably true on ESP32, its #else really is dead and must stay pruned.""" self.assertEqual( self.scan('#if defined(ESP32)\n#include \n' '#else\n#include \n#endif'), ['A.h']) def test_nested_dead_block(self): self.assertEqual( self.scan('#ifdef ESP8266\n#ifdef FOO\n#include \n' '#endif\n#endif\n#include '), ['Top.h']) def test_comments_are_not_includes(self): self.assertEqual( self.scan('// #include \n' '/* #include */\n' '#include '), ['Real.h']) def test_plain_sketch_unchanged(self): got = self.scan('#include \n' '#include \n' '#include ') self.assertIn('ESPAsyncWebServer.h', got) self.assertIn('ESP32Servo.h', got) def test_idf_internal_and_pathed_headers_still_skipped(self): got = self.scan('#include \n' '#include \n' '#include ') self.assertEqual(got, ['DHT.h']) if __name__ == '__main__': unittest.main()