Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1628 +/- ##
=======================================
+ Coverage 90.4% 90.6% +0.1%
=======================================
Files 441 445 +4
Lines 38314 39492 +1178
Branches 2347 2403 +56
=======================================
+ Hits 34674 35810 +1136
- Misses 3159 3185 +26
- Partials 481 497 +16 ☔ 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 introduces MavenWithFallbackDetector, an experimental Maven detector that provides resilient dependency detection by combining Maven CLI execution with static pom.xml parsing fallback. The detector automatically falls back to static parsing when Maven CLI fails (e.g., due to authentication errors or missing CLI), ensuring components are still detected even in challenging build environments.
Changes:
- Added
MavenWithFallbackDetectorwith dual detection strategy (Maven CLI primary, static parsing fallback) and comprehensive telemetry - Registered new experiment configuration comparing the new detector against the standard
MvnCliComponentDetector - Added 13 comprehensive unit tests covering CLI scenarios, static parser edge cases, environment variable control, and nested pom.xml handling
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs |
Main detector implementation with Maven CLI detection, static XML parsing fallback, nested pom filtering, and telemetry tracking |
src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/MavenWithFallbackExperiment.cs |
Experiment configuration to compare new detector against existing MvnCliComponentDetector |
src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs |
Registered new detector and experiment in DI container |
test/Microsoft.ComponentDetection.Detectors.Tests/MavenWithFallbackDetectorTests.cs |
Comprehensive test suite with 13 tests covering all detection scenarios and edge cases |
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs
Show resolved
Hide resolved
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs
Show resolved
Hide resolved
| foreach (var error in this.mavenCliErrors) | ||
| { | ||
| var endpoints = this.ExtractFailedEndpoints(error); | ||
| foreach (var endpoint in endpoints) | ||
| { | ||
| this.failedEndpoints.Add(endpoint); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var error in this.mavenCliErrors) | |
| { | |
| var endpoints = this.ExtractFailedEndpoints(error); | |
| foreach (var endpoint in endpoints) | |
| { | |
| this.failedEndpoints.Add(endpoint); | |
| } | |
| } | |
| foreach (var endpoint in this.mavenCliErrors.SelectMany(this.ExtractFailedEndpoints)) | |
| { | |
| this.failedEndpoints.Add(endpoint); | |
| } |
| .Returns("false"); | ||
|
|
||
| // Act | ||
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility |
There was a problem hiding this comment.
This assignment to componentRecorder is useless, since its value is never read.
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility | |
| var (detectorResult, _) = await this.DetectorTestUtility |
| string versionString; | ||
|
|
||
| if (versionRef.StartsWith("${")) | ||
| { | ||
| versionString = this.ResolveVersion(versionRef, document, file.Location); | ||
| } | ||
| else | ||
| { | ||
| versionString = versionRef; | ||
| } | ||
|
|
There was a problem hiding this comment.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
| string versionString; | |
| if (versionRef.StartsWith("${")) | |
| { | |
| versionString = this.ResolveVersion(versionRef, document, file.Location); | |
| } | |
| else | |
| { | |
| versionString = versionRef; | |
| } | |
| var versionString = versionRef.StartsWith("${") | |
| ? this.ResolveVersion(versionRef, document, file.Location) | |
| : versionRef; |
|
👋 Hi! It looks like you modified some files in the
If none of the above scenarios apply, feel free to ignore this comment 🙂 |
| private int mvnCliComponentCount; | ||
| private int staticParserComponentCount; |
There was a problem hiding this comment.
The mvnCliComponentCount and staticParserComponentCount fields are incremented without synchronization in ProcessMvnCliResult (line 499) and ProcessPomFileStatically (line 574). Since OnFileFoundAsync can be called concurrently (ActionBlock with MaxDegreeOfParallelism > 1), these increments are not thread-safe and can result in lost updates. Consider using Interlocked.Add for these operations or protecting them with a lock.
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs
Outdated
Show resolved
Hide resolved
| private readonly ConcurrentBag<ProcessRequest> originalPomFiles = []; | ||
|
|
||
| // Track Maven CLI errors for analysis | ||
| private readonly ConcurrentBag<string> mavenCliErrors = []; |
There was a problem hiding this comment.
The mavenCliErrors collection is declared but never populated with any error messages. The authentication error analysis in AnalyzeMvnCliFailure (line 377-403) will never detect authentication failures because there are no errors to analyze. The MavenCommandService logs errors but doesn't return them to the detector. Consider modifying the IMavenCommandService interface to return error information, or capture errors from the command line invocation service output.
| foreach (var error in this.mavenCliErrors) | ||
| { | ||
| var endpoints = this.ExtractFailedEndpoints(error); | ||
| foreach (var endpoint in endpoints) | ||
| { | ||
| this.failedEndpoints.Add(endpoint); | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var error in this.mavenCliErrors) | |
| { | |
| var endpoints = this.ExtractFailedEndpoints(error); | |
| foreach (var endpoint in endpoints) | |
| { | |
| this.failedEndpoints.Add(endpoint); | |
| } | |
| foreach (var endpoint in this.mavenCliErrors.SelectMany(this.ExtractFailedEndpoints)) | |
| { | |
| this.failedEndpoints.Add(endpoint); |
| .Returns("false"); | ||
|
|
||
| // Act | ||
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility |
There was a problem hiding this comment.
This assignment to componentRecorder is useless, since its value is never read.
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility | |
| var (detectorResult, _) = await this.DetectorTestUtility |
| string versionString; | ||
|
|
||
| if (versionRef.StartsWith("${")) | ||
| { | ||
| versionString = this.ResolveVersion(versionRef, document, file.Location); | ||
| } | ||
| else | ||
| { | ||
| versionString = versionRef; | ||
| } | ||
|
|
There was a problem hiding this comment.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
| string versionString; | |
| if (versionRef.StartsWith("${")) | |
| { | |
| versionString = this.ResolveVersion(versionRef, document, file.Location); | |
| } | |
| else | |
| { | |
| versionString = versionRef; | |
| } | |
| var versionString = versionRef.StartsWith("${") | |
| ? this.ResolveVersion(versionRef, document, file.Location) | |
| : versionRef; |
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs
Show resolved
Hide resolved
| { | ||
| using var reader = new StreamReader(componentStream.Stream); | ||
| var content = reader.ReadToEnd(); | ||
| return new ProcessRequest | ||
| { | ||
| ComponentStream = new ComponentStream | ||
| { | ||
| Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)), | ||
| Location = componentStream.Location, | ||
| Pattern = MavenManifest, | ||
| }, | ||
| SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(componentStream.Location), | ||
| }; | ||
| }) | ||
| .ToObservable(); |
There was a problem hiding this comment.
The stream is disposed prematurely by the 'using' statement. The ComponentStream.Stream is read in the 'using' block, then the content is passed to a new MemoryStream in the returned ProcessRequest. However, in OnFileFoundAsync, the ProcessRequest.ComponentStream.Stream is accessed again to process the file (line 518 loads document from file.Stream). At that point, the original stream has been disposed. This will cause ObjectDisposedException when the detector tries to process these streams.
| { | ||
| using var reader = new StreamReader(componentStream.Stream); | ||
| var content = reader.ReadToEnd(); | ||
| return new ProcessRequest | ||
| { | ||
| ComponentStream = new ComponentStream | ||
| { | ||
| Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)), | ||
| Location = componentStream.Location, | ||
| Pattern = MavenManifest, | ||
| }, | ||
| SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(componentStream.Location), | ||
| }; | ||
| }) | ||
| .ToList(); |
There was a problem hiding this comment.
The stream is disposed prematurely by the 'using' statement. The ComponentStream.Stream is read in the 'using' block, then the content is passed to a new MemoryStream in the returned ProcessRequest. However, in OnFileFoundAsync, the ProcessRequest.ComponentStream.Stream is accessed again to process the file (line 518 loads document from file.Stream). At that point, the original stream has been disposed. This will cause ObjectDisposedException when the detector tries to process these streams.
| .Select(componentStream => | ||
| { | ||
| var depsDir = Path.GetDirectoryName(componentStream.Location); | ||
| successfulDirectories.Add(depsDir); | ||
|
|
||
| using var reader = new StreamReader(componentStream.Stream); | ||
| var content = reader.ReadToEnd(); | ||
| return new ProcessRequest | ||
| { | ||
| ComponentStream = new ComponentStream | ||
| { | ||
| Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)), | ||
| Location = componentStream.Location, | ||
| Pattern = BcdeMvnDepsFileName, | ||
| }, | ||
| SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder( | ||
| Path.Combine(depsDir, MavenManifest)), | ||
| }; | ||
| }) | ||
| .ToList(); |
There was a problem hiding this comment.
The stream is disposed prematurely by the 'using' statement. The ComponentStream.Stream is read in the 'using' block at line 278, then the content is passed to a new MemoryStream in the returned ProcessRequest. However, in OnFileFoundAsync, the ProcessRequest.ComponentStream.Stream is accessed again to process the file (line 518 loads document from file.Stream). At that point, the original stream has been disposed. This will cause ObjectDisposedException when the detector tries to process these streams.
| foreach (var error in this.mavenCliErrors) | ||
| { | ||
| var endpoints = this.ExtractFailedEndpoints(error); | ||
| foreach (var endpoint in endpoints) | ||
| { | ||
| this.failedEndpoints.Add(endpoint); | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| foreach (var error in this.mavenCliErrors) | |
| { | |
| var endpoints = this.ExtractFailedEndpoints(error); | |
| foreach (var endpoint in endpoints) | |
| { | |
| this.failedEndpoints.Add(endpoint); | |
| } | |
| var endpoints = this.mavenCliErrors.SelectMany(this.ExtractFailedEndpoints); | |
| foreach (var endpoint in endpoints) | |
| { | |
| this.failedEndpoints.Add(endpoint); |
| .Returns("false"); | ||
|
|
||
| // Act | ||
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility |
There was a problem hiding this comment.
This assignment to componentRecorder is useless, since its value is never read.
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility | |
| var (detectorResult, _) = await this.DetectorTestUtility |
| string versionString; | ||
|
|
||
| if (versionRef.StartsWith("${")) | ||
| { | ||
| versionString = this.ResolveVersion(versionRef, document, file.Location); | ||
| } | ||
| else | ||
| { | ||
| versionString = versionRef; | ||
| } | ||
|
|
There was a problem hiding this comment.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
| string versionString; | |
| if (versionRef.StartsWith("${")) | |
| { | |
| versionString = this.ResolveVersion(versionRef, document, file.Location); | |
| } | |
| else | |
| { | |
| versionString = versionRef; | |
| } | |
| var versionString = versionRef.StartsWith("${") | |
| ? this.ResolveVersion(versionRef, document, file.Location) | |
| : versionRef; |
| public class MavenWithFallbackExperiment : IExperimentConfiguration | ||
| { | ||
| /// <inheritdoc /> | ||
| public string Name => "MavenWithFallbackExperiment"; |
There was a problem hiding this comment.
The experiment name is set to "MavenWithFallbackExperiment" which is redundant (includes "Experiment" twice). Looking at other experiments in the codebase:
- SimplePipExperiment has Name => "NewPipDetector" (line 12 in SimplePipExperiment.cs)
- UvLockDetectorExperiment has Name => "UvLockDetectorExperiment" (includes "Experiment")
- LinuxApplicationLayerExperiment has Name => "LinuxApplicationLayer" (excludes "Experiment")
For consistency and brevity, consider changing this to "MavenWithFallback" to match the detector naming pattern and avoid the redundant "Experiment" suffix.
| // Arrange - Maven CLI is available but fails for all pom.xml files (e.g., auth error) | ||
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | ||
| .ReturnsAsync(true); | ||
|
|
||
| // MvnCli runs but produces no bcde-fallback.mvndeps files (simulating complete failure) | ||
| this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<string>(), It.IsAny<CancellationToken>())) | ||
| .ReturnsAsync(new MavenCliResult(true, null)); |
There was a problem hiding this comment.
This test doesn't set up the UseFallbackDetectorEnvVar environment variable, which means the detector will run in experimental mode (static parsing only) and will not execute Maven CLI at all. Looking at the detector logic (lines 182-200 in MavenWithFallbackDetector.cs), when useFallbackDetector is false, the detector returns immediately with static parsing only and never calls MavenCLIExistsAsync().
However, the test sets up MavenCLIExistsAsync() to return true (line 683) and expects Maven CLI to run but fail. This test should enable the fallback detector similar to other Maven CLI tests (e.g., lines 770-774, 893-896).
| // Arrange | ||
| this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync()) | ||
| .ReturnsAsync(true); | ||
|
|
||
| // MvnCli runs but produces no bcde-fallback.mvndeps files (simulating failure) | ||
| this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<string>(), It.IsAny<CancellationToken>())) | ||
| .ReturnsAsync(new MavenCliResult(true, null)); |
There was a problem hiding this comment.
This test doesn't set up the UseFallbackDetectorEnvVar environment variable, which means the detector will run in experimental mode (static parsing only) and will not execute Maven CLI at all. The test expects Maven CLI to run but produce no output, then fall back to static parsing. However, without enabling the fallback detector, the Maven CLI path is never executed.
This test should enable the fallback detector by setting up the environment variable mock:
this.envVarServiceMock.Setup(x => x.DoesEnvironmentVariableExist(MavenWithFallbackDetector.UseFallbackDetectorEnvVar))
.Returns(true);
this.envVarServiceMock.Setup(x => x.GetEnvironmentVariable(MavenWithFallbackDetector.UseFallbackDetectorEnvVar))
.Returns("true");| this.mvnCliComponentCount += newCount - initialCount; | ||
| } | ||
|
|
||
| private void ProcessPomFileStatically(ProcessRequest processRequest) | ||
| { | ||
| var file = processRequest.ComponentStream; | ||
| var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; | ||
| var filePath = file.Location; | ||
|
|
||
| try | ||
| { | ||
| var document = new XmlDocument(); | ||
| document.Load(file.Stream); | ||
|
|
||
| lock (this.documentsLoaded) | ||
| { | ||
| this.documentsLoaded.TryAdd(file.Location, document); | ||
| } | ||
|
|
||
| var namespaceManager = new XmlNamespaceManager(document.NameTable); | ||
| namespaceManager.AddNamespace(ProjNamespace, MavenXmlNamespace); | ||
|
|
||
| var dependencyList = document.SelectNodes(DependencyNode, namespaceManager); | ||
| var componentsFoundInFile = 0; | ||
|
|
||
| foreach (XmlNode dependency in dependencyList) | ||
| { | ||
| var groupId = dependency[GroupIdSelector]?.InnerText; | ||
| var artifactId = dependency[ArtifactIdSelector]?.InnerText; | ||
|
|
||
| if (groupId == null || artifactId == null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var version = dependency[VersionSelector]; | ||
| if (version != null && !version.InnerText.Contains(',')) | ||
| { | ||
| var versionRef = version.InnerText.Trim('[', ']'); | ||
| string versionString; | ||
|
|
||
| if (versionRef.StartsWith("${")) | ||
| { | ||
| versionString = this.ResolveVersion(versionRef, document, file.Location); | ||
| } | ||
| else | ||
| { | ||
| versionString = versionRef; | ||
| } | ||
|
|
||
| if (!versionString.StartsWith("${")) | ||
| { | ||
| var component = new MavenComponent(groupId, artifactId, versionString); | ||
| var detectedComponent = new DetectedComponent(component); | ||
| singleFileComponentRecorder.RegisterUsage(detectedComponent); | ||
| componentsFoundInFile++; | ||
| } | ||
| else | ||
| { | ||
| this.Logger.LogDebug( | ||
| "Version string {Version} for component {Group}/{Artifact} is invalid or unsupported and a component will not be recorded.", | ||
| versionString, | ||
| groupId, | ||
| artifactId); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| this.Logger.LogDebug( | ||
| "Version string for component {Group}/{Artifact} is invalid or unsupported and a component will not be recorded.", | ||
| groupId, | ||
| artifactId); | ||
| } | ||
| } | ||
|
|
||
| this.staticParserComponentCount += componentsFoundInFile; |
There was a problem hiding this comment.
The counter increments for mvnCliComponentCount (line 505) and staticParserComponentCount (line 580) are not thread-safe. While the detector currently runs single-threaded by default (EnableParallelism defaults to false), if parallelism is enabled in the future, these increments could result in race conditions and inaccurate counts.
Consider using Interlocked.Add for thread-safe increments:
- Line 505:
Interlocked.Add(ref this.mvnCliComponentCount, newCount - initialCount); - Line 580:
Interlocked.Add(ref this.staticParserComponentCount, componentsFoundInFile);
This would make the code more robust and prevent potential future bugs if parallelism is enabled.
| foreach (var error in this.mavenCliErrors) | ||
| { | ||
| var endpoints = this.ExtractFailedEndpoints(error); | ||
| foreach (var endpoint in endpoints) | ||
| { | ||
| this.failedEndpoints.Add(endpoint); | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| var (detectorResult, componentRecorder) = await this.DetectorTestUtility | ||
| .WithFile("pom.xml", componentString) | ||
| .WithFile("pom.xml", componentString, searchPatterns: [BcdeMvnFileName]) | ||
| .ExecuteDetectorAsync(); |
There was a problem hiding this comment.
This assignment to componentRecorder is useless, since its value is never read.
| if (versionRef.StartsWith("${")) | ||
| { | ||
| versionString = this.ResolveVersion(versionRef, document, file.Location); | ||
| } | ||
| else | ||
| { | ||
| versionString = versionRef; | ||
| } |
There was a problem hiding this comment.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
MavenWithFallbackDetector
Overview
The
MavenWithFallbackDetectoris an experimental Maven detector that combines Maven CLI detection with static pom.xml parsing fallback. It provides resilient Maven dependency detection even when the Maven CLI fails (e.g., due to authentication errors with private repositories).Key Features
mvn dependency:tree) for full transitive dependency resolutionDetection Flow
Core Components
1. Environment Variable Control
CD_USE_MAVEN_FALLBACK_DETECTOR (New):
trueto fully enable this detector and disableMvnCliComponentDetectorMvnCliComponentDetectorCD_MAVEN_DISABLE_CLI:
trueto bypass Maven CLI entirely and use only static parsingCD_USE_MAVEN_FALLBACK_DETECTOR=trueUseful when:
2. RemoveNestedPomXmls
Filters pom.xml files to keep only root-level ones for Maven CLI processing.
Why? In multi-module Maven projects:
Running
mvn dependency:treeon the root pom automatically processes all child modules. Processing each nested pom separately would be redundant and slow.Algorithm:
3. Maven CLI Execution
Uses
IMavenCommandService.GenerateDependenciesFileAsync()to run:This generates a
bcde-fallback.mvndepsfile containing the full dependency tree.4. Failure Detection
After Maven CLI runs, the detector scans for
bcde-fallback.mvndepsfiles:5. Static Parsing Fallback
When Maven CLI fails, static parsing extracts dependencies directly from pom.xml:
Limitations of static parsing:
[1.0,2.0))${project.version})6. Nested Pom Restoration
When Maven CLI fails (partial or complete), nested pom.xml files that were filtered out by
RemoveNestedPomXmlsare restored for static parsing.Why? Maven CLI on the root pom would have processed nested modules. Since it failed, we need to parse each nested pom.xml individually.
Implementation:
GetAllPomFilesForStaticParsing()- Re-scans ALL pom.xml files (used for complete failure)GetStaticParsingRequestsForFailedFiles()- Re-scans pom.xml files only under failed directories (used for partial failure)7. Authentication Error Analysis
When Maven CLI fails, the detector analyzes error patterns:
If authentication errors are detected:
fallbackReason = MavenFallbackReason.AuthenticationFailureDetection Methods
MvnCliOnlyStaticParserOnlyMixedFallback Reasons
NoneMvnCliDisabledByUserCD_MAVEN_DISABLE_CLI=trueMavenCliNotAvailablemvncommand not found in PATHAuthenticationFailureOtherMvnCliFailureTelemetry
The detector records the following telemetry:
DetectionMethodMvnCliOnly,StaticParserOnly, orMixedFallbackReasonMvnCliComponentCountStaticParserComponentCountTotalComponentCountMavenCliAvailableOriginalPomFileCountFailedEndpointsVersion Property Resolution
Static parsing supports resolving version properties:
The resolver:
<properties>section in current documentUsage
As Part of Component Detection
The detector is registered as an experimental detector and runs automatically when Component Detection scans a directory containing
pom.xmlfiles.Enabling Full Detector Mode
By default, this detector runs in static-only mode when running alongside
MvnCliComponentDetectorto avoid race conditions. To fully enable the detector (including Maven CLI):When
CD_USE_MAVEN_FALLBACK_DETECTOR=true:MavenWithFallbackDetectorruns with full Maven CLI supportMvnCliComponentDetectoris automatically disabledDisabling Maven CLI (Static Parsing Only)
Set the environment variable to use only static parsing:
Experiment Configuration
The detector is paired with
MavenWithFallbackExperimentwhich compares results against the standardMvnCliComponentDetector:MvnCliComponentDetectorMavenWithFallbackDetectorEnable experiments via:
export CD_DETECTOR_EXPERIMENTS=trueUnit Tests
The following unit tests are implemented in
MavenWithFallbackDetectorTests.cs:Maven CLI Scenarios
WhenMavenCliNotAvailable_FallsBackToStaticParsing_AsyncWhenMavenCliNotAvailable_DetectsMultipleDependencies_AsyncWhenMavenCliSucceeds_UsesMvnCliResults_AsyncWhenMavenCliSucceeds_PreservesTransitiveDependencies_AsyncWhenMavenCliProducesNoOutput_FallsBackToStaticParsing_AsyncStatic Parser Edge Cases
StaticParser_IgnoresDependenciesWithoutVersion_AsyncStaticParser_IgnoresDependenciesWithVersionRanges_Async[1.0,2.0)) are skippedStaticParser_ResolvesPropertyVersions_Async${property}versions are resolved from<properties>StaticParser_IgnoresDependenciesWithUnresolvablePropertyVersions_AsyncEmpty/Missing File Scenarios
WhenNoPomXmlFiles_ReturnsSuccessWithNoComponents_AsyncWhenPomXmlHasNoDependencies_ReturnsSuccessWithNoComponents_AsyncEnvironment Variable Control
WhenUseFallbackDetectorNotSet_UsesStaticParsingOnly_AsyncCD_USE_MAVEN_FALLBACK_DETECTORis not set (default behavior to avoid race conditions)WhenUseFallbackDetectorTrue_AndDisableMvnCliTrue_UsesStaticParsing_AsyncCD_MAVEN_DISABLE_CLI=truebypasses Maven CLI when fallback detector is enabledWhenDisableMvnCliEnvVarIsFalse_UsesMvnCliNormally_AsyncCD_MAVEN_DISABLE_CLI=falseuses Maven CLI when fallback detector is enabledNested Pom.xml Handling
WhenMvnCliSucceeds_NestedPomXmlsAreFilteredOut_AsyncWhenMvnCliFailsCompletely_AllNestedPomXmlsAreRestoredForStaticParsing_AsyncWhenMvnCliPartiallyFails_NestedPomXmlsRestoredOnlyForFailedDirectories_AsyncError Analysis and Telemetry
WhenMvnCliFailsWithAuthError_LogsFailedEndpointAndSetsTelemetry_AsyncAuthenticationFailureWhenMvnCliFailsWithNonAuthError_SetsFallbackReasonToOther_AsyncOtherMvnCliFailurewithout extracting endpointsFile Structure