Skip to content

Commit e738ddc

Browse files
author
Aayush Maini
committed
Add more UTs for Rust Parsers
1 parent 0ecb941 commit e738ddc

File tree

3 files changed

+542
-1
lines changed

3 files changed

+542
-1
lines changed

src/Microsoft.ComponentDetection.Detectors/rust/Parsers/RustCliParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ private void TraverseAndRecordComponents(
250250
{
251251
try
252252
{
253-
var isDevelopmentDependency = depInfo?.DepKinds.Any(x => x.Kind is Kind.Dev) ?? false;
253+
var isDevelopmentDependency = depInfo?.DepKinds?.Any(x => x.Kind is Kind.Dev) ?? false;
254254

255255
if (!packagesMetadata.TryGetValue($"{id}", out var cargoComponent))
256256
{

test/Microsoft.ComponentDetection.Detectors.Tests/RustCliParserTests.cs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)