@@ -861,6 +861,280 @@ public async Task AuthorsNormalization_MultipleNonBlankAuthors_JoinsWithComma()
861861 comp . Author . Should ( ) . Be ( "Alice Smith, Bob Jones, Charlie Brown" ) ;
862862 }
863863
864+ [ TestMethod ]
865+ public async Task ProcessMetadata_EmptyPackagesAndNodes_Success ( )
866+ {
867+ // Tests handling of empty packages and nodes
868+ var json = """
869+ {
870+ "packages": [],
871+ "resolve": {
872+ "root": null,
873+ "nodes":[]
874+ }
875+ }
876+ """ ;
877+
878+ var metadata = ParseMetadata ( json ) ;
879+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
880+
881+ var result = await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
882+
883+ result . Success . Should ( ) . BeTrue ( ) ;
884+ result . LocalPackageDirectories . Should ( ) . BeEmpty ( ) ;
885+ fallback . Invocations . Count ( i => i . Method . Name == "RegisterUsage" ) . Should ( ) . Be ( 0 ) ;
886+ }
887+
888+ [ TestMethod ]
889+ public async Task Traverse_IndexOutOfRangeException_RegistersParseFailure ( )
890+ {
891+ // Tests IndexOutOfRangeException handling in TraverseAndRecordComponents
892+ var json = """
893+ {
894+ "packages": [
895+ { "name":"root", "version":"1.0.0", "id":"root 1.0.0", "authors":[""], "license":"", "source":null, "manifest_path":"C:/repo/root/Cargo.toml" },
896+ { "name":"child", "version":"2.0.0", "id":"child 2.0.0", "authors":["A"], "license":"MIT", "source":"registry+https://github.com/rust-lang/crates.io-index", "manifest_path":"C:/repo/child/Cargo.toml" }
897+ ],
898+ "resolve": {
899+ "root":"root 1.0.0",
900+ "nodes":[
901+ { "id":"root 1.0.0", "deps":[ { "pkg":"child 2.0.0", "dep_kinds":[{"kind":"build"}] } ] },
902+ { "id":"child 2.0.0", "deps":[] }
903+ ]
904+ }
905+ }
906+ """ ;
907+
908+ var metadata = ParseMetadata ( json ) ;
909+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
910+
911+ // Mock RegisterPackageParseFailure to verify it's called
912+ fallback . Setup ( f => f . RegisterPackageParseFailure ( It . IsAny < string > ( ) ) ) ;
913+
914+ var result = await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
915+
916+ result . Success . Should ( ) . BeTrue ( ) ;
917+ }
918+
919+ [ TestMethod ]
920+ public async Task ProcessMetadata_LocalPackageWithEmptyManifestPath_Skipped ( )
921+ {
922+ // Tests handling of packages with empty manifest paths
923+ var json = """
924+ {
925+ "packages": [
926+ { "name":"root", "version":"1.0.0", "id":"root 1.0.0", "authors":[""], "license":"", "source":null, "manifest_path":"" }
927+ ],
928+ "resolve": {
929+ "root":"root 1.0.0",
930+ "nodes":[
931+ { "id":"root 1.0.0", "deps":[] }
932+ ]
933+ }
934+ }
935+ """ ;
936+
937+ var metadata = ParseMetadata ( json ) ;
938+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
939+
940+ var result = await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
941+
942+ result . Success . Should ( ) . BeTrue ( ) ;
943+ result . LocalPackageDirectories . Should ( ) . BeEmpty ( ) ;
944+ }
945+
946+ [ TestMethod ]
947+ public async Task ApplyOwners_WithParentInGraph_PassesParentId ( )
948+ {
949+ // Tests that parentComponentId is passed when parent exists in graph
950+ var metadata = ParseMetadata ( BuildNormalRootMetadataJson ( ) ) ;
951+
952+ var parentRecorder = new Mock < IComponentRecorder > ( MockBehavior . Strict ) ;
953+ var owner = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
954+ var ownerGraph = new Mock < IDependencyGraph > ( ) ;
955+
956+ // Setup graph to contain the parent
957+ ownerGraph . Setup ( g => g . Contains ( "childA 2.0.0" ) ) . Returns ( true ) ;
958+ owner . Setup ( r => r . DependencyGraph ) . Returns ( ownerGraph . Object ) ;
959+
960+ parentRecorder . Setup ( p => p . CreateSingleFileComponentRecorder ( "manifests/one" ) ) . Returns ( owner . Object ) ;
961+
962+ var ownershipMap = new Dictionary < string , HashSet < string > >
963+ {
964+ { "childA 2.0.0" , new HashSet < string > { "manifests/one" } } ,
965+ } ;
966+
967+ var result = await this . parser . ParseFromMetadataAsync (
968+ MakeTomlStream ( "C:/repo/Cargo.toml" ) ,
969+ new Mock < ISingleFileComponentRecorder > ( ) . Object ,
970+ metadata ,
971+ parentRecorder . Object ,
972+ ownershipMap ) ;
973+
974+ result . Success . Should ( ) . BeTrue ( ) ;
975+
976+ var registrations = owner . Invocations . Where ( i => i . Method . Name == "RegisterUsage" ) . ToList ( ) ;
977+ registrations . Should ( ) . ContainSingle ( ) ;
978+ }
979+
980+ [ TestMethod ]
981+ public async Task ApplyOwners_EmptyOwnersSet_UsesFallback ( )
982+ {
983+ // Tests that empty owners set falls back to fallback recorder
984+ var metadata = ParseMetadata ( BuildNormalRootMetadataJson ( ) ) ;
985+
986+ var parentRecorder = new Mock < IComponentRecorder > ( MockBehavior . Strict ) ;
987+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
988+
989+ var ownershipMap = new Dictionary < string , HashSet < string > >
990+ {
991+ { "childA 2.0.0" , new HashSet < string > ( ) } , // Empty set
992+ } ;
993+
994+ var result = await this . parser . ParseFromMetadataAsync (
995+ MakeTomlStream ( "C:/repo/Cargo.toml" ) ,
996+ fallback . Object ,
997+ metadata ,
998+ parentRecorder . Object ,
999+ ownershipMap ) ;
1000+
1001+ result . Success . Should ( ) . BeTrue ( ) ;
1002+
1003+ // Should use fallback for childA since owners set is empty
1004+ fallback . Invocations . Count ( i => i . Method . Name == "RegisterUsage" ) . Should ( ) . BeGreaterOrEqualTo ( 1 ) ;
1005+ }
1006+
1007+ [ TestMethod ]
1008+ public async Task Traverse_DepKindsNull_NotTreatedAsDevelopmentDependency ( )
1009+ {
1010+ // Tests handling of null DepKinds
1011+ var json = """
1012+ {
1013+ "packages": [
1014+ { "name":"root", "version":"1.0.0", "id":"root 1.0.0", "authors":[""], "license":"", "source":null, "manifest_path":"C:/repo/root/Cargo.toml" },
1015+ { "name":"child", "version":"2.0.0", "id":"child 2.0.0", "authors":["A"], "license":"MIT", "source":"registry+https://github.com/rust-lang/crates.io-index", "manifest_path":"C:/repo/child/Cargo.toml" }
1016+ ],
1017+ "resolve": {
1018+ "root":"root 1.0.0",
1019+ "nodes":[
1020+ { "id":"root 1.0.0", "deps":[ { "pkg":"child 2.0.0", "dep_kinds":null } ] },
1021+ { "id":"child 2.0.0", "deps":[] }
1022+ ]
1023+ }
1024+ }
1025+ """ ;
1026+
1027+ var metadata = ParseMetadata ( json ) ;
1028+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
1029+
1030+ await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
1031+
1032+ var registrations = fallback . Invocations . Where ( i => i . Method . Name == "RegisterUsage" ) . ToList ( ) ;
1033+ registrations . Should ( ) . ContainSingle ( ) ;
1034+
1035+ // Verify isDevelopmentDependency is false (not true)
1036+ registrations [ 0 ] . Arguments [ 3 ] . Should ( ) . Be ( false ) ;
1037+ }
1038+
1039+ [ TestMethod ]
1040+ public async Task ProcessMetadata_PackageWithNullAuthors_AuthorIsNull ( )
1041+ {
1042+ // Tests that null authors array results in null Author
1043+ var json = """
1044+ {
1045+ "packages": [
1046+ { "name":"root", "version":"1.0.0", "id":"root 1.0.0", "authors":null, "license":"MIT", "source":null, "manifest_path":"C:/repo/root/Cargo.toml" },
1047+ { "name":"child", "version":"2.0.0", "id":"child 2.0.0", "authors":null, "license":"MIT", "source":"registry+https://github.com/rust-lang/crates.io-index", "manifest_path":"C:/repo/child/Cargo.toml" }
1048+ ],
1049+ "resolve": {
1050+ "root":"root 1.0.0",
1051+ "nodes":[
1052+ { "id":"root 1.0.0", "deps":[ { "pkg":"child 2.0.0", "dep_kinds":[{"kind":"build"}] } ] },
1053+ { "id":"child 2.0.0", "deps":[] }
1054+ ]
1055+ }
1056+ }
1057+ """ ;
1058+
1059+ var metadata = ParseMetadata ( json ) ;
1060+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
1061+
1062+ await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
1063+
1064+ var reg = fallback . Invocations . Single ( i => i . Method . Name == "RegisterUsage" ) ;
1065+ var comp = ( ( DetectedComponent ) reg . Arguments [ 0 ] ) . Component as CargoComponent ;
1066+
1067+ comp . Author . Should ( ) . BeNull ( ) ;
1068+ }
1069+
1070+ [ TestMethod ]
1071+ public async Task ProcessMetadata_PackageWithNullLicense_LicenseIsNull ( )
1072+ {
1073+ // Tests that null license results in null License
1074+ var json = """
1075+ {
1076+ "packages": [
1077+ { "name":"root", "version":"1.0.0", "id":"root 1.0.0", "authors":["A"], "license":null, "source":null, "manifest_path":"C:/repo/root/Cargo.toml" },
1078+ { "name":"child", "version":"2.0.0", "id":"child 2.0.0", "authors":["B"], "license":null, "source":"registry+https://github.com/rust-lang/crates.io-index", "manifest_path":"C:/repo/child/Cargo.toml" }
1079+ ],
1080+ "resolve": {
1081+ "root":"root 1.0.0",
1082+ "nodes":[
1083+ { "id":"root 1.0.0", "deps":[ { "pkg":"child 2.0.0", "dep_kinds":[{"kind":"build"}] } ] },
1084+ { "id":"child 2.0.0", "deps":[] }
1085+ ]
1086+ }
1087+ }
1088+ """ ;
1089+
1090+ var metadata = ParseMetadata ( json ) ;
1091+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
1092+
1093+ await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
1094+
1095+ var reg = fallback . Invocations . Single ( i => i . Method . Name == "RegisterUsage" ) ;
1096+ var comp = ( ( DetectedComponent ) reg . Arguments [ 0 ] ) . Component as CargoComponent ;
1097+
1098+ comp . License . Should ( ) . BeNull ( ) ;
1099+ }
1100+
1101+ [ TestMethod ]
1102+ public async Task VirtualManifest_MultipleRootNodes_AllProcessed ( )
1103+ {
1104+ // Tests virtual manifest with multiple independent root nodes
1105+ var json = """
1106+ {
1107+ "packages": [
1108+ { "name":"pkgA", "version":"1.0.0", "id":"pkgA 1.0.0", "authors":["A"], "license":"MIT", "source":"registry+https://github.com/rust-lang/crates.io-index", "manifest_path":"C:/repo/pkgA/Cargo.toml" },
1109+ { "name":"pkgB", "version":"2.0.0", "id":"pkgB 2.0.0", "authors":["B"], "license":"MIT", "source":"registry+https://github.com/rust-lang/crates.io-index", "manifest_path":"C:/repo/pkgB/Cargo.toml" }
1110+ ],
1111+ "resolve": {
1112+ "root": null,
1113+ "nodes":[
1114+ { "id":"pkgA 1.0.0", "deps":[] },
1115+ { "id":"pkgB 2.0.0", "deps":[] }
1116+ ]
1117+ }
1118+ }
1119+ """ ;
1120+
1121+ var metadata = ParseMetadata ( json ) ;
1122+ var fallback = new Mock < ISingleFileComponentRecorder > ( MockBehavior . Loose ) ;
1123+
1124+ var result = await this . InvokeProcessMetadataAsync ( "C:/repo/Cargo.toml" , fallback . Object , metadata ) ;
1125+
1126+ result . Success . Should ( ) . BeTrue ( ) ;
1127+
1128+ var registrations = fallback . Invocations . Where ( i => i . Method . Name == "RegisterUsage" ) . ToList ( ) ;
1129+ var distinctNames = registrations
1130+ . Select ( r => ( ( CargoComponent ) ( ( DetectedComponent ) r . Arguments [ 0 ] ) . Component ) . Name )
1131+ . Distinct ( )
1132+ . ToList ( ) ;
1133+
1134+ distinctNames . Should ( ) . Contain ( "pkgA" ) ;
1135+ distinctNames . Should ( ) . Contain ( "pkgB" ) ;
1136+ }
1137+
8641138 private async Task < ParseResult > InvokeProcessMetadataAsync ( string manifestLocation , ISingleFileComponentRecorder fallbackRecorder , CargoMetadata metadata ) =>
8651139 await this . parser . ParseFromMetadataAsync (
8661140 new ComponentStream { Location = manifestLocation , Pattern = "Cargo.toml" , Stream = new MemoryStream ( [ ] ) } ,
0 commit comments