Move new maven detector out from experiment#1683
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1683 +/- ##
=======================================
- Coverage 90.8% 90.7% -0.2%
=======================================
Files 453 451 -2
Lines 40322 39919 -403
Branches 2447 2426 -21
=======================================
- Hits 36631 36223 -408
- Misses 3191 3197 +6
+ Partials 500 499 -1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR promotes the experimental MavenWithFallbackDetector logic into the production MvnCliComponentDetector, consolidating Maven dependency detection. The MavenWithFallbackDetector is now deleted, and its behavior (Maven CLI execution with static pom.xml parsing fallback) is merged directly into MvnCliComponentDetector. A separate PR will handle removing the legacy static parsing detector from another repository.
Changes:
MvnCliComponentDetectoris significantly expanded with fallback logic, environment variable control (CD_MAVEN_DISABLE_CLI), telemetry tracking, static pom.xml parsing, and timeout protection previously found only inMavenWithFallbackDetectorMavenWithFallbackDetector.csis deleted along with its dedicated test fileMavenWithFallbackDetectorTests.cs- Corresponding tests are ported to
MvnCliDetectorTests.csand theMavenWithFallbackDetectorregistration is removed from the DI container
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/Microsoft.ComponentDetection.Detectors/maven/MvnCliComponentDetector.cs |
Merges MavenWithFallbackDetector logic (static pom.xml fallback, env var toggle, telemetry, timeout guard) into the production detector; version bumped from 4 → 5 |
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs |
Deleted — logic moved into MvnCliComponentDetector |
src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs |
Removes the now-deleted MavenWithFallbackDetector singleton registration |
test/Microsoft.ComponentDetection.Detectors.Tests/MavenWithFallbackDetectorTests.cs |
Deleted — tests ported to MvnCliDetectorTests.cs |
test/Microsoft.ComponentDetection.Detectors.Tests/MvnCliDetectorTests.cs |
Adds mocks for new dependencies (IEnvironmentVariableService, IFileUtilityService) and ports relevant tests from the deleted MavenWithFallbackDetectorTests.cs; updates MvnCliHappyPath helper to configure the new mock setup |
|
|
||
| // Verify telemetry shows fallback reason as other | ||
| detectorResult.AdditionalTelemetryDetails.Should().ContainKey("FallbackReason"); | ||
| detectorResult.AdditionalTelemetryDetails["FallbackReason"].Should().Be("OtherMvnCliFailure"); |
There was a problem hiding this comment.
The WhenMvnCliFailsWithNonAuthError_SetsFallbackReasonToOther_Async test does not assert that the FailedEndpoints telemetry key is absent when the Maven CLI failure is not due to authentication. The equivalent test in the now-deleted MavenWithFallbackDetectorTests.cs had this assertion: detectorResult.AdditionalTelemetryDetails.Should().NotContainKey("FailedEndpoints"). Without this assertion, if a bug caused FailedEndpoints to be set on non-auth errors, it would go undetected.
| detectorResult.AdditionalTelemetryDetails["FallbackReason"].Should().Be("OtherMvnCliFailure"); | |
| detectorResult.AdditionalTelemetryDetails["FallbackReason"].Should().Be("OtherMvnCliFailure"); | |
| detectorResult.AdditionalTelemetryDetails.Should().NotContainKey("FailedEndpoints"); |
| x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>()), | ||
| Times.Once); | ||
| } | ||
|
|
There was a problem hiding this comment.
Several important test scenarios from the deleted MavenWithFallbackDetectorTests.cs are not ported to MvnCliDetectorTests.cs. These scenarios now apply to the promoted MvnCliComponentDetector but have no test coverage:
- Transitive dependency preservation:
WhenMavenCliSucceeds_PreservesTransitiveDependencies_Async— verifies that when Maven CLI succeeds, the dependency graph (parent → transitive) is preserved correctly. - Complete CLI failure with nested modules:
WhenMvnCliFailsCompletely_AllNestedPomXmlsAreRestoredForStaticParsing_Async— verifies that when CLI fails for all pom.xml files, nested modules are restored for static parsing. - Partial CLI failure:
WhenMvnCliPartiallyFails_NestedPomXmlsRestoredOnlyForFailedDirectories_Async— verifies that nested pom.xml files under directories that succeeded with CLI are not statically parsed, while directories where CLI failed are restored for static parsing. DetectionMethodtelemetry: No test verifies theDetectionMethod=MvnCliOnlytelemetry entry when CLI succeeds for all files.CD_MAVEN_DISABLE_CLI=falsebehavior:WhenDisableMvnCliEnvVarIsFalse_UsesMvnCliNormally_Asyncand related tests — verify that setting the env var to false or an invalid value still allows CLI to run normally.
These scenarios represent core functionality of the merged detector logic and should have corresponding tests.
| [TestMethod] | |
| public async Task WhenMavenCliSucceeds_PreservesTransitiveDependencies_Async() | |
| { | |
| // Arrange | |
| const string bcdeMvnFileName = "bcde.mvndeps"; | |
| var cliOutput = @" | |
| <dependencies> | |
| <dependency> | |
| <groupId>com.example</groupId> | |
| <artifactId>parent-app</artifactId> | |
| <version>1.0.0</version> | |
| </dependency> | |
| <dependency> | |
| <groupId>com.example</groupId> | |
| <artifactId>child-lib</artifactId> | |
| <version>2.0.0</version> | |
| </dependency> | |
| </dependencies>"; | |
| this.mavenCommandServiceMock.Setup(x => x.BcdeMvnDependencyFileName) | |
| .Returns(bcdeMvnFileName); | |
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | |
| .ReturnsAsync(true); | |
| this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>())) | |
| .ReturnsAsync(new MavenCliResult(true, null)); | |
| this.fileUtilityServiceMock.Setup(x => x.Exists(It.IsAny<string>())).Returns(true); | |
| this.fileUtilityServiceMock.Setup(x => x.ReadAllText(It.IsAny<string>())).Returns(cliOutput); | |
| const string rootPom = @"<project> | |
| <modelVersion>4.0.0</modelVersion> | |
| <groupId>com.example</groupId> | |
| <artifactId>parent-app</artifactId> | |
| <version>1.0.0</version> | |
| </project>"; | |
| var (scanResult, componentRecorder) = await this.DetectorTestUtility | |
| .WithFile("pom.xml", rootPom) | |
| .WithFile("pom.xml", cliOutput, searchPatterns: [bcdeMvnFileName]) | |
| .ExecuteDetectorAsync(); | |
| // Assert | |
| scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); | |
| var components = componentRecorder.GetDetectedComponents(); | |
| components.Should().HaveCount(2); | |
| components.Should().Contain(c => c.Component is MavenComponent mc && | |
| mc.ArtifactId == "parent-app"); | |
| components.Should().Contain(c => c.Component is MavenComponent mc && | |
| mc.ArtifactId == "child-lib"); | |
| } | |
| [TestMethod] | |
| public async Task WhenMvnCliFailsCompletely_AllNestedPomXmlsAreRestoredForStaticParsing_Async() | |
| { | |
| // Arrange | |
| const string bcdeMvnFileName = "bcde.mvndeps"; | |
| this.mavenCommandServiceMock.Setup(x => x.BcdeMvnDependencyFileName) | |
| .Returns(bcdeMvnFileName); | |
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | |
| .ReturnsAsync(true); | |
| this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>())) | |
| .ReturnsAsync(new MavenCliResult(false, "mvn failed")); | |
| const string rootPom = @"<project> | |
| <modelVersion>4.0.0</modelVersion> | |
| <groupId>com.example</groupId> | |
| <artifactId>root-app</artifactId> | |
| <version>1.0.0</version> | |
| <modules> | |
| <module>module-a</module> | |
| </modules> | |
| </project>"; | |
| const string modulePom = @"<project> | |
| <modelVersion>4.0.0</modelVersion> | |
| <groupId>com.example</groupId> | |
| <artifactId>module-a</artifactId> | |
| <version>1.0.0</version> | |
| </project>"; | |
| var (scanResult, _) = await this.DetectorTestUtility | |
| .WithFile("pom.xml", rootPom) | |
| .WithFile("module-a/pom.xml", modulePom) | |
| .ExecuteDetectorAsync(); | |
| // Assert | |
| scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); | |
| // CLI should have been attempted for both root and nested pom.xml | |
| this.mavenCommandServiceMock.Verify( | |
| x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>()), | |
| Times.Exactly(2)); | |
| } | |
| [TestMethod] | |
| public async Task WhenMvnCliPartiallyFails_NestedPomXmlsRestoredOnlyForFailedDirectories_Async() | |
| { | |
| // Arrange | |
| const string bcdeMvnFileName = "bcde.mvndeps"; | |
| this.mavenCommandServiceMock.Setup(x => x.BcdeMvnDependencyFileName) | |
| .Returns(bcdeMvnFileName); | |
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | |
| .ReturnsAsync(true); | |
| // First call succeeds, second fails | |
| this.mavenCommandServiceMock.SetupSequence( | |
| x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>())) | |
| .ReturnsAsync(new MavenCliResult(true, null)) | |
| .ReturnsAsync(new MavenCliResult(false, "mvn failed")); | |
| const string rootPom = @"<project> | |
| <modelVersion>4.0.0</modelVersion> | |
| <groupId>com.example</groupId> | |
| <artifactId>root-app</artifactId> | |
| <version>1.0.0</version> | |
| <modules> | |
| <module>module-a</module> | |
| </modules> | |
| </project>"; | |
| const string modulePom = @"<project> | |
| <modelVersion>4.0.0</modelVersion> | |
| <groupId>com.example</groupId> | |
| <artifactId>module-a</artifactId> | |
| <version>1.0.0</version> | |
| </project>"; | |
| var (scanResult, _) = await this.DetectorTestUtility | |
| .WithFile("pom.xml", rootPom) | |
| .WithFile("module-a/pom.xml", modulePom) | |
| .ExecuteDetectorAsync(); | |
| // Assert | |
| scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); | |
| this.mavenCommandServiceMock.Verify( | |
| x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>()), | |
| Times.Exactly(2)); | |
| } | |
| [TestMethod] | |
| public async Task WhenMvnCliSucceeds_EmitsMvnCliOnlyDetectionMethodTelemetry_Async() | |
| { | |
| // Arrange | |
| const string cliOutput = @"<dependencies></dependencies>"; | |
| this.MvnCliHappyPath(cliOutput); | |
| // Act | |
| var (scanResult, _) = await this.DetectorTestUtility.ExecuteDetectorAsync(); | |
| // Assert | |
| scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); | |
| scanResult.AdditionalTelemetryDetails.Should().ContainKey("DetectionMethod"); | |
| scanResult.AdditionalTelemetryDetails["DetectionMethod"].Should().Be("MvnCliOnly"); | |
| } | |
| [TestMethod] | |
| public async Task WhenDisableMvnCliEnvVarIsFalse_UsesMvnCliNormally_Async() | |
| { | |
| // Arrange | |
| const string cliOutput = @"<dependencies></dependencies>"; | |
| Environment.SetEnvironmentVariable("CD_MAVEN_DISABLE_CLI", "false"); | |
| try | |
| { | |
| this.MvnCliHappyPath(cliOutput); | |
| // Act | |
| var (scanResult, _) = await this.DetectorTestUtility.ExecuteDetectorAsync(); | |
| // Assert | |
| scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); | |
| this.mavenCommandServiceMock.Verify( | |
| x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>()), | |
| Times.AtLeastOnce); | |
| } | |
| finally | |
| { | |
| Environment.SetEnvironmentVariable("CD_MAVEN_DISABLE_CLI", null); | |
| } | |
| } | |
| [TestMethod] | |
| public async Task WhenDisableMvnCliEnvVarIsInvalid_UsesMvnCliNormally_Async() | |
| { | |
| // Arrange | |
| const string cliOutput = @"<dependencies></dependencies>"; | |
| Environment.SetEnvironmentVariable("CD_MAVEN_DISABLE_CLI", "not-a-bool"); | |
| try | |
| { | |
| this.MvnCliHappyPath(cliOutput); | |
| // Act | |
| var (scanResult, _) = await this.DetectorTestUtility.ExecuteDetectorAsync(); | |
| // Assert | |
| scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); | |
| this.mavenCommandServiceMock.Verify( | |
| x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>()), | |
| Times.AtLeastOnce); | |
| } | |
| finally | |
| { | |
| Environment.SetEnvironmentVariable("CD_MAVEN_DISABLE_CLI", null); | |
| } | |
| } |
| public async Task WhenDisableMvnCliTrue_UsesStaticParsing_Async() | ||
| { | ||
| // Arrange | ||
| this.environmentVariableServiceMock.Setup(x => x.IsEnvironmentVariableValueTrue("CD_MAVEN_DISABLE_CLI")) |
There was a problem hiding this comment.
The test uses the hardcoded string "CD_MAVEN_DISABLE_CLI" instead of the constant MvnCliComponentDetector.DisableMvnCliEnvVar. While the constant is internal and not directly accessible from the test assembly, the old MavenWithFallbackDetectorTests.cs used MavenWithFallbackDetector.DisableMvnCliEnvVar because that constant was also internal but accessible because tests were aware of it. Consider making DisableMvnCliEnvVar accessible via an [assembly: InternalsVisibleTo("Microsoft.ComponentDetection.Detectors.Tests")] attribute (as done in other parts of the codebase, e.g., src/Microsoft.ComponentDetection.Detectors/pip/IPyPiClient.cs:21) or changing the visibility to public, so the test references the constant directly instead of a magic string that can silently diverge.
| this.environmentVariableServiceMock.Setup(x => x.IsEnvironmentVariableValueTrue("CD_MAVEN_DISABLE_CLI")) | |
| this.environmentVariableServiceMock.Setup(x => x.IsEnvironmentVariableValueTrue(MvnCliComponentDetector.DisableMvnCliEnvVar)) |
|
👋 Hi! It looks like you modified some files in the
If none of the above scenarios apply, feel free to ignore this comment 🙂 |
| IFileUtilityService fileUtilityService, | ||
| ILogger<MvnCliComponentDetector> logger) | ||
| { | ||
| this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; | ||
| this.Scanner = walkerFactory; | ||
| this.mavenCommandService = mavenCommandService; | ||
| this.envVarService = envVarService; | ||
| this.fileUtilityService = fileUtilityService; |
There was a problem hiding this comment.
The IFileUtilityService dependency is injected and assigned to this.fileUtilityService in the constructor, but it is never actually used anywhere else in MvnCliComponentDetector. The old MavenWithFallbackDetector used it to read the generated deps file content after a successful Maven CLI run (fileUtilityService.Exists(depsFilePath) and fileUtilityService.ReadAllText(depsFilePath)), but the new implementation instead relies on ComponentStreamEnumerableFactory.GetComponentStreams() to find all generated dependency files in the source directory. This unused dependency should be removed from the constructor signature and class to avoid confusion.
| IFileUtilityService fileUtilityService, | |
| ILogger<MvnCliComponentDetector> logger) | |
| { | |
| this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; | |
| this.Scanner = walkerFactory; | |
| this.mavenCommandService = mavenCommandService; | |
| this.envVarService = envVarService; | |
| this.fileUtilityService = fileUtilityService; | |
| ILogger<MvnCliComponentDetector> logger) | |
| { | |
| this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; | |
| this.Scanner = walkerFactory; | |
| this.mavenCommandService = mavenCommandService; | |
| this.envVarService = envVarService; |
| [TestMethod] | ||
| public async Task WhenMavenCliProducesNoOutput_FallsBackToStaticParsing_Async() | ||
| { | ||
| // Arrange | ||
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | ||
| .ReturnsAsync(true); | ||
| this.mavenCommandServiceMock.Setup(x => x.BcdeMvnDependencyFileName) | ||
| .Returns("bcde.mvndeps"); | ||
| this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<CancellationToken>())) | ||
| .ReturnsAsync(new MavenCliResult(true, null)); | ||
|
|
||
| // File exists but is empty | ||
| this.fileUtilityServiceMock.Setup(x => x.Exists(It.IsAny<string>())).Returns(true); | ||
| this.fileUtilityServiceMock.Setup(x => x.ReadAllText(It.IsAny<string>())).Returns(string.Empty); | ||
|
|
||
| var pomXmlContent = @"<?xml version=""1.0"" encoding=""UTF-8""?> | ||
| <project xmlns=""http://maven.apache.org/POM/4.0.0""> | ||
| <modelVersion>4.0.0</modelVersion> | ||
| <groupId>com.test</groupId> | ||
| <artifactId>my-app</artifactId> | ||
| <version>1.0.0</version> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.apache.commons</groupId> | ||
| <artifactId>commons-lang3</artifactId> | ||
| <version>3.12.0</version> | ||
| </dependency> | ||
| </dependencies> | ||
| </project>"; | ||
|
|
||
| // Act | ||
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility | ||
| .WithFile("pom.xml", pomXmlContent) | ||
| .ExecuteDetectorAsync(); | ||
|
|
||
| // Assert | ||
| detectorResult.ResultCode.Should().Be(ProcessingResultCode.Success); | ||
|
|
||
| var detectedComponents = componentRecorder.GetDetectedComponents(); | ||
| detectedComponents.Should().ContainSingle(); | ||
|
|
||
| var mavenComponent = detectedComponents.First().Component as MavenComponent; | ||
| mavenComponent.Should().NotBeNull(); | ||
| mavenComponent.GroupId.Should().Be("org.apache.commons"); | ||
| mavenComponent.ArtifactId.Should().Be("commons-lang3"); | ||
| mavenComponent.Version.Should().Be("3.12.0"); | ||
| } |
There was a problem hiding this comment.
This test is copied from the now-deleted MavenWithFallbackDetectorTests but is no longer testing the correct behavior. In the old MavenWithFallbackDetector, the fallback to static parsing occurred when GenerateDependenciesFileAsync returned Success=true but fileUtilityService.ReadAllText(depsFilePath) returned an empty string. The new MvnCliComponentDetector does NOT use IFileUtilityService at all — it relies on ComponentStreamEnumerableFactory.GetComponentStreams() to find any generated bcde.mvndeps files. When CLI reports success but no bcde.mvndeps file exists in the test virtual filesystem, the new implementation does NOT fall back to static parsing (since no failedDirectories are recorded). As a result, this test likely passes with zero detected components, making the assertion detectedComponents.Should().ContainSingle() incorrect. The fileUtilityServiceMock setups are also dead code. This test should be removed or rewritten to reflect the actual behavior of the new implementation.
| /// This allows the MvnCliComponentDetector | ||
| /// to safely share the same output file without race conditions. |
There was a problem hiding this comment.
The updated comment at lines 23-27 is now grammatically incomplete. The sentence "This allows the MvnCliComponentDetector" ends without finishing the thought, then the next line "to safely share the same output file without race conditions." starts with "to" without a subject, making the full sentence read: "This allows the MvnCliComponentDetector to safely share the same output file without race conditions." This is actually a dangling incomplete sentence in the XML doc comment. The comment should be rephrased to form a complete, readable sentence, such as: "This allows the MvnCliComponentDetector to safely share the same output file without race conditions."
| /// This allows the MvnCliComponentDetector | |
| /// to safely share the same output file without race conditions. | |
| /// This design allows the MvnCliComponentDetector to safely share the same output file without race conditions. |
- Add comprehensive directory scanning after Maven CLI execution to find all generated dependency files - Combines original direct file reading (for test compatibility) with ComponentStreamEnumerableFactory scanning (for submodule detection) - Maintains all existing functionality while fixing missing submodule dependencies - Minimal changes to preserve existing behavior and test compatibility
cdba85e to
351a80c
Compare
- Add expectedRemovedDetectors list to handle intentional detector removals - Skip assertions for expected removed detectors in ProcessDetectorVersions - Adjust detector count validation to account for expected removals - Resolves verification test failures caused by consolidating Maven detection into MvnCli
| private void ProcessDetectorVersions() | ||
| { | ||
| // List of detectors that were intentionally removed | ||
| var expectedRemovedDetectors = new HashSet<string> { "MavenWithFallback" }; |
There was a problem hiding this comment.
The expectedRemovedDetectors HashSet is defined independently in both CheckDetectorsRunTimesAndCounts (line 176) and ProcessDetectorVersions (line 247), with the same literal content { "MavenWithFallback" }. This duplication means any future updates to the list must be made in two places. Consider extracting it to a class-level field or property to avoid divergence.
| private void ProcessDetectorVersions() | |
| { | |
| // List of detectors that were intentionally removed | |
| var expectedRemovedDetectors = new HashSet<string> { "MavenWithFallback" }; | |
| private static readonly HashSet<string> ExpectedRemovedDetectors = new HashSet<string> { "MavenWithFallback" }; | |
| private void ProcessDetectorVersions() | |
| { | |
| // List of detectors that were intentionally removed | |
| var expectedRemovedDetectors = ExpectedRemovedDetectors; |
| [TestMethod] | ||
| public void CheckDetectorsRunTimesAndCounts() | ||
| { | ||
| // List of detectors that were intentionally removed | ||
| var expectedRemovedDetectors = new HashSet<string> { "MavenWithFallback" }; |
There was a problem hiding this comment.
The same expectedRemovedDetectors HashSet with identical content is also defined at line 176 in CheckDetectorsRunTimesAndCounts. Both definitions contain "MavenWithFallback" and would need to be updated together whenever the list changes.
| [TestMethod] | |
| public void CheckDetectorsRunTimesAndCounts() | |
| { | |
| // List of detectors that were intentionally removed | |
| var expectedRemovedDetectors = new HashSet<string> { "MavenWithFallback" }; | |
| private static readonly string[] ExpectedRemovedDetectors = { "MavenWithFallback" }; | |
| [TestMethod] | |
| public void CheckDetectorsRunTimesAndCounts() | |
| { | |
| // List of detectors that were intentionally removed | |
| var expectedRemovedDetectors = new HashSet<string>(ExpectedRemovedDetectors); |
| @@ -89,74 +401,440 @@ await this.RemoveNestedPomXmls(processRequests).ForEachAsync(processRequest => | |||
| SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder( | |||
| Path.Combine(Path.GetDirectoryName(componentStream.Location), MavenManifest)), | |||
| }; | |||
| }) | |||
| .ToObservable(); | |||
| }); | |||
|
|
|||
| // Combine dependency files from CLI success with pom.xml files from CLI failures | |||
| return results.Concat(allGeneratedDependencyFiles).ToObservable(); | |||
There was a problem hiding this comment.
The RunMavenCliDetectionAsync method in the updated MvnCliComponentDetector constructs the return value by concatenating two sources: (1) the results ConcurrentBag, which is populated with ProcessRequests for every successfully-processed deps file (line ~312), and (2) allGeneratedDependencyFiles which scans the entire source directory for ALL bcde.mvndeps files (lines 383-404). These two sources will both yield the same deps file for each successful Maven CLI run, causing ProcessMvnCliResult (and thus ParseDependenciesFile) to be called twice for the same file. This will double-count components in mvnCliComponentCount and may cause duplicate graph entries depending on the recorder's behavior. The allGeneratedDependencyFiles scan appears intended to handle sub-module deps files generated by Maven that were not explicitly tracked in results, but it should be filtered to exclude paths already present in results, or results should not add items that will also be picked up by allGeneratedDependencyFiles.
| public async Task StaticParser_IgnoresDependenciesWithoutVersion_Async() | ||
| { | ||
| // Arrange | ||
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | ||
| .ReturnsAsync(false); | ||
|
|
||
| var pomXmlContent = @"<?xml version=""1.0"" encoding=""UTF-8""?> | ||
| <project xmlns=""http://maven.apache.org/POM/4.0.0""> | ||
| <modelVersion>4.0.0</modelVersion> | ||
| <groupId>com.test</groupId> | ||
| <artifactId>my-app</artifactId> | ||
| <version>1.0.0</version> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.apache.commons</groupId> | ||
| <artifactId>commons-lang3</artifactId> | ||
| </dependency> | ||
| </dependencies> | ||
| </project>"; | ||
|
|
||
| // Act | ||
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility | ||
| .WithFile("pom.xml", pomXmlContent) | ||
| .ExecuteDetectorAsync(); | ||
|
|
||
| // Assert | ||
| detectorResult.ResultCode.Should().Be(ProcessingResultCode.Success); | ||
| var detectedComponents = componentRecorder.GetDetectedComponents(); | ||
| detectedComponents.Should().BeEmpty(); | ||
| } |
There was a problem hiding this comment.
The new StaticParser_IgnoresDependenciesWithoutVersion_Async test only includes a single dependency without a version, asserting that zero components are detected. The equivalent test in the deleted MavenWithFallbackDetectorTests had two dependencies (one without version, one with a valid version), verifying that the valid one is still detected while the versionless one is skipped. This mixed case — where some dependencies have missing versions and others are valid — is now untested.
| // Account for expected removed detectors | ||
| var expectedMatchCount = oldMatches.Count - expectedRemovedDetectors.Count; |
There was a problem hiding this comment.
The expectedMatchCount calculation assumes exactly one log line per removed detector (oldMatches.Count - expectedRemovedDetectors.Count). However, each detector may have zero or more log entries (e.g., if MavenWithFallback ran but produced no output, or produced multiple entries). If MavenWithFallback contributed more than one match in the old log, this calculation will produce an over-permissive threshold (too few expected matches required), causing a legitimate lost-detector regression to be silently ignored.
| // Account for expected removed detectors | |
| var expectedMatchCount = oldMatches.Count - expectedRemovedDetectors.Count; | |
| // Account for expected removed detectors by excluding their matches from the old count | |
| var removedDetectorMatchCount = oldMatches.Cast<Match>() | |
| .Count(m => m.Groups[2].Success && expectedRemovedDetectors.Contains(m.Groups[2].Value)); | |
| var expectedMatchCount = oldMatches.Count - removedDetectorMatchCount; |
| finally | ||
| { | ||
| semaphore.Release(); | ||
| } |
There was a problem hiding this comment.
Technically don't need this semaphore anymore, but i don't think there's any harm in keeping it there
|
|
||
| newMatches.Should().HaveCountGreaterThanOrEqualTo(oldMatches.Count, "A detector was lost, make sure this was intentional."); | ||
| // Account for expected removed detectors | ||
| var expectedMatchCount = oldMatches.Count - expectedRemovedDetectors.Count; |
There was a problem hiding this comment.
There is an issue with adding this, as it would allow any detector to be removed after this and no longer fail this first check phase.
Since this is a test that runs as part of the PPE phase and not during the PR, i think it would be best to keep it as is and have that manual confirm step be the expectation.
We also just need to fix those PPE tests in general as they are flaky and pretty much always fail: https://dev.azure.com/cg-scus1/MyFirstProject/_build?definitionId=697&_a=summary
| if (newDetector == null) | ||
| { | ||
| newDetector.Should().NotBeNull($"the detector {cd.DetectorId} was lost, verify this is expected behavior"); | ||
| if (!expectedRemovedDetectors.Contains(cd.DetectorId)) |
There was a problem hiding this comment.
This one is fine though
We are replacing
MvnCliComponentDetectorlogic and its unit test withMavenWithFallBackDetectorA separate PR will remove the legacy Maven static parsing detector in another repository.