@@ -789,5 +789,144 @@ def test_real_observer_binds_builder_scrub_and_checks_every_recipe(self) -> None
789789 )
790790
791791
792+ class Rp2BuildEnvironmentParserTests (unittest .TestCase ):
793+ """Parse real driver bytes; never execute the shell or a compiler."""
794+
795+ MARKER = (
796+ "# Never allow ambient compiler/make flags to influence the pinned build.\n "
797+ "unset \\ \n "
798+ )
799+ END = '\n \n make -C "$RETAINED_UPSTREAM/mpy-cross"'
800+ SETUP = "# Bind Pico SDK's CMake configure to the verified package"
801+ PACKAGE_VARIABLES = (
802+ "CMAKE_ARGS" , "picotool_DIR" , "FETCHCONTENT_FULLY_DISCONNECTED" ,
803+ "FETCHCONTENT_SOURCE_DIR_PICOTOOL" , "PICOTOOL_FETCH_FROM_GIT_PATH" ,
804+ "PICOTOOL_FORCE_FETCH_FROM_GIT" , "CMAKE_FIND_USE_PACKAGE_REGISTRY" ,
805+ "CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY" , "CMAKE_PREFIX_PATH" ,
806+ )
807+
808+ def setUp (self ) -> None :
809+ self .driver = BUILD_RP2 .read_text (encoding = "utf-8" )
810+
811+ def parse (self , source : str , version : str = "0.6.1" ) -> list [str ]:
812+ parser = getattr (RELEASE , "_audit_rp2_build_environment" , None )
813+ self .assertTrue (callable (parser ), "missing real RP2 environment parser" )
814+ return parser (source , firmware_version = version )
815+
816+ def test_checked_in_driver_accepts_one_unset_and_exact_offline_setup (self ) -> None :
817+ variables = self .parse (self .driver )
818+ self .assertIn ("picotool_DIR" , variables )
819+ self .assertEqual (len (variables ), len (set (variables )))
820+ for variable in (* RELEASE ._RP2_GCC_ENVIRONMENT_OVERRIDES , * self .PACKAGE_VARIABLES ):
821+ with self .subTest (variable = variable ):
822+ self .assertIn (variable , variables )
823+
824+ def test_actual_observer_reaches_cache_after_current_driver_parse (self ) -> None :
825+ class CacheReached (Exception ):
826+ pass
827+
828+ original_read = RELEASE ._read_regular_file_bytes
829+
830+ def read (path , label ):
831+ if label == "RP2 CMake cache" :
832+ raise CacheReached
833+ return original_read (path , label )
834+
835+ with tempfile .TemporaryDirectory () as temporary , mock .patch .object (
836+ RELEASE , "_read_regular_file_bytes" , side_effect = read
837+ ), self .assertRaises (CacheReached ):
838+ target = Path (temporary ) / "unopened-target"
839+ RELEASE ._audit_observe_rp2_arm_runtime_closure (
840+ repo_root = ROOT , target_root = target , cache = target / "CMakeCache.txt" ,
841+ mappings = {}, link_path = target / "link.txt" , map_loads = [],
842+ direct_contributors = set (), archive_members = [], compiler_dependency_closure = [],
843+ )
844+
845+ def test_missing_or_duplicate_boundaries_are_rejected (self ) -> None :
846+ for marker in (self .MARKER , self .END ):
847+ for changed in (self .driver .replace (marker , "" , 1 ),
848+ self .driver .replace (marker , marker + marker , 1 )):
849+ with self .subTest (marker = marker , changed = changed ), self .assertRaises (RELEASE .ReleaseError ):
850+ self .parse (changed )
851+
852+ def test_every_gcc_and_package_override_must_be_scrubbed (self ) -> None :
853+ for variable in (* RELEASE ._RP2_GCC_ENVIRONMENT_OVERRIDES , * self .PACKAGE_VARIABLES ):
854+ line = " " + variable + ("\n " if variable == "CMAKE_PREFIX_PATH" else " \\ \n " )
855+ self .assertEqual (self .driver .count (line ), 1 if variable not in {
856+ "CMAKE_ARGS" , "picotool_DIR" , "FETCHCONTENT_FULLY_DISCONNECTED" ,
857+ "PICOTOOL_FORCE_FETCH_FROM_GIT" , "CMAKE_FIND_USE_PACKAGE_REGISTRY"
858+ } else 2 )
859+ with self .subTest (variable = variable ), self .assertRaises (RELEASE .ReleaseError ):
860+ self .parse (self .driver .replace (line , "" , 1 ))
861+
862+ def test_duplicate_scrub_name_is_rejected_even_if_coverage_is_complete (self ) -> None :
863+ line = " GCC_EXEC_PREFIX \\ \n "
864+ with self .assertRaises (RELEASE .ReleaseError ):
865+ self .parse (self .driver .replace (line , line + line , 1 ))
866+
867+ def test_shell_identifier_case_is_preserved_without_shell_syntax (self ) -> None :
868+ safe = self .driver .replace (self .MARKER , self .MARKER + " _safe_Mixed9 \\ \n " , 1 )
869+ self .assertIn ("_safe_Mixed9" , self .parse (safe ))
870+ for variable in ("9BAD" , "BAD-NAME" , "A=1" , "$PATH" , "A;echo" , "$(id)" , "" ):
871+ with self .subTest (variable = variable ), self .assertRaises (RELEASE .ReleaseError ):
872+ self .parse (self .driver .replace (" CFLAGS_EXTRA \\ \n " , " " + variable + " \\ \n " , 1 ))
873+
874+ def test_continuations_indentation_and_terminal_line_are_exact (self ) -> None :
875+ cases = (
876+ self .driver .replace (" CFLAGS_EXTRA \\ \n " , " CFLAGS_EXTRA\n " , 1 ),
877+ self .driver .replace (" CFLAGS_EXTRA \\ \n " , " CFLAGS_EXTRA \\ \n " , 1 ),
878+ self .driver .replace (" CFLAGS_EXTRA \\ \n " , "\t CFLAGS_EXTRA \\ \n " , 1 ),
879+ self .driver .replace (" CMAKE_PREFIX_PATH\n " , " CMAKE_PREFIX_PATH \\ \n " , 1 ),
880+ self .driver .replace (" CFLAGS_EXTRA \\ \n " , " CFLAGS_EXTRA \\ \n " , 1 ),
881+ )
882+ for changed in cases :
883+ with self .subTest (changed = changed ), self .assertRaises (RELEASE .ReleaseError ):
884+ self .parse (changed )
885+
886+ def test_unknown_intervening_commands_and_reassignments_are_rejected (self ) -> None :
887+ for command in ('GCC_EXEC_PREFIX=/tmp/other' , 'export CFLAGS=-fplugin=bad' ,
888+ 'echo injected' , 'unset CMAKE_ARGS' , ': "$(id)"' ):
889+ for changed in (
890+ self .driver .replace (self .SETUP , command + "\n " + self .SETUP , 1 ),
891+ self .driver .replace (self .END , "\n " + command + self .END , 1 ),
892+ ):
893+ with self .subTest (command = command ), self .assertRaises (RELEASE .ReleaseError ):
894+ self .parse (changed )
895+
896+ def test_offline_values_and_package_binding_are_exact (self ) -> None :
897+ for old , new in (
898+ ('-Dpicotool_DIR=$PICOTOOL_PACKAGE_DIR' , '-Dpicotool_DIR=/tmp/other' ),
899+ ('-DFETCHCONTENT_FULLY_DISCONNECTED:BOOL=ON' , '-DFETCHCONTENT_FULLY_DISCONNECTED=ON' ),
900+ ('-DFETCHCONTENT_FULLY_DISCONNECTED:BOOL=ON' , '-DFETCHCONTENT_FULLY_DISCONNECTED:BOOL=OFF' ),
901+ ('-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF' , '-DCMAKE_FIND_USE_PACKAGE_REGISTRY=ON' ),
902+ ('-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF' , '-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=ON' ),
903+ ('-DPICOTOOL_FORCE_FETCH_FROM_GIT=OFF' , '-DPICOTOOL_FORCE_FETCH_FROM_GIT=ON' ),
904+ ('picotool_DIR="$PICOTOOL_PACKAGE_DIR"' , 'picotool_DIR="$(echo bad)"' ),
905+ ('FETCHCONTENT_FULLY_DISCONNECTED=ON' , 'FETCHCONTENT_FULLY_DISCONNECTED=OFF' ),
906+ ):
907+ self .assertIn (old , self .driver )
908+ with self .subTest (old = old ), self .assertRaises (RELEASE .ReleaseError ):
909+ self .parse (self .driver .replace (old , new , 1 ))
910+
911+ def test_offline_export_is_not_optional_or_extensible (self ) -> None :
912+ start = self .driver .index ("\n export \\ \n " , self .driver .index (self .SETUP ))
913+ end = self .driver .index (self .END , start )
914+ export = self .driver [start :end ]
915+ for changed_export in ("" , export .replace (" picotool_DIR \\ \n " , "" , 1 ),
916+ export .replace ("export \\ \n " , "export \\ \n GCC_EXEC_PREFIX \\ \n " , 1 )):
917+ with self .subTest (export = changed_export ), self .assertRaises (RELEASE .ReleaseError ):
918+ self .parse (self .driver [:start ] + changed_export + self .driver [end :])
919+
920+ def test_historical_scrub_only_form_is_version_selected (self ) -> None :
921+ start = self .driver .index ("\n \n " + self .SETUP )
922+ end = self .driver .index (self .END , start )
923+ historical = self .driver [:start ] + self .driver [end :]
924+ self .assertIn ("GCC_EXEC_PREFIX" , self .parse (historical , "0.6.0" ))
925+ with self .assertRaises (RELEASE .ReleaseError ):
926+ self .parse (historical , "0.6.1" )
927+ with self .assertRaises (RELEASE .ReleaseError ):
928+ self .parse (self .driver , "0.6.0" )
929+
930+
792931if __name__ == "__main__" :
793932 unittest .main (verbosity = 2 )
0 commit comments