From 0b8a2e68895e63d1459887d7996d26882eb216a6 Mon Sep 17 00:00:00 2001 From: Fernando Rojo Date: Fri, 23 Feb 2024 18:09:29 -0700 Subject: [PATCH] Update RustCLI processing to handle virtual manifests / skip over vendor packages (#1015) * Update RustCLI processing to handle virtual manifests and skip over vendor packages * update tests for new logic * update detector version * resolve comment --- .../rust/Contracts/CargoMetadata.cs | 12 +- .../rust/RustCliDetector.cs | 74 +- .../RustCliDetectorTests.cs | 1209 ++++++++--------- 3 files changed, 659 insertions(+), 636 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoMetadata.cs b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoMetadata.cs index c4b143822..97ee1c1d1 100644 --- a/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoMetadata.cs +++ b/src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoMetadata.cs @@ -345,7 +345,7 @@ public partial class DepKind public enum Kind { Build, Dev }; -public enum CrateType { Bench, Bin, CustomBuild, Example, Lib, ProcMacro, Test }; +public enum CrateType { Bench, Bin, CustomBuild, Example, Lib, ProcMacro, Test, RLib, DyLib, CdyLib, StaticLib }; public partial class CargoMetadata { @@ -453,8 +453,16 @@ public override CrateType Read(ref Utf8JsonReader reader, Type typeToConvert, Js return CrateType.ProcMacro; case "test": return CrateType.Test; + case "rlib": + return CrateType.RLib; + case "dylib": + return CrateType.DyLib; + case "cdylib": + return CrateType.CdyLib; + case "staticlib": + return CrateType.StaticLib; } - throw new Exception("Cannot unmarshal type CrateType"); + throw new Exception($"Cannot unmarshal type CrateType - {value}"); } public override void Write(Utf8JsonWriter writer, CrateType value, JsonSerializerOptions options) diff --git a/src/Microsoft.ComponentDetection.Detectors/rust/RustCliDetector.cs b/src/Microsoft.ComponentDetection.Detectors/rust/RustCliDetector.cs index fca3f7c19..9f65d18c0 100644 --- a/src/Microsoft.ComponentDetection.Detectors/rust/RustCliDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/rust/RustCliDetector.cs @@ -62,7 +62,7 @@ public RustCliDetector( public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Cargo }; /// - public override int Version => 2; + public override int Version => 3; /// public override IList SearchPatterns { get; } = new[] { "Cargo.toml" }; @@ -99,9 +99,9 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID if (cliResult.ExitCode != 0) { - this.Logger.LogWarning("`cargo metadata` failed with {Location}. Ensure the Cargo.lock is up to date.", processRequest.ComponentStream.Location); + this.Logger.LogWarning("`cargo metadata` failed while processing {Location}. with error: {Error}", processRequest.ComponentStream.Location, cliResult.StdErr); record.DidRustCliCommandFail = true; - record.WasRustFallbackStrategyUsed = true; + record.WasRustFallbackStrategyUsed = ShouldFallbackFromError(cliResult.StdErr); record.RustCliCommandError = cliResult.StdErr; record.FallbackReason = "`cargo metadata` failed"; } @@ -118,22 +118,28 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID string.IsNullOrWhiteSpace(x.License) ? null : x.License)); var root = metadata.Resolve.Root; + HashSet visitedDependencies = new(); // A cargo.toml can be used to declare a workspace and not a package (A Virtual Manifest). // In this case, the root will be null as it will not be pulling in dependencies itself. // https://doc.rust-lang.org/cargo/reference/workspaces.html#virtual-workspace if (root == null) { - this.Logger.LogWarning("Virtual Manifest: {Location}, falling back to cargo.lock parsing", processRequest.ComponentStream.Location); - record.DidRustCliCommandFail = true; - record.WasRustFallbackStrategyUsed = true; - record.FallbackReason = "Virtual Manifest"; - } + this.Logger.LogWarning("Virtual Manifest: {Location}", processRequest.ComponentStream.Location); - HashSet visitedDependencies = new(); - if (!record.WasRustFallbackStrategyUsed) + foreach (var dep in metadata.Resolve.Nodes) + { + var componentKey = $"{dep.Id}"; + if (!visitedDependencies.Contains(componentKey)) + { + visitedDependencies.Add(componentKey); + this.TraverseAndRecordComponents(processRequest.SingleFileComponentRecorder, componentStream.Location, graph, dep.Id, null, null, packages, visitedDependencies, explicitlyReferencedDependency: true, isTomlRoot: true); + } + } + } + else { - this.TraverseAndRecordComponents(processRequest.SingleFileComponentRecorder, componentStream.Location, graph, root, null, null, packages, visitedDependencies); + this.TraverseAndRecordComponents(processRequest.SingleFileComponentRecorder, componentStream.Location, graph, root, null, null, packages, visitedDependencies, explicitlyReferencedDependency: true, isTomlRoot: true); } } } @@ -169,13 +175,17 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID private static Dictionary BuildGraph(CargoMetadata cargoMetadata) => cargoMetadata.Resolve.Nodes.ToDictionary(x => x.Id); - private static (string Name, string Version) ParseNameAndVersion(string nameAndVersion) + private static bool IsLocalPackage(CargoPackage package) => package.Source == null; + + private static bool ShouldFallbackFromError(string error) { - var parts = nameAndVersion.Split(' '); - return (parts[0], parts[1]); - } + if (error.Contains("current package believes it's in a workspace", StringComparison.OrdinalIgnoreCase)) + { + return false; + } - private static bool IsLocalPackage(CargoPackage package) => package.Source == null; + return true; + } private static bool ParseDependency(string dependency, out string packageName, out string version, out string source) { @@ -205,12 +215,18 @@ private void TraverseAndRecordComponents( Dep depInfo, IReadOnlyDictionary packagesMetadata, ISet visitedDependencies, - bool explicitlyReferencedDependency = false) + bool explicitlyReferencedDependency = false, + bool isTomlRoot = false) { try { var isDevelopmentDependency = depInfo?.DepKinds.Any(x => x.Kind is Kind.Dev) ?? false; - var (name, version) = ParseNameAndVersion(id); + if (!ParseDependency(id, out var name, out var version, out var source)) + { + // Could not parse the dependency string + this.Logger.LogWarning("Failed to parse dependency '{Id}'", id); + return; + } var (authors, license) = packagesMetadata.TryGetValue($"{name} {version}", out var package) ? package @@ -218,25 +234,29 @@ private void TraverseAndRecordComponents( var detectedComponent = new DetectedComponent(new CargoComponent(name, version, authors, license)); - recorder.RegisterUsage( - detectedComponent, - explicitlyReferencedDependency, - isDevelopmentDependency: isDevelopmentDependency, - parentComponentId: parent?.Component.Id); - if (!graph.TryGetValue(id, out var node)) { this.Logger.LogWarning("Could not find {Id} at {Location} in cargo metadata output", id, location); return; } + var shouldRegister = !isTomlRoot && !source.StartsWith("path+file"); + if (shouldRegister) + { + recorder.RegisterUsage( + detectedComponent, + explicitlyReferencedDependency, + isDevelopmentDependency: isDevelopmentDependency, + parentComponentId: parent?.Component.Id); + } + foreach (var dep in node.Deps) { var componentKey = $"{detectedComponent.Component.Id}{dep.Pkg}"; if (!visitedDependencies.Contains(componentKey)) { visitedDependencies.Add(componentKey); - this.TraverseAndRecordComponents(recorder, location, graph, dep.Pkg, detectedComponent, dep, packagesMetadata, visitedDependencies, parent == null); + this.TraverseAndRecordComponents(recorder, location, graph, dep.Pkg, shouldRegister ? detectedComponent : null, dep, packagesMetadata, visitedDependencies, parent == null); } } } @@ -281,6 +301,10 @@ private async Task ProcessCargoLockFallbackAsync(IComponentStream cargoTomlFile, record.FallbackCargoLockFound = false; return; } + else + { + this.Logger.LogWarning("Falling back to cargo.lock processing using {CargoTomlLocation}", cargoLockFileStream.Location); + } record.FallbackCargoLockLocation = cargoLockFileStream.Location; record.FallbackCargoLockFound = true; diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliDetectorTests.cs index 66c8f7d72..882411c90 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/RustCliDetectorTests.cs @@ -19,64 +19,111 @@ namespace Microsoft.ComponentDetection.Detectors.Tests; [TestCategory("Governance/ComponentDetection")] public class RustCliDetectorTests : BaseDetectorTest { - private Mock mockCliService; - private Mock mockComponentStreamEnumerableFactory; - - [TestInitialize] - public void InitCliMock() - { - this.mockCliService = new Mock(); - this.DetectorTestUtility.AddServiceMock(this.mockCliService); - this.mockComponentStreamEnumerableFactory = new Mock(); - this.DetectorTestUtility.AddServiceMock(this.mockComponentStreamEnumerableFactory); - } - - [TestMethod] - public async Task RustCLiDetector_CommandCantBeLocatedSuccessAsync() - { - this.mockCliService - .Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())) - .ReturnsAsync(false); - - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("Cargo.toml", string.Empty) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().BeEmpty(); - } - - [TestMethod] - public async Task RustCliDetector_FailExecutingCommandSuccessAsync() - { - this.mockCliService - .Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())) - .ReturnsAsync(true); - this.mockCliService - .Setup(x => x.ExecuteCommandAsync("cargo", It.IsAny>(), It.IsAny())) - .ThrowsAsync(new InvalidOperationException()); - - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("Cargo.toml", string.Empty) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().BeEmpty(); - } - - [TestMethod] - public async Task RustCliDetector_HandlesNonZeroExitCodeAsync() - { - var cargoMetadata = @" + private readonly string mockMetadataV1 = @" { - ""packages"": [], + ""packages"": [ + { + ""name"": ""registry-package-1"", + ""version"": ""1.0.1"", + ""id"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""license"": null, + ""license_file"": null, + ""description"": ""test registry package 1"", + ""source"": ""registry+https://test.com/registry-package-1"", + ""dependencies"": [ + { + ""name"": ""inner-dependency-1"", + ""source"": ""registry+registry+https://test.com/inner-dependency-1"", + ""req"": ""^0.3.0"", + ""kind"": ""dev"", + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + } + ] + }, + { + ""name"": ""rust-test"", + ""version"": ""0.1.0"", + ""id"": ""rust-test 0.1.0 (path+file:///C:/test)"", + ""license"": null, + ""license_file"": null, + ""description"": null, + ""source"": null, + ""dependencies"": [ + { + ""name"": ""registry-package-1"", + ""source"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""req"": ""^1.0.1"", + ""kind"": null, + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + }, + { + ""name"": ""rust-test-inner"", + ""source"": ""(path+file:///C:/test/rust-test-inner)"", + ""req"": ""*"", + ""kind"": null, + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null, + ""path"": ""C:\\test\\rust-test-inner"" + }, + { + ""name"": ""dev-dependency-1"", + ""source"": ""registry+https://test.com/dev-dependency-1"", + ""req"": ""^0.4.0"", + ""kind"": ""dev"", + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + } + ] + }, + { + ""name"": ""rust-test-inner"", + ""version"": ""0.1.0"", + ""id"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""license"": null, + ""license_file"": null, + ""description"": null, + ""source"": null, + ""dependencies"": [] + }, + { + ""name"": ""dev-dependency-1"", + ""version"": ""0.4.0"", + ""id"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""license"": null, + ""license_file"": null, + ""description"": ""test dev dependency"", + ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", + ""dependencies"": [] + } + ], ""workspace_members"": [ - ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" + ""rust-test 0.1.0 (path+file:///C:/test)"" + ], + ""workspace_default_members"": [ + ""rust-test 0.1.0 (path+file:///C:/test)"" ], ""resolve"": { ""nodes"": [ { - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", + ""id"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", ""dependencies"": [], ""deps"": [], ""features"": [ @@ -85,57 +132,188 @@ public async Task RustCliDetector_HandlesNonZeroExitCodeAsync() ] }, { - ""id"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"", + ""id"": ""rust-test 0.1.0 (path+file:///C:/test)"", ""dependencies"": [ - ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"" + ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"" ], ""deps"": [ { - ""name"": ""libc"", - ""pkg"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", + ""name"": ""registry-package-1"", + ""pkg"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", ""dep_kinds"": [ { ""kind"": null, ""target"": null } ] + }, + { + ""name"": ""cargo"", + ""pkg"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dep_kinds"": [ + { + ""kind"": null, + ""target"": null + } + ] + }, + { + ""name"": ""dev-dependency-1"", + ""pkg"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""dep_kinds"": [ + { + ""kind"": ""dev"", + ""target"": null + } + ] } ], ""features"": [] + }, + { + ""id"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dependencies"": [], + ""deps"": [], + ""features"": [] + }, + { + ""id"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""dependencies"": [], + ""deps"": [], + ""features"": [] } ], - ""root"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" + ""root"": ""rust-test 0.1.0 (path+file:///C:/test)"" }, - ""target_directory"": ""/home/justin/rust-test/target"", + ""target_directory"": ""C:\\test"", ""version"": 1, - ""workspace_root"": ""/home/justin/rust-test"", + ""workspace_root"": ""C:\\test"", ""metadata"": null }"; - this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); - this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata, ExitCode = -1 }); - - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("Cargo.toml", string.Empty) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().BeEmpty(); - } - [TestMethod] - public async Task RustCliDetector_RegistersCorrectRootDepsAsync() - { - var cargoMetadata = @" + private readonly string mockMetadataWithLicenses = @" { - ""packages"": [], + ""packages"": [ + { + ""name"": ""registry-package-1"", + ""version"": ""1.0.1"", + ""id"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": ""test registry package 1"", + ""source"": ""registry+https://test.com/registry-package-1"", + ""dependencies"": [ + { + ""name"": ""inner-dependency-1"", + ""source"": ""registry+registry+https://test.com/inner-dependency-1"", + ""req"": ""^0.3.0"", + ""kind"": ""dev"", + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + } + ] + }, + { + ""name"": ""rust-test"", + ""version"": ""0.1.0"", + ""id"": ""rust-test 0.1.0 (path+file:///C:/test)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": null, + ""source"": null, + ""dependencies"": [ + { + ""name"": ""registry-package-1"", + ""source"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""req"": ""^1.0.1"", + ""kind"": null, + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + }, + { + ""name"": ""rust-test-inner"", + ""source"": ""(path+file:///C:/test/rust-test-inner)"", + ""req"": ""*"", + ""kind"": null, + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null, + ""path"": ""C:\\test\\rust-test-inner"" + }, + { + ""name"": ""dev-dependency-1"", + ""source"": ""registry+https://test.com/dev-dependency-1"", + ""req"": ""^0.4.0"", + ""kind"": ""dev"", + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + } + ] + }, + { + ""name"": ""rust-test-inner"", + ""version"": ""0.1.0"", + ""id"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": null, + ""source"": null, + ""dependencies"": [] + }, + { + ""name"": ""dev-dependency-1"", + ""version"": ""0.4.0"", + ""id"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": ""test dev dependency"", + ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", + ""dependencies"": [] + } + ], ""workspace_members"": [ - ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" + ""rust-test 0.1.0 (path+file:///C:/test)"" + ], + ""workspace_default_members"": [ + ""rust-test 0.1.0 (path+file:///C:/test)"" ], ""resolve"": { ""nodes"": [ { - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", + ""id"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", ""dependencies"": [], ""deps"": [], ""features"": [ @@ -144,32 +322,252 @@ public async Task RustCliDetector_RegistersCorrectRootDepsAsync() ] }, { - ""id"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"", + ""id"": ""rust-test 0.1.0 (path+file:///C:/test)"", ""dependencies"": [ - ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"" + ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"" ], ""deps"": [ { - ""name"": ""libc"", - ""pkg"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", + ""name"": ""registry-package-1"", + ""pkg"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", ""dep_kinds"": [ { ""kind"": null, ""target"": null } ] + }, + { + ""name"": ""cargo"", + ""pkg"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dep_kinds"": [ + { + ""kind"": null, + ""target"": null + } + ] + }, + { + ""name"": ""dev-dependency-1"", + ""pkg"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""dep_kinds"": [ + { + ""kind"": ""dev"", + ""target"": null + } + ] } ], ""features"": [] + }, + { + ""id"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dependencies"": [], + ""deps"": [], + ""features"": [] + }, + { + ""id"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""dependencies"": [], + ""deps"": [], + ""features"": [] } ], - ""root"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" + ""root"": ""rust-test 0.1.0 (path+file:///C:/test)"" }, - ""target_directory"": ""/home/justin/rust-test/target"", + ""target_directory"": ""C:\\test"", ""version"": 1, - ""workspace_root"": ""/home/justin/rust-test"", + ""workspace_root"": ""C:\\test"", + ""metadata"": null +}"; + + private readonly string mockMetadataVirtualManifest = @" +{ + ""packages"": [ + { + ""name"": ""registry-package-1"", + ""version"": ""1.0.1"", + ""id"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": ""test registry package 1"", + ""source"": ""registry+https://test.com/registry-package-1"", + ""dependencies"": [ + { + ""name"": ""inner-dependency-1"", + ""source"": ""registry+registry+https://test.com/inner-dependency-1"", + ""req"": ""^0.3.0"", + ""kind"": ""dev"", + ""rename"": null, + ""optional"": false, + ""uses_default_features"": true, + ""features"": [], + ""target"": null, + ""registry"": null + } + ] + }, + { + ""name"": ""rust-test-inner"", + ""version"": ""0.1.0"", + ""id"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": null, + ""source"": null, + ""dependencies"": [] + }, + { + ""name"": ""dev-dependency-1"", + ""version"": ""0.4.0"", + ""id"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""license"": ""MIT"", + ""authors"": [ + ""Sample Author 1"", + ""Sample Author 2"" + ], + ""license_file"": null, + ""description"": ""test dev dependency"", + ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", + ""dependencies"": [] + } + ], + ""workspace_members"": [ + ""rust-test 0.1.0 (path+file:///C:/test)"" + ], + ""workspace_default_members"": [ + ""rust-test 0.1.0 (path+file:///C:/test)"" + ], + ""resolve"": { + ""nodes"": [ + { + ""id"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""dependencies"": [], + ""deps"": [], + ""features"": [ + ""default"", + ""std"" + ] + }, + { + ""id"": ""rust-test-inner 0.1.0 (path+file:///C:/test/rust-test-inner)"", + ""dependencies"": [ + ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"" + ], + ""deps"": [ + { + ""name"": ""registry-package-1"", + ""pkg"": ""registry-package-1 1.0.1 (registry+https://test.com/registry-package-1)"", + ""dep_kinds"": [ + { + ""kind"": null, + ""target"": null + } + ] + }, + { + ""name"": ""dev-dependency-1"", + ""pkg"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""dep_kinds"": [ + { + ""kind"": ""dev"", + ""target"": null + } + ] + }], + ""features"": [] + }, + { + ""id"": ""dev-dependency-1 0.4.0 (registry+https://test.com/dev-dependency-1)"", + ""dependencies"": [], + ""deps"": [], + ""features"": [] + } + ], + ""root"": null + }, + ""target_directory"": ""C:\\test"", + ""version"": 1, + ""workspace_root"": ""C:\\test"", ""metadata"": null }"; + + private Mock mockCliService; + private Mock mockComponentStreamEnumerableFactory; + + [TestInitialize] + public void InitCliMock() + { + this.mockCliService = new Mock(); + this.DetectorTestUtility.AddServiceMock(this.mockCliService); + this.mockComponentStreamEnumerableFactory = new Mock(); + this.DetectorTestUtility.AddServiceMock(this.mockComponentStreamEnumerableFactory); + } + + [TestMethod] + public async Task RustCLiDetector_CommandCantBeLocatedSuccessAsync() + { + this.mockCliService + .Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())) + .ReturnsAsync(false); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("Cargo.toml", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task RustCliDetector_FailExecutingCommandSuccessAsync() + { + this.mockCliService + .Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())) + .ReturnsAsync(true); + this.mockCliService + .Setup(x => x.ExecuteCommandAsync("cargo", It.IsAny>(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException()); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("Cargo.toml", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task RustCliDetector_HandlesNonZeroExitCodeAsync() + { + var cargoMetadata = this.mockMetadataV1; + this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); + this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata, ExitCode = -1 }); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("Cargo.toml", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().BeEmpty(); + } + + [TestMethod] + public async Task RustCliDetector_RegistersCorrectRootDepsAsync() + { + var cargoMetadata = this.mockMetadataV1; this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); @@ -185,7 +583,7 @@ public async Task RustCliDetector_RegistersCorrectRootDepsAsync() .GetDetectedComponents() .Select(x => x.Component.Id) .Should() - .BeEquivalentTo("libc 0.2.147 - Cargo", "rust-test 0.1.0 - Cargo"); + .BeEquivalentTo("registry-package-1 1.0.1 - Cargo", "dev-dependency-1 0.4.0 - Cargo"); } [TestMethod] @@ -235,12 +633,7 @@ public async Task RustCliDetector_NotInGraphAsync() .ExecuteDetectorAsync(); scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().HaveCount(2); - componentRecorder - .GetDetectedComponents() - .Select(x => x.Component.Id) - .Should() - .BeEquivalentTo("libc 0.2.147 - Cargo", "rust-test 0.1.0 - Cargo"); + componentRecorder.GetDetectedComponents().Should().HaveCount(0); } [TestMethod] @@ -290,383 +683,46 @@ public async Task RustCliDetector_InvalidNameAsync() .ExecuteDetectorAsync(); scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().HaveCount(1); - - componentRecorder - .GetDetectedComponents() - .Select(x => x.Component.Id) - .Should() - .BeEquivalentTo("rust-test 0.1.0 - Cargo"); + componentRecorder.GetDetectedComponents().Should().HaveCount(0); } [TestMethod] public async Task RustCliDetector_ComponentContainsAuthorAndLicenseAsync() { - var cargoMetadata = @" -{ - ""packages"": [], - ""workspace_members"": [ - ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - ], - ""resolve"": { - ""nodes"": [ - { - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dependencies"": [], - ""deps"": [], - ""features"": [ - ""default"", - ""std"" - ] - }, - { - ""id"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"", - ""dependencies"": [ - ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"" - ], - ""deps"": [ - { - ""name"": ""libc"", - ""pkg"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dep_kinds"": [ - { - ""kind"": null, - ""target"": null - } - ] - } - ], - ""features"": [] - } - ], - ""root"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - }, - ""packages"": [ - { - ""name"": ""libc"", - ""version"": ""0.2.147"", - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""license"": ""MIT"", - ""license_file"": null, - ""description"": """", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""authors"": [ - ""Sample Author 1"", - ""Sample Author 2"" - ], - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""00"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - }, - { - ""name"": ""rust-test"", - ""version"": ""0.1.0"", - ""id"": ""rust-test (registry+https://github.com/rust-lang/crates.io-index)"", - ""license"": ""MIT"", - ""license_file"": null, - ""description"": ""A non-cryptographic hash function using AES-NI for high performance"", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""authors"": [ - ""Sample Author 1"", - ""Sample Author 2"" - ], - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""000"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - } -], - ""target_directory"": ""/home/justin/rust-test/target"", - ""version"": 1, - ""workspace_root"": ""/home/justin/rust-test"", - ""metadata"": null -}"; + var cargoMetadata = this.mockMetadataWithLicenses; this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); - this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); - - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("Cargo.toml", string.Empty) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().HaveCount(2); - - componentRecorder - .GetDetectedComponents() - .Select(x => x.Component.Id) - .Should() - .BeEquivalentTo("libc 0.2.147 - Cargo", "rust-test 0.1.0 - Cargo"); - - var components = componentRecorder.GetDetectedComponents(); - - foreach (var component in components) - { - if (component.Component is CargoComponent cargoComponent) - { - cargoComponent.Author.Should().Be("Sample Author 1, Sample Author 2"); - cargoComponent.License.Should().Be("MIT"); - } - } - } - - [TestMethod] - public async Task RustCliDetector_AuthorAndLicenseNullAsync() - { - var cargoMetadata = @" -{ - ""packages"": [], - ""workspace_members"": [ - ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - ], - ""resolve"": { - ""nodes"": [ - { - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dependencies"": [], - ""deps"": [], - ""features"": [ - ""default"", - ""std"" - ] - }, - { - ""id"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"", - ""dependencies"": [ - ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"" - ], - ""deps"": [ - { - ""name"": ""libc"", - ""pkg"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dep_kinds"": [ - { - ""kind"": null, - ""target"": null - } - ] - } - ], - ""features"": [] - } - ], - ""root"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - }, - ""packages"": [ - { - ""name"": ""libc"", - ""version"": ""0.2.147"", - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""license_file"": null, - ""description"": """", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""00"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - }, - { - ""name"": ""rust-test"", - ""version"": ""0.1.0"", - ""id"": ""rust-test (registry+https://github.com/rust-lang/crates.io-index)"", - ""license_file"": null, - ""description"": """", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""000"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - } -], - ""target_directory"": ""/home/justin/rust-test/target"", - ""version"": 1, - ""workspace_root"": ""/home/justin/rust-test"", - ""metadata"": null -}"; - this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); - this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); - - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("Cargo.toml", string.Empty) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().HaveCount(2); - - componentRecorder - .GetDetectedComponents() - .Select(x => x.Component.Id) - .Should() - .BeEquivalentTo("libc 0.2.147 - Cargo", "rust-test 0.1.0 - Cargo"); - - var components = componentRecorder.GetDetectedComponents(); - - foreach (var component in components) - { - if (component.Component is CargoComponent cargoComponent) - { - cargoComponent.Author.Should().Be(null); - cargoComponent.License.Should().Be(null); - } - } - } - - [TestMethod] - public async Task RustCliDetector_AuthorAndLicenseEmptyStringAsync() - { - var cargoMetadata = @" -{ - ""packages"": [], - ""workspace_members"": [ - ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - ], - ""resolve"": { - ""nodes"": [ - { - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dependencies"": [], - ""deps"": [], - ""features"": [ - ""default"", - ""std"" - ] - }, - { - ""id"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"", - ""dependencies"": [ - ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"" - ], - ""deps"": [ - { - ""name"": ""libc"", - ""pkg"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dep_kinds"": [ - { - ""kind"": null, - ""target"": null - } - ] - } - ], - ""features"": [] - } - ], - ""root"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - }, - ""packages"": [ - { - ""name"": ""libc"", - ""version"": ""0.2.147"", - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""license"": """", - ""license_file"": null, - ""description"": """", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""authors"": [ - """" - ], - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""00"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - }, - { - ""name"": ""rust-test"", - ""version"": ""0.1.0"", - ""id"": ""rust-test (registry+https://github.com/rust-lang/crates.io-index)"", - ""license"": """", - ""license_file"": null, - ""description"": ""A non-cryptographic hash function using AES-NI for high performance"", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""authors"": [ - """" - ], - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""000"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null + this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("Cargo.toml", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().HaveCount(2); + + componentRecorder + .GetDetectedComponents() + .Select(x => x.Component.Id) + .Should() + .BeEquivalentTo("registry-package-1 1.0.1 - Cargo", "dev-dependency-1 0.4.0 - Cargo"); + + var components = componentRecorder.GetDetectedComponents(); + + foreach (var component in components) + { + if (component.Component is CargoComponent cargoComponent) + { + cargoComponent.Author.Should().Be("Sample Author 1, Sample Author 2"); + cargoComponent.License.Should().Be("MIT"); + } + } } -], - ""target_directory"": ""/home/justin/rust-test/target"", - ""version"": 1, - ""workspace_root"": ""/home/justin/rust-test"", - ""metadata"": null -}"; + [TestMethod] + public async Task RustCliDetector_AuthorAndLicenseNullAsync() + { + var cargoMetadata = this.mockMetadataV1; this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); @@ -682,7 +738,7 @@ public async Task RustCliDetector_AuthorAndLicenseEmptyStringAsync() .GetDetectedComponents() .Select(x => x.Component.Id) .Should() - .BeEquivalentTo("libc 0.2.147 - Cargo", "rust-test 0.1.0 - Cargo"); + .BeEquivalentTo("registry-package-1 1.0.1 - Cargo", "dev-dependency-1 0.4.0 - Cargo"); var components = componentRecorder.GetDetectedComponents(); @@ -697,7 +753,7 @@ public async Task RustCliDetector_AuthorAndLicenseEmptyStringAsync() } [TestMethod] - public async Task RustCliDetector_FallBackLogicTriggeredOnVirtualManifestAsync() + public async Task RustCliDetector_AuthorAndLicenseEmptyStringAsync() { var cargoMetadata = @" { @@ -736,7 +792,7 @@ public async Task RustCliDetector_FallBackLogicTriggeredOnVirtualManifestAsync() ""features"": [] } ], - ""root"": null + ""root"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" }, ""packages"": [ { @@ -802,62 +858,22 @@ public async Task RustCliDetector_FallBackLogicTriggeredOnVirtualManifestAsync() ""metadata"": null }"; - var testCargoLockString = @" -[[package]] -name = ""my_dependency"" -version = ""1.0.0"" -source = ""registry+https://github.com/rust-lang/crates.io-index"" -dependencies = [ - ""same_package 1.0.0"" -] - -[[package]] -name = ""other_dependency"" -version = ""0.4.0"" -source = ""registry+https://github.com/rust-lang/crates.io-index"" -dependencies = [ - ""other_dependency_dependency"", -] - -[[package]] -name = ""other_dependency_dependency"" -version = ""0.1.12-alpha.6"" -source = ""registry+https://github.com/rust-lang/crates.io-index"" - -[[package]] -name = ""my_dev_dependency"" -version = ""1.0.0"" -source = ""registry+https://github.com/rust-lang/crates.io-index"" -dependencies = [ - ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dev_dependency_dependency 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)"", -]"; this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); - using var stream = new MemoryStream(); - using var writer = new StreamWriter(stream); - await writer.WriteAsync(testCargoLockString); - await writer.FlushAsync(); - stream.Position = 0; - this.mockComponentStreamEnumerableFactory.Setup(x => x.GetComponentStreams(It.IsAny(), new List { "Cargo.lock" }, It.IsAny(), false)) - .Returns(new[] { new ComponentStream() { Location = "Cargo.toml", Stream = stream } }); - - testCargoLockString = testCargoLockString.Replace("\r", string.Empty); - var (scanResult, componentRecorder) = await this.DetectorTestUtility .WithFile("Cargo.toml", string.Empty) .ExecuteDetectorAsync(); scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - componentRecorder.GetDetectedComponents().Should().HaveCount(4); + componentRecorder.GetDetectedComponents().Should().HaveCount(1); componentRecorder .GetDetectedComponents() .Select(x => x.Component.Id) .Should() - .BeEquivalentTo("other_dependency_dependency 0.1.12-alpha.6 - Cargo", "my_dev_dependency 1.0.0 - Cargo", "my_dependency 1.0.0 - Cargo", "other_dependency 0.4.0 - Cargo"); + .BeEquivalentTo("libc 0.2.147 - Cargo"); var components = componentRecorder.GetDetectedComponents(); @@ -869,119 +885,40 @@ public async Task RustCliDetector_FallBackLogicTriggeredOnVirtualManifestAsync() cargoComponent.License.Should().Be(null); } } + } + + [TestMethod] + public async Task RustCliDetector_VirtualManifestSuccessfullyProcessedAsync() + { + var cargoMetadata = this.mockMetadataVirtualManifest; + + this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); + this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("Cargo.toml", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().HaveCount(2); + + componentRecorder + .GetDetectedComponents() + .Select(x => x.Component.Id) + .Should() + .BeEquivalentTo("registry-package-1 1.0.1 - Cargo", "dev-dependency-1 0.4.0 - Cargo"); + var components = componentRecorder.GetDetectedComponents(); return; } [TestMethod] public async Task RustCliDetector_FallBackLogicFailsIfNoCargoLockFoundAsync() { - var cargoMetadata = @" -{ - ""packages"": [], - ""workspace_members"": [ - ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"" - ], - ""resolve"": { - ""nodes"": [ - { - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dependencies"": [], - ""deps"": [], - ""features"": [ - ""default"", - ""std"" - ] - }, - { - ""id"": ""rust-test 0.1.0 (path+file:///home/justin/rust-test)"", - ""dependencies"": [ - ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"" - ], - ""deps"": [ - { - ""name"": ""libc"", - ""pkg"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""dep_kinds"": [ - { - ""kind"": null, - ""target"": null - } - ] - } - ], - ""features"": [] - } - ], - ""root"": null - }, - ""packages"": [ - { - ""name"": ""libc"", - ""version"": ""0.2.147"", - ""id"": ""libc 0.2.147 (registry+https://github.com/rust-lang/crates.io-index)"", - ""license"": """", - ""license_file"": null, - ""description"": """", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""authors"": [ - """" - ], - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""00"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - }, - { - ""name"": ""rust-test"", - ""version"": ""0.1.0"", - ""id"": ""rust-test (registry+https://github.com/rust-lang/crates.io-index)"", - ""license"": """", - ""license_file"": null, - ""description"": ""A non-cryptographic hash function using AES-NI for high performance"", - ""source"": ""registry+https://github.com/rust-lang/crates.io-index"", - ""dependencies"": [], - ""targets"": [], - ""features"": {}, - ""manifest_path"": """", - ""metadata"": {}, - ""publish"": null, - ""authors"": [ - """" - ], - ""categories"": [], - ""keywords"": [], - ""readme"": ""README.md"", - ""repository"": ""https://github.com/tkaitchuck/ahash"", - ""homepage"": null, - ""documentation"": """", - ""edition"": ""000"", - ""links"": null, - ""default_run"": null, - ""rust_version"": null - } -], - ""target_directory"": ""/home/justin/rust-test/target"", - ""version"": 1, - ""workspace_root"": ""/home/justin/rust-test"", - ""metadata"": null -}"; - this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync(new CommandLineExecutionResult { StdOut = cargoMetadata }); + .ReturnsAsync(new CommandLineExecutionResult { StdOut = string.Empty, ExitCode = -1 }); this.mockComponentStreamEnumerableFactory.Setup(x => x.GetComponentStreams(It.IsAny(), new List { "Cargo.lock" }, It.IsAny(), false)) .Returns(Enumerable.Empty()); @@ -1137,4 +1074,58 @@ public async Task RustCliDetector_FallBackLogicTriggeredOnFailedProcessingAsync( return; } + + [TestMethod] + public async Task RustCliDetector_FallBackLogicSkippedOnWorkspaceErrorAsync() + { + this.mockCliService.Setup(x => x.CanCommandBeLocatedAsync("cargo", It.IsAny>())).ReturnsAsync(true); + this.mockCliService.Setup(x => x.ExecuteCommandAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(new CommandLineExecutionResult { StdOut = null, StdErr = "current package believes it's in a workspace when it's not:", ExitCode = -1 }); + var testCargoLockString = @" +[[package]] +name = ""my_dependency"" +version = ""1.0.0"" +source = ""registry+https://github.com/rust-lang/crates.io-index"" +dependencies = [ + ""same_package 1.0.0"" +] + +[[package]] +name = ""other_dependency"" +version = ""0.4.0"" +source = ""registry+https://github.com/rust-lang/crates.io-index"" +dependencies = [ + ""other_dependency_dependency"", +] + +[[package]] +name = ""other_dependency_dependency"" +version = ""0.1.12-alpha.6"" +source = ""registry+https://github.com/rust-lang/crates.io-index"" + +[[package]] +name = ""my_dev_dependency"" +version = ""1.0.0"" +source = ""registry+https://github.com/rust-lang/crates.io-index"" +dependencies = [ + ""other_dependency_dependency 0.1.12-alpha.6 (registry+https://github.com/rust-lang/crates.io-index)"", + ""dev_dependency_dependency 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)"", +]"; + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + await writer.WriteAsync(testCargoLockString); + await writer.FlushAsync(); + stream.Position = 0; + this.mockComponentStreamEnumerableFactory.Setup(x => x.GetComponentStreams(It.IsAny(), new List { "Cargo.lock" }, It.IsAny(), false)) + .Returns(new[] { new ComponentStream() { Location = "Cargo.toml", Stream = stream } }); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("Cargo.toml", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().HaveCount(0); + + return; + } }