@@ -475,5 +475,234 @@ def test_format_fix_record_transitions(self):
475475 self .assertTrue (any ("Manifest: Host (missing) -> Expected [ver v2.14.1, built 2025-12-18]" in l for l in lines_missing ))
476476
477477
478+ class TestPackageValidation (unittest .TestCase ):
479+ """Test Debian (.deb) and RPM (.rpm) package extraction, inspection, and validation."""
480+
481+ def setUp (self ):
482+ self .temp_dir = tempfile .TemporaryDirectory ()
483+ self .base = Path (self .temp_dir .name )
484+
485+ def tearDown (self ):
486+ self .temp_dir .cleanup ()
487+
488+ def _build_test_deb (self , data_files : Dict [str , bytes ], control_fields : Dict [str , str ], out_path : Path ) -> Path :
489+ """Create a standard Debian .deb package (ar archive containing debian-binary, control.tar.gz, data.tar.gz)."""
490+ import io
491+ import tarfile
492+
493+ # 1. debian-binary
494+ deb_bin = b"2.0\n "
495+
496+ # 2. control.tar.gz
497+ ctrl_buf = io .BytesIO ()
498+ with tarfile .open (fileobj = ctrl_buf , mode = "w:gz" ) as tar :
499+ ctrl_text = "" .join (f"{ k } : { v } \n " for k , v in control_fields .items ()).encode ("utf-8" )
500+ ti = tarfile .TarInfo (name = "control" )
501+ ti .size = len (ctrl_text )
502+ ti .mode = 0o644
503+ tar .addfile (ti , io .BytesIO (ctrl_text ))
504+ ctrl_data = ctrl_buf .getvalue ()
505+
506+ # 3. data.tar.gz
507+ data_buf = io .BytesIO ()
508+ with tarfile .open (fileobj = data_buf , mode = "w:gz" ) as tar :
509+ for rel_path , content in data_files .items ():
510+ ti = tarfile .TarInfo (name = rel_path )
511+ ti .size = len (content )
512+ ti .mode = 0o644
513+ tar .addfile (ti , io .BytesIO (content ))
514+ data_bytes = data_buf .getvalue ()
515+
516+ def make_ar_member (name : str , payload : bytes ) -> bytes :
517+ hdr = f"{ name :<16} { 1700000000 :<12} { 0 :<6} { 0 :<6} { 100644 :<8} { len (payload ):<10} \x60 \n " .encode ("ascii" )
518+ pad = b"\n " if len (payload ) % 2 == 1 else b""
519+ return hdr + payload + pad
520+
521+ ar_data = b"!<arch>\n "
522+ ar_data += make_ar_member ("debian-binary" , deb_bin )
523+ ar_data += make_ar_member ("control.tar.gz" , ctrl_data )
524+ ar_data += make_ar_member ("data.tar.gz" , data_bytes )
525+
526+ out_path .parent .mkdir (parents = True , exist_ok = True )
527+ out_path .write_bytes (ar_data )
528+ return out_path
529+
530+ def test_extract_deb_package (self ):
531+ from validate_sof_install import extract_deb
532+
533+ deb_path = self .base / "firmware-sof-signed_2.14.1_all.deb"
534+ self ._build_test_deb (
535+ data_files = {
536+ "lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri" : b"TEST_TGL_FW_DATA" ,
537+ "lib/firmware/intel/sof-ipc4-tplg/sof-tgl-nocodec.tplg" : b"TEST_TPLG_DATA" ,
538+ },
539+ control_fields = {
540+ "Package" : "firmware-sof-signed" ,
541+ "Version" : "2.14.1-1" ,
542+ "Architecture" : "all" ,
543+ "Description" : "Sound Open Firmware binary release" ,
544+ },
545+ out_path = deb_path ,
546+ )
547+
548+ dest_dir = self .base / "extracted_deb"
549+ dest_dir .mkdir ()
550+ meta = extract_deb (deb_path , dest_dir )
551+
552+ self .assertEqual (meta .get ("package" ), "firmware-sof-signed" )
553+ self .assertEqual (meta .get ("version" ), "2.14.1-1" )
554+ self .assertEqual (meta .get ("architecture" ), "all" )
555+
556+ extracted_files = [str (p .relative_to (dest_dir )) for p in dest_dir .rglob ("*" ) if p .is_file ()]
557+ self .assertIn ("lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri" , extracted_files )
558+ self .assertIn ("lib/firmware/intel/sof-ipc4-tplg/sof-tgl-nocodec.tplg" , extracted_files )
559+
560+ def test_collect_packages (self ):
561+ from validate_sof_install import collect_packages
562+
563+ deb1 = self .base / "pkg1.deb"
564+ deb2 = self .base / "pkg2.deb"
565+ deb1 .write_bytes (b"dummy1" )
566+ deb2 .write_bytes (b"dummy2" )
567+
568+ # Comma-separated list
569+ res = collect_packages ([f"{ deb1 } ,{ deb2 } " ])
570+ self .assertEqual (res , [deb1 .resolve (), deb2 .resolve ()])
571+
572+ # Directory containing packages
573+ pkg_dir = self .base / "pkgs"
574+ pkg_dir .mkdir ()
575+ (pkg_dir / "a.deb" ).write_bytes (b"a" )
576+ (pkg_dir / "b.rpm" ).write_bytes (b"b" )
577+
578+ dir_res = collect_packages ([str (pkg_dir )])
579+ self .assertEqual (len (dir_res ), 2 )
580+
581+ # Missing file error
582+ with self .assertRaises (FileNotFoundError ):
583+ collect_packages ([str (self .base / "nonexistent.deb" )])
584+
585+ def test_package_target_validation_and_identify (self ):
586+ from validate_sof_install import PackageTarget , SofBinRepo , SofValidator , Status
587+
588+ # 1. Create mock sof-bin repo with v2.99
589+ repo_dir = self .base / "sof-bin"
590+ vdir = repo_dir / "v2.99.x"
591+ fw_dir = vdir / "sof-ipc4-v2.99" / "tgl" / "community"
592+ tplg_dir = vdir / "sof-ipc4-tplg-v2.99"
593+ fw_dir .mkdir (parents = True )
594+ tplg_dir .mkdir (parents = True )
595+
596+ fw_content = b"IDENTICAL_FW_DATA_FOR_VALIDATION"
597+ tplg_content = b"IDENTICAL_TPLG_DATA_FOR_VALIDATION"
598+
599+ (fw_dir / "sof-tgl.ri" ).write_bytes (fw_content )
600+ (tplg_dir / "sof-tgl-hdmi.tplg" ).write_bytes (tplg_content )
601+
602+ repo = SofBinRepo (repo_dir )
603+
604+ # 2. Build test deb matching repo contents
605+ deb_path = self .base / "firmware-sof_2.99_all.deb"
606+ self ._build_test_deb (
607+ data_files = {
608+ "lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri" : fw_content ,
609+ "lib/firmware/intel/sof-ipc4-tplg/sof-tgl-hdmi.tplg" : tplg_content ,
610+ },
611+ control_fields = {"Package" : "firmware-sof" , "Version" : "2.99-1" , "Architecture" : "all" },
612+ out_path = deb_path ,
613+ )
614+
615+ # 3. Validate PackageTarget against v2.99
616+ target = PackageTarget ([deb_path ])
617+ try :
618+ validator = SofValidator (
619+ repo = repo ,
620+ target = target ,
621+ platform_filter = {"tgl" },
622+ )
623+
624+ records = validator .validate_installation ("v2.99" )
625+ self .assertGreater (len (records ), 0 )
626+ self .assertTrue (all (r .status in (Status .MATCH , Status .SYMLINK_MATCH ) for r in records ))
627+
628+ # 4. Identify mode on PackageTarget
629+ identified = validator .identify_installed ()
630+ self .assertEqual (len (identified ), 2 )
631+ self .assertTrue (all (item ["installed_version" ] == "v2.99" for item in identified ))
632+ finally :
633+ target .cleanup ()
634+
635+ def test_rpm_package_target (self ):
636+ import shutil
637+ import subprocess
638+ from validate_sof_install import PackageTarget , SofBinRepo , SofValidator , Status
639+
640+ if not shutil .which ("rpmbuild" ) or not shutil .which ("rpm2cpio" ):
641+ self .skipTest ("rpmbuild or rpm2cpio not installed" )
642+
643+ # 1. Create mock repo
644+ repo_dir = self .base / "sof-bin-rpm"
645+ vdir = repo_dir / "v2.99.x"
646+ fw_dir = vdir / "sof-ipc4-v2.99" / "tgl" / "community"
647+ fw_dir .mkdir (parents = True )
648+ fw_content = b"RPM_TGL_FW_TEST_PAYLOAD"
649+ (fw_dir / "sof-tgl.ri" ).write_bytes (fw_content )
650+ repo = SofBinRepo (repo_dir )
651+
652+ # 2. Build minimal test RPM
653+ topdir = self .base / "rpmbuild_test"
654+ for sub in ["BUILD" , "RPMS" , "SOURCES" , "SPECS" , "SRPMS" ]:
655+ (topdir / sub ).mkdir (parents = True )
656+
657+ payload_dir = self .base / "payload"
658+ (payload_dir / "lib/firmware/intel/sof-ipc4/tgl/community" ).mkdir (parents = True )
659+ (payload_dir / "lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri" ).write_bytes (fw_content )
660+
661+ spec = f"""
662+ Name: test-sof-firmware
663+ Version: 2.99
664+ Release: 1
665+ Summary: Test SOF Firmware Package
666+ License: BSD
667+ BuildArch: noarch
668+
669+ %description
670+ Test package
671+
672+ %install
673+ mkdir -p %{{buildroot}}/lib/firmware/intel/sof-ipc4/tgl/community
674+ cp { payload_dir } /lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri %{{buildroot}}/lib/firmware/intel/sof-ipc4/tgl/community/
675+
676+ %files
677+ /lib/firmware/intel/sof-ipc4/tgl/community/sof-tgl.ri
678+ """
679+ spec_file = self .base / "test.spec"
680+ spec_file .write_text (spec )
681+
682+ subprocess .run (
683+ ["rpmbuild" , "-bb" , "--define" , f"_topdir { topdir } " , str (spec_file )],
684+ check = True ,
685+ capture_output = True ,
686+ )
687+
688+ rpm_files = list ((topdir / "RPMS" ).rglob ("*.rpm" ))
689+ self .assertTrue (len (rpm_files ) > 0 )
690+ rpm_file = rpm_files [0 ]
691+
692+ # 3. Validate with PackageTarget
693+ target = PackageTarget ([rpm_file ])
694+ try :
695+ validator = SofValidator (repo = repo , target = target , platform_filter = {"tgl" })
696+ records = validator .validate_installation ("v2.99" )
697+ self .assertEqual (len (records ), 1 )
698+ self .assertEqual (records [0 ].status , Status .MATCH )
699+
700+ identified = validator .identify_installed ()
701+ self .assertEqual (len (identified ), 1 )
702+ self .assertEqual (identified [0 ]["installed_version" ], "v2.99" )
703+ finally :
704+ target .cleanup ()
705+
706+
478707if __name__ == "__main__" :
479708 unittest .main ()
0 commit comments