Skip to content

Make command line handling testable via dependency injection#412

Merged
sensslen merged 10 commits into
mainfrom
copilot/implement-command-line-evaluation
Feb 2, 2026
Merged

Make command line handling testable via dependency injection#412
sensslen merged 10 commits into
mainfrom
copilot/implement-command-line-evaluation

Conversation

Copy link
Copy Markdown

Copilot AI commented Jan 30, 2026

  • Extract command line parameter evaluation logic into a separate class
  • Create a CommandLineOptions class to hold parsed command line parameters
  • Create a LicenseValidationHandler class that accepts options and dependencies via constructor
  • Update Program.CreateRootCommand() to use the new handler
  • Add System.IO.Abstractions.TestingHelpers to test project
  • Create tests for command line option evaluation
  • Run tests to verify the refactoring
  • Request code review
  • Address code review feedback
  • Fix ObjectDisposedException in GetPackageInformations
Original prompt

This section details on the original issue you should resolve

<issue_title>Implement command line option evaluation in a testable manner</issue_title>
<issue_description>Implement command line parameter evaluation separate of the main app so that it can be tested. Also implement tests for this. Make use of TestableIO.System.IO.Abstractions for making file access testable</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Summary by CodeRabbit

  • New Features
    • Unified license validation workflow with richer command-line options and selectable output formats (JSON, pretty JSON, table, Markdown).
  • Refactor
    • Streamlined program flow to delegate processing to a centralized validation handler.
  • Chores
    • CI workflows pinned to specific action revisions for reproducible runs.
  • Tests
    • Added comprehensive tests for the validation workflow and updated test dependencies.

Copilot AI and others added 4 commits January 30, 2026 10:05
Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement command line option evaluation in a testable manner Make command line handling testable via dependency injection Jan 30, 2026
Copilot AI requested a review from sensslen January 30, 2026 10:22
@sensslen sensslen marked this pull request as ready for review February 2, 2026 07:42
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 2, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

The PR adds a CommandLineOptions class and a LicenseValidationHandler that centralizes the license validation pipeline, moving logic out of Program.cs and delegating processing to the handler. It introduces comprehensive tests for LicenseValidationHandler, expands test project dependencies (NSubstitute and TestableIO.System.IO.Abstractions.TestingHelpers), and updates GitHub workflows to pin the vstest-action to specific commit hashes. Several new public properties and a new public handler class/method were added; Program.cs was simplified to use the handler.

Possibly related PRs

  • fix invalid license paths #353: Modifies NuGetLicense's program/validation flow and license-file matching/handling through changes to Program.cs and validation integration logic.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Make command line handling testable via dependency injection' accurately and concisely describes the main change: refactoring command-line logic into separate, testable classes using dependency injection patterns.
Linked Issues check ✅ Passed PR fulfills all objectives from #411: extracts command line parameter evaluation into CommandLineOptions and LicenseValidationHandler classes, implements comprehensive tests in LicenseValidationHandlerTest, and uses TestableIO.System.IO.Abstractions for testable file access.
Out of Scope Changes check ✅ Passed All changes directly support the linked issue objectives: CommandLineOptions and LicenseValidationHandler extraction, test suite creation, TestableIO dependency, and workflow pinning for reproducibility.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@sensslen
Copy link
Copy Markdown
Owner

sensslen commented Feb 2, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.github/workflows/action.yml:
- Line 115: The workflow's action reference "uses:
josepho0918/vstest-action@78cb5377bbb68282f8130758b0d810b1bb76b7b0" points to a
non‑verifiable repository; replace it with the public Microsoft action (e.g.,
"uses: microsoft/vstest-action@v2" or an explicit, documented tag/sha) in the
action.yml, or if this is an intentional private/internal action, update the
uses entry to a reachable internal repository and add documentation in the repo
explaining why a private action is used and how access is provisioned and
verified (ensure the SHA/tag used is valid and accessible).

In `@src/NuGetLicense/LicenseValidationHandler.cs`:
- Around line 262-275: GetPackageInformations currently disposes
sourceRepositoryProvider before the returned IAsyncEnumerable from
PackageInformationReader.GetPackageInfo is enumerated, causing
ObjectDisposedException; convert GetPackageInformations into an async iterator
(make it async IAsyncEnumerable<ReferencedPackageWithContext>) and remove the
using for sourceRepositoryProvider so the provider stays alive during
enumeration, then iterate over informationReader.GetPackageInfo with await
foreach and yield return each item, disposing sourceRepositoryProvider (and any
other disposable resources like globalPackagesFolderUtility) in a finally block
after enumeration completes; reference GetPackageInformations,
sourceRepositoryProvider/WrappedSourceRepositoryProvider, and
PackageInformationReader.GetPackageInfo to locate where to apply the change.
🧹 Nitpick comments (2)
src/NuGetLicense/CommandLineOptions.cs (1)

8-28: Add XML docs for the public option properties.

The class has a summary, but the public properties should also carry <summary> (and other relevant tags) to meet API documentation requirements.
As per coding guidelines, Public APIs in C# should have XML documentation comments using <summary>, <param>, <returns>, <exception> tags as appropriate.

src/NuGetLicense/LicenseValidationHandler.cs (1)

32-60: Add XML docs for the public handler API.

Please add <summary> / <param> / <returns> / <exception> docs for the public class, constructor, and HandleAsync.
As per coding guidelines, Public APIs in C# should have XML documentation comments using <summary>, <param>, <returns>, <exception> tags as appropriate.

Comment thread .github/workflows/action.yml
Comment thread src/NuGetLicense/LicenseValidationHandler.cs Outdated
@sensslen
Copy link
Copy Markdown
Owner

sensslen commented Feb 2, 2026

@copilot In @src/NuGetLicense/LicenseValidationHandler.cs:

  • Around line 262-275: GetPackageInformations currently disposes
    sourceRepositoryProvider before the returned IAsyncEnumerable from
    PackageInformationReader.GetPackageInfo is enumerated, causing
    ObjectDisposedException; convert GetPackageInformations into an async iterator
    (make it async IAsyncEnumerable) and remove the
    using for sourceRepositoryProvider so the provider stays alive during
    enumeration, then iterate over informationReader.GetPackageInfo with await
    foreach and yield return each item, disposing sourceRepositoryProvider (and any
    other disposable resources like globalPackagesFolderUtility) in a finally block
    after enumeration completes; reference GetPackageInformations,
    sourceRepositoryProvider/WrappedSourceRepositoryProvider, and
    PackageInformationReader.GetPackageInfo to locate where to apply the change.

…o async iterator

Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Feb 2, 2026

@copilot In @src/NuGetLicense/LicenseValidationHandler.cs:

  • Around line 262-275: GetPackageInformations currently disposes
    sourceRepositoryProvider before the returned IAsyncEnumerable from
    ...

Fixed in commit 9f7a17b. Converted GetPackageInformations to an async iterator method with async IAsyncEnumerable<ReferencedPackageWithContext> return type. The method now uses await foreach to iterate over informationReader.GetPackageInfo and yield return each item. The sourceRepositoryProvider is properly disposed in a finally block after enumeration completes, ensuring it stays alive during enumeration. Added [EnumeratorCancellation] attribute to properly propagate the cancellation token.

@sensslen
Copy link
Copy Markdown
Owner

sensslen commented Feb 2, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/NuGetLicense/LicenseValidationHandler.cs`:
- Around line 244-260: In GetLicenseMatcher rename the misspelled local variable
spdxLicemseMatcher to spdxLicenseMatcher and update all references (the variable
declaration and its use in the return expression for new
FileLicenseMatcher.Combine.LicenseMatcher) so the identifier is consistent;
ensure the method returns the correctly named spdxLicenseMatcher alongside the
FileLicenseMatcher.Compare.LicenseMatcher instance.
🧹 Nitpick comments (3)
src/NuGetLicense/LicenseValidationHandler.cs (3)

32-58: Consider adding XML documentation for the public class and constructor.

Per coding guidelines, public APIs should have XML documentation comments. The LicenseValidationHandler class and its constructor are public entry points that would benefit from documentation describing their purpose and parameter expectations.

📝 Suggested documentation
 namespace NuGetLicense
 {
+    /// <summary>
+    /// Handles the license validation workflow for NuGet packages.
+    /// </summary>
     public class LicenseValidationHandler
     {
         private readonly IFileSystem _fileSystem;
         private readonly HttpClient _httpClient;
         private readonly ISolutionPersistanceWrapper _solutionPersistance;
         private readonly IMsBuildAbstraction _msBuild;
         private readonly IPackagesConfigReader _packagesConfigReader;
         private readonly Stream _outputStream;
         private readonly Stream _errorStream;

+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseValidationHandler"/> class.
+        /// </summary>
+        /// <param name="fileSystem">The file system abstraction for file operations.</param>
+        /// <param name="httpClient">The HTTP client for downloading license files.</param>
+        /// <param name="solutionPersistance">The solution persistence wrapper for reading solution files.</param>
+        /// <param name="msBuild">The MSBuild abstraction for project file operations.</param>
+        /// <param name="packagesConfigReader">The packages.config reader.</param>
+        /// <param name="outputStream">The stream for writing output.</param>
+        /// <param name="errorStream">The stream for writing errors.</param>
         public LicenseValidationHandler(

As per coding guidelines: "Public APIs in C# should have XML documentation comments using <summary>, <param>, <returns>, <exception> tags as appropriate."


200-213: Exception type may be misleading.

FileNotFoundException is thrown when neither inputFile nor inputJsonFile is provided, but the actual issue is a missing command-line argument, not a missing file. Consider using ArgumentException or InvalidOperationException for clearer semantics.

♻️ Proposed fix
-            throw new FileNotFoundException("Please provide an input file");
+            throw new ArgumentException("Please provide an input file using --input or --input-file-json");

141-198: Consider the behavior with malformed or empty JSON files.

Several methods use the null-forgiving operator (!) after JsonSerializer.Deserialize<T>(). If a JSON file contains only null or is malformed, this will result in a NullReferenceException rather than a descriptive error.

For a CLI tool, failing fast is acceptable, but you might want to provide clearer error messages for better user experience.

💡 Example improvement for one method
         private string[] GetAllowedLicenses(string? allowedLicenses)
         {
             if (allowedLicenses == null)
             {
                 return Array.Empty<string>();
             }

-            return JsonSerializer.Deserialize<string[]>(_fileSystem.File.ReadAllText(allowedLicenses))!;
+            string[] result = JsonSerializer.Deserialize<string[]>(_fileSystem.File.ReadAllText(allowedLicenses))
+                ?? throw new InvalidOperationException($"Failed to deserialize allowed licenses from '{allowedLicenses}'");
+            return result;
         }

Comment thread src/NuGetLicense/LicenseValidationHandler.cs
@sensslen sensslen force-pushed the copilot/implement-command-line-evaluation branch from 33ffc52 to 6f142a0 Compare February 2, 2026 10:42
@sensslen sensslen merged commit 5586b55 into main Feb 2, 2026
30 of 40 checks passed
@sensslen sensslen deleted the copilot/implement-command-line-evaluation branch February 2, 2026 10:43
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Feb 2, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement command line option evaluation in a testable manner

2 participants