From 80146ce1b940c4075156165f6d677b68d7e368f7 Mon Sep 17 00:00:00 2001 From: Greg Villicana <58237075+grvillic@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:56:59 -0700 Subject: [PATCH] Add logs to MvnCLI and use dictionaries to improve perf on large repos (#1213) * Add logs to MvnCLI and use dictionaries to improve perf on large repos * Add cancellation token to MvnCLI command --- docs/detectors/maven.md | 4 + docs/environment-variables.md | 4 + .../DirectoryItemFacadeOptimized.cs | 12 ++ .../FileComponentDetector.cs | 4 +- .../go/GoComponentDetector.cs | 4 +- .../go/GoComponentWithReplaceDetector.cs | 4 +- .../ivy/IvyDetector.cs | 5 +- .../maven/IMavenCommandService.cs | 5 +- .../maven/MavenCommandService.cs | 45 +++++- .../maven/MvnCliComponentDetector.cs | 69 +++++---- .../npm/NpmLockfileDetectorBase.cs | 5 +- .../pip/PipComponentDetector.cs | 5 +- .../pip/PipReportComponentDetector.cs | 2 +- .../pip/SimplePipComponentDetector.cs | 5 +- .../MavenCommandServiceTests.cs | 144 +++++++++++++++++- 15 files changed, 265 insertions(+), 52 deletions(-) create mode 100644 src/Microsoft.ComponentDetection.Common/DirectoryItemFacadeOptimized.cs diff --git a/docs/detectors/maven.md b/docs/detectors/maven.md index 3d873b421..55b02652d 100644 --- a/docs/detectors/maven.md +++ b/docs/detectors/maven.md @@ -19,3 +19,7 @@ Full dependency graph generation is supported. ## Known limitations Maven detection will not run if `mvn` is unavailable. + +## Environment Variables + +The environment variable `MvnCLIFileLevelTimeoutSeconds` is used to control the max execution time Mvn CLI is allowed to take per each `pom.xml` file. Default value, unbounded. This will restrict any spikes in scanning time caused by Mvn CLI during package restore. We suggest to restore the Maven packages beforehand, so that no network calls happen when executing "mvn dependency:tree" and the graph is captured quickly. diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 4c6ccc584..8d6a00473 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -36,3 +36,7 @@ the given configurations are considered development dependencies. If an entire lockfile will contain only dev dependencies, see `CD_GRADLE_DEV_LOCKFILES` above. [1]: https://go.dev/ref/mod#go-mod-graph + +## `MvnCLIFileLevelTimeoutSeconds` + +When set to any positive integer value, it controls the max execution time Mvn CLI is allowed to take per each `pom.xml` file. Default behavior is unbounded. diff --git a/src/Microsoft.ComponentDetection.Common/DirectoryItemFacadeOptimized.cs b/src/Microsoft.ComponentDetection.Common/DirectoryItemFacadeOptimized.cs new file mode 100644 index 000000000..3dec36a80 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Common/DirectoryItemFacadeOptimized.cs @@ -0,0 +1,12 @@ +namespace Microsoft.ComponentDetection.Common; + +using System.Collections.Generic; +using System.Diagnostics; + +[DebuggerDisplay("{Name}")] +public class DirectoryItemFacadeOptimized +{ + public string Name { get; set; } + + public HashSet FileNames { get; set; } +} diff --git a/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs b/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs index c50ed74b4..819619744 100644 --- a/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Contracts/FileComponentDetector.cs @@ -118,7 +118,7 @@ private async Task ProcessAsync(IObservable processor.Post(processRequest)); @@ -135,7 +135,7 @@ private async Task ProcessAsync(IObservable> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) + protected virtual Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs, CancellationToken cancellationToken = default) { return Task.FromResult(processRequests); } diff --git a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs index 35123b0d5..523a784ce 100644 --- a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs @@ -53,7 +53,9 @@ public GoComponentDetector( public override int Version => 7; protected override Task> OnPrepareDetectionAsync( - IObservable processRequests, IDictionary detectorArgs) + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) { // Filter out any go.sum process requests if the adjacent go.mod file is present and has a go version >= 1.17 var goModProcessRequests = processRequests.Where(processRequest => diff --git a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs index 1964663fa..eed69fbab 100644 --- a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs @@ -53,7 +53,9 @@ public GoComponentWithReplaceDetector( public override int Version => 1; protected override Task> OnPrepareDetectionAsync( - IObservable processRequests, IDictionary detectorArgs) + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) { // Filter out any go.sum process requests if the adjacent go.mod file is present and has a go version >= 1.17 var goModProcessRequests = processRequests.Where(processRequest => diff --git a/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs b/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs index a93ff8f10..2ff6f4d17 100644 --- a/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/ivy/IvyDetector.cs @@ -69,7 +69,10 @@ public IvyDetector( public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) }; - protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) + protected override async Task> OnPrepareDetectionAsync( + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) { if (await this.IsAntLocallyAvailableAsync()) { diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs b/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs index 6f59aa038..8eeb33a42 100644 --- a/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs +++ b/src/Microsoft.ComponentDetection.Detectors/maven/IMavenCommandService.cs @@ -1,5 +1,6 @@ -namespace Microsoft.ComponentDetection.Detectors.Maven; +namespace Microsoft.ComponentDetection.Detectors.Maven; +using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts.Internal; @@ -9,7 +10,7 @@ public interface IMavenCommandService Task MavenCLIExistsAsync(); - Task GenerateDependenciesFileAsync(ProcessRequest processRequest); + Task GenerateDependenciesFileAsync(ProcessRequest processRequest, CancellationToken cancellationToken = default); void ParseDependenciesFile(ProcessRequest processRequest); } diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs index aac439aac..b36bb53a6 100644 --- a/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MavenCommandService.cs @@ -2,6 +2,7 @@ namespace Microsoft.ComponentDetection.Detectors.Maven; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.Internal; @@ -9,6 +10,8 @@ namespace Microsoft.ComponentDetection.Detectors.Maven; public class MavenCommandService : IMavenCommandService { + private const string DetectorLogPrefix = "MvnCli detector"; + internal const string MvnCLIFileLevelTimeoutSecondsEnvVar = "MvnCLIFileLevelTimeoutSeconds"; internal const string PrimaryCommand = "mvn"; internal const string MvnVersionArgument = "--version"; @@ -17,15 +20,18 @@ public class MavenCommandService : IMavenCommandService private readonly ICommandLineInvocationService commandLineInvocationService; private readonly IMavenStyleDependencyGraphParserService parserService; + private readonly IEnvironmentVariableService envVarService; private readonly ILogger logger; public MavenCommandService( ICommandLineInvocationService commandLineInvocationService, IMavenStyleDependencyGraphParserService parserService, + IEnvironmentVariableService envVarService, ILogger logger) { this.commandLineInvocationService = commandLineInvocationService; this.parserService = parserService; + this.envVarService = envVarService; this.logger = logger; } @@ -36,16 +42,45 @@ public async Task MavenCLIExistsAsync() return await this.commandLineInvocationService.CanCommandBeLocatedAsync(PrimaryCommand, AdditionalValidCommands, MvnVersionArgument); } - public async Task GenerateDependenciesFileAsync(ProcessRequest processRequest) + public async Task GenerateDependenciesFileAsync(ProcessRequest processRequest, CancellationToken cancellationToken = default) { + var cliFileTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeoutSeconds = -1; + if (this.envVarService.DoesEnvironmentVariableExist(MvnCLIFileLevelTimeoutSecondsEnvVar) + && int.TryParse(this.envVarService.GetEnvironmentVariable(MvnCLIFileLevelTimeoutSecondsEnvVar), out timeoutSeconds) + && timeoutSeconds >= 0) + { + cliFileTimeout.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); + this.logger.LogInformation("{DetectorPrefix}: {TimeoutVar} var was set to {TimeoutSeconds} seconds.", DetectorLogPrefix, MvnCLIFileLevelTimeoutSecondsEnvVar, timeoutSeconds); + } + var pomFile = processRequest.ComponentStream; + this.logger.LogDebug("{DetectorPrefix}: Running \"dependency:tree\" on {PomFileLocation}", DetectorLogPrefix, pomFile.Location); + var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={this.BcdeMvnDependencyFileName}", "-DoutputType=text", $"-f{pomFile.Location}" }; - var result = await this.commandLineInvocationService.ExecuteCommandAsync(PrimaryCommand, AdditionalValidCommands, cliParameters); + + var result = await this.commandLineInvocationService.ExecuteCommandAsync(PrimaryCommand, AdditionalValidCommands, cancellationToken: cliFileTimeout.Token, cliParameters); + if (result.ExitCode != 0) { - this.logger.LogDebug("Mvn execution failed for pom file: {PomFileLocation}", pomFile.Location); - this.logger.LogError("Mvn output: {MvnStdErr}", string.IsNullOrEmpty(result.StdErr) ? result.StdOut : result.StdErr); - processRequest.SingleFileComponentRecorder.RegisterPackageParseFailure(pomFile.Location); + this.logger.LogDebug("{DetectorPrefix}: execution failed for pom file: {PomFileLocation}", DetectorLogPrefix, pomFile.Location); + var errorMessage = string.IsNullOrWhiteSpace(result.StdErr) ? result.StdOut : result.StdErr; + var isErrorMessagePopulated = !string.IsNullOrWhiteSpace(errorMessage); + + if (isErrorMessagePopulated) + { + this.logger.LogError("Mvn output: {MvnStdErr}", errorMessage); + processRequest.SingleFileComponentRecorder.RegisterPackageParseFailure(pomFile.Location); + } + + if (timeoutSeconds != -1 && cliFileTimeout.IsCancellationRequested) + { + this.logger.LogWarning("{DetectorPrefix}: There was a timeout in {PomFileLocation} file. Increase it using {TimeoutVar} environment variable.", DetectorLogPrefix, pomFile.Location, MvnCLIFileLevelTimeoutSecondsEnvVar); + } + } + else + { + this.logger.LogDebug("{DetectorPrefix}: Execution of \"dependency:tree\" on {PomFileLocation} completed successfully", DetectorLogPrefix, pomFile.Location); } } diff --git a/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs index e85a0af2b..93195c3e9 100644 --- a/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs @@ -1,6 +1,7 @@ namespace Microsoft.ComponentDetection.Detectors.Maven; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -17,6 +18,8 @@ namespace Microsoft.ComponentDetection.Detectors.Maven; public class MvnCliComponentDetector : FileComponentDetector { + private const string MavenManifest = "pom.xml"; + private readonly IMavenCommandService mavenCommandService; public MvnCliComponentDetector( @@ -33,23 +36,28 @@ public MvnCliComponentDetector( public override string Id => "MvnCli"; - public override IList SearchPatterns => new List { "pom.xml" }; + public override IList SearchPatterns => new List { MavenManifest }; public override IEnumerable SupportedComponentTypes => new[] { ComponentType.Maven }; - public override int Version => 3; + public override int Version => 4; public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Maven) }; - protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) + private void LogDebugWithId(string message) + { + this.Logger.LogDebug("{DetectorId} detector: {Message}", this.Id, message); + } + + protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs, CancellationToken cancellationToken = default) { if (!await this.mavenCommandService.MavenCLIExistsAsync()) { - this.Logger.LogDebug("Skipping maven detection as maven is not available in the local PATH."); + this.LogDebugWithId("Skipping maven detection as maven is not available in the local PATH."); return Enumerable.Empty().ToObservable(); } - var processPomFile = new ActionBlock(this.mavenCommandService.GenerateDependenciesFileAsync); + var processPomFile = new ActionBlock(x => this.mavenCommandService.GenerateDependenciesFileAsync(x, cancellationToken)); await this.RemoveNestedPomXmls(processRequests).ForEachAsync(processRequest => { @@ -60,6 +68,8 @@ await this.RemoveNestedPomXmls(processRequests).ForEachAsync(processRequest => await processPomFile.Completion; + this.LogDebugWithId($"Nested {MavenManifest} files processed successfully, retrieving generated dependency graphs."); + return this.ComponentStreamEnumerableFactory.GetComponentStreams(this.CurrentScanRequest.SourceDirectory, new[] { this.mavenCommandService.BcdeMvnDependencyFileName }, this.CurrentScanRequest.DirectoryExclusionPredicate) .Select(componentStream => { @@ -76,7 +86,7 @@ await this.RemoveNestedPomXmls(processRequests).ForEachAsync(processRequest => Pattern = componentStream.Pattern, }, SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder( - Path.Combine(Path.GetDirectoryName(componentStream.Location), "pom.xml")), + Path.Combine(Path.GetDirectoryName(componentStream.Location), MavenManifest)), }; }) .ToObservable(); @@ -93,8 +103,9 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID private IObservable RemoveNestedPomXmls(IObservable componentStreams) { - var directoryItemFacades = new List(); - var directoryItemFacadesByPath = new Dictionary(); + var directoryItemFacades = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + var topLevelDirectories = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + return Observable.Create(s => { return componentStreams.Subscribe( @@ -102,59 +113,47 @@ private IObservable RemoveNestedPomXmls(IObservable new DirectoryItemFacadeOptimized { - directoryItemFacadesByPath[currentDir] = current = new DirectoryItemFacade - { - Name = currentDir, - Files = new List(), - Directories = new List(), - }; - } - - // If we came from a directory, we add it to our graph. - if (last != null) - { - current.Directories.Add(last); - } + Name = currentDir, + FileNames = new HashSet(), + }); // If we didn't come from a directory, it's because we're just getting started. Our current directory should include the file that led to it showing up in the graph. - else + if (last == null) { - current.Files.Add(item); + current.FileNames.Add(Path.GetFileName(item.Location)); } - if (last != null && current.Files.FirstOrDefault(x => string.Equals(Path.GetFileName(x.Location), "pom.xml", StringComparison.OrdinalIgnoreCase)) != null) + if (last != null && current.FileNames.Contains(MavenManifest)) { - this.Logger.LogDebug("Ignoring pom.xml at {ChildPomXmlLocation}, as it has a parent pom.xml that will be processed at {ParentDirName}\\pom.xml .", item.Location, current.Name); + this.LogDebugWithId($"Ignoring {MavenManifest} at {item.Location}, as it has a parent {MavenManifest} that will be processed at {current.Name}\\{MavenManifest} ."); break; } last = current; } - - // Go all the way up - while (currentDir != null); }, s.OnCompleted); }); diff --git a/src/Microsoft.ComponentDetection.Detectors/npm/NpmLockfileDetectorBase.cs b/src/Microsoft.ComponentDetection.Detectors/npm/NpmLockfileDetectorBase.cs index e06d862c7..b619e8524 100644 --- a/src/Microsoft.ComponentDetection.Detectors/npm/NpmLockfileDetectorBase.cs +++ b/src/Microsoft.ComponentDetection.Detectors/npm/NpmLockfileDetectorBase.cs @@ -76,7 +76,10 @@ protected abstract bool TryEnqueueFirstLevelDependencies( TypedComponent parentComponent = null, bool skipValidation = false); - protected override Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) => + protected override Task> OnPrepareDetectionAsync( + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) => Task.FromResult(this.RemoveNodeModuleNestedFiles(processRequests) .Where(pr => { diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs index ef5bcac79..058fcbb9e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipComponentDetector.cs @@ -40,7 +40,10 @@ public PipComponentDetector( public override int Version { get; } = 12; - protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) + protected override async Task> OnPrepareDetectionAsync( + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) { this.CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PythonExePath", out var pythonExePath); if (!await this.pythonCommandService.PythonExistsAsync(pythonExePath)) diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs index 0d808e03e..07469be89 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pip/PipReportComponentDetector.cs @@ -80,7 +80,7 @@ private enum PipReportOverrideBehavior protected override bool EnableParallelism { get; set; } = true; - protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) + protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs, CancellationToken cancellationToken = default) { this.CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PipExePath", out var pipExePath); if (!await this.pipCommandService.PipExistsAsync(pipExePath)) diff --git a/src/Microsoft.ComponentDetection.Detectors/pip/SimplePipComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pip/SimplePipComponentDetector.cs index 2a7467989..21d084e6e 100644 --- a/src/Microsoft.ComponentDetection.Detectors/pip/SimplePipComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/pip/SimplePipComponentDetector.cs @@ -40,7 +40,10 @@ public SimplePipComponentDetector( public override int Version { get; } = 3; - protected override async Task> OnPrepareDetectionAsync(IObservable processRequests, IDictionary detectorArgs) + protected override async Task> OnPrepareDetectionAsync( + IObservable processRequests, + IDictionary detectorArgs, + CancellationToken cancellationToken = default) { this.CurrentScanRequest.DetectorArgs.TryGetValue("Pip.PythonExePath", out var pythonExePath); if (!await this.pythonCommandService.PythonExistsAsync(pythonExePath)) diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs index aad02717b..eac9b8c7c 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/MavenCommandServiceTests.cs @@ -1,9 +1,10 @@ -namespace Microsoft.ComponentDetection.Detectors.Tests; +namespace Microsoft.ComponentDetection.Detectors.Tests; using System; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.ComponentDetection.Common; @@ -20,12 +21,14 @@ public class MavenCommandServiceTests { private readonly Mock commandLineMock; + private readonly Mock environmentVarServiceMock; private readonly Mock parserServiceMock; private readonly MavenCommandService mavenCommandService; public MavenCommandServiceTests() { this.commandLineMock = new Mock(); + this.environmentVarServiceMock = new Mock(); var loggerMock = new Mock>(); this.parserServiceMock = new Mock(); @@ -33,6 +36,7 @@ public MavenCommandServiceTests() this.mavenCommandService = new MavenCommandService( this.commandLineMock.Object, this.parserServiceMock.Object, + this.environmentVarServiceMock.Object, loggerMock.Object); } @@ -80,6 +84,7 @@ public async Task GenerateDependenciesFile_SuccessAsync() this.commandLineMock.Setup(x => x.ExecuteCommandAsync( MavenCommandService.PrimaryCommand, MavenCommandService.AdditionalValidCommands, + It.Is(x => !x.IsCancellationRequested), It.Is(y => this.ShouldBeEquivalentTo(y, cliParameters)))) .ReturnsAsync(new CommandLineExecutionResult { @@ -92,6 +97,143 @@ public async Task GenerateDependenciesFile_SuccessAsync() Mock.Verify(this.commandLineMock); } + [TestMethod] + public async Task GenerateDependenciesFile_SuccessWithParentCancellationTokenAsync() + { + var cts = new CancellationTokenSource(); + var pomLocation = "Test/location"; + var processRequest = new ProcessRequest + { + ComponentStream = new ComponentStream + { + Location = pomLocation, + }, + }; + + cts.Cancel(); + + var bcdeMvnFileName = "bcde.mvndeps"; + var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={bcdeMvnFileName}", "-DoutputType=text", $"-f{pomLocation}" }; + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync( + MavenCommandService.PrimaryCommand, + MavenCommandService.AdditionalValidCommands, + It.Is(x => x.IsCancellationRequested), // We just care that this is cancelled, not the actual output + It.Is(y => this.ShouldBeEquivalentTo(y, cliParameters)))) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + }) + .Verifiable(); + + await this.mavenCommandService.GenerateDependenciesFileAsync(processRequest, cts.Token); + + this.commandLineMock.Verify(); + } + + [TestMethod] + public async Task GenerateDependenciesFile_SuccessWithTimeoutExceptionAsync() + { + var cts = new CancellationTokenSource(); + var pomLocation = "Test/location"; + var processRequest = new ProcessRequest + { + ComponentStream = new ComponentStream + { + Location = pomLocation, + }, + }; + + var bcdeMvnFileName = "bcde.mvndeps"; + var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={bcdeMvnFileName}", "-DoutputType=text", $"-f{pomLocation}" }; + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync( + MavenCommandService.PrimaryCommand, + MavenCommandService.AdditionalValidCommands, + It.IsAny(), + It.Is(y => this.ShouldBeEquivalentTo(y, cliParameters)))) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = -1, + }) + .Verifiable(); + + await this.mavenCommandService.GenerateDependenciesFileAsync(processRequest, cts.Token); + + this.commandLineMock.Verify(); + } + + [TestMethod] + public async Task GenerateDependenciesFile_SuccessWithTimeoutVariableTimeoutAsync() + { + var cts = new CancellationTokenSource(); + var pomLocation = "Test/location"; + var processRequest = new ProcessRequest + { + ComponentStream = new ComponentStream + { + Location = pomLocation, + }, + }; + + var bcdeMvnFileName = "bcde.mvndeps"; + var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={bcdeMvnFileName}", "-DoutputType=text", $"-f{pomLocation}" }; + + this.environmentVarServiceMock + .Setup(x => x.DoesEnvironmentVariableExist(MavenCommandService.MvnCLIFileLevelTimeoutSecondsEnvVar)) + .Returns(true); + + this.environmentVarServiceMock + .Setup(x => x.GetEnvironmentVariable(MavenCommandService.MvnCLIFileLevelTimeoutSecondsEnvVar)) + .Returns("0"); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync( + MavenCommandService.PrimaryCommand, + MavenCommandService.AdditionalValidCommands, + It.Is(x => x.IsCancellationRequested), + It.Is(y => this.ShouldBeEquivalentTo(y, cliParameters)))) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = -1, + }) + .Verifiable(); + + await this.mavenCommandService.GenerateDependenciesFileAsync(processRequest, cts.Token); + + this.commandLineMock.Verify(); + this.environmentVarServiceMock.Verify(); + } + + [TestMethod] + public async Task GenerateDependenciesFile_SuccessWithRandomExceptionAsync() + { + var cts = new CancellationTokenSource(); + var pomLocation = "Test/location"; + var processRequest = new ProcessRequest + { + ComponentStream = new ComponentStream + { + Location = pomLocation, + }, + }; + + var bcdeMvnFileName = "bcde.mvndeps"; + var cliParameters = new[] { "dependency:tree", "-B", $"-DoutputFile={bcdeMvnFileName}", "-DoutputType=text", $"-f{pomLocation}" }; + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync( + MavenCommandService.PrimaryCommand, + MavenCommandService.AdditionalValidCommands, + It.IsAny(), + It.Is(y => this.ShouldBeEquivalentTo(y, cliParameters)))) + .ThrowsAsync(new ArgumentNullException("Something Broke")) + .Verifiable(); + + var action = async () => await this.mavenCommandService.GenerateDependenciesFileAsync(processRequest, cts.Token); + await action.Should().ThrowAsync(); + + this.commandLineMock.Verify(); + } + [TestMethod] public void ParseDependenciesFile_Success() {