Skip to content

Commit 4492648

Browse files
authored
Improve coverlet.MTP code coverage (coverlet-coverage#1813)
* Update coverlet package question in bug report template Clarified the question about the coverlet package in the bug report template. * revert update * Simplify GetTestAssemblyPath and add uncovered path tests Refactored CoverageConfiguration.GetTestAssemblyPath to use only the entry assembly and throw if unavailable, removing all fallback logic. Added CollectorExtensionMoreTests.cs to cover previously untested code paths in CollectorExtension, including output directory creation and report generation scenarios, using Moq and xUnit. * Refactor coverage report generation for testability Extract report file generation and display logic into separate methods. Introduce IReporterFactory for dependency injection and mocking. Add CreateDirectory to IFileSystem for better test support. Enable Moq internal type mocking via InternalsVisibleTo. Add unit tests for report file generation with multiple formats. * Add cross-platform report method tests and path helpers Added CollectorExtensionReportMethodsTests for comprehensive coverage of report generation methods, including file output, output device interaction, and path handling. Refactored CollectorExtensionUncoveredPathsTests to use a platform-agnostic CreatePlatformPath helper, replacing hardcoded paths for improved cross-platform reliability. Updated all simulated file paths to use platform-specific construction. * Remove unused helper methods from test class Removed CreateMockServiceProvider and CreateMockReporterFactory from CollectorExtensionReportMethodsTests. These methods were responsible for setting up mock dependencies for report generation but are no longer used. No other changes were made to the test class. * Make FileSystem.CreateDirectory virtual for better testability Refactored FileSystem to allow mocking CreateDirectory in tests. Updated unit tests to mock directory creation and removed tests that relied on unmockable static methods. Improved mocking of output device in report generation tests and removed redundant multi-format report tests. Enhances test isolation and coverage.
1 parent 2e1e7da commit 4492648

File tree

9 files changed

+1031
-67
lines changed

9 files changed

+1031
-67
lines changed

src/coverlet.MTP/Collector/CollectorExtension.cs

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using Coverlet.Core;
99
using Coverlet.Core.Abstractions;
1010
using Coverlet.Core.Helpers;
11-
using Coverlet.Core.Reporters;
1211
using Coverlet.Core.Symbols;
1312
using Coverlet.MTP.CommandLine;
1413
using Coverlet.MTP.Configuration;
@@ -45,6 +44,7 @@ internal sealed class CollectorExtension : ITestHostProcessLifetimeHandler, ITes
4544
private string? _coverageIdentifier;
4645

4746
private readonly CoverletExtension _extension = new();
47+
private readonly IReporterFactory _reporterFactory;
4848

4949
string IExtension.Uid => _extension.Uid;
5050
string IExtension.Version => _extension.Version;
@@ -56,13 +56,15 @@ public CollectorExtension(
5656
Microsoft.Testing.Platform.CommandLine.ICommandLineOptions commandLineOptions,
5757
Microsoft.Testing.Platform.OutputDevice.IOutputDevice? outputDevice,
5858
Microsoft.Testing.Platform.Configurations.IConfiguration? configuration,
59-
IFileSystem? fileSystem = null) // Add optional parameter for backward compatibility
59+
IFileSystem? fileSystem = null,
60+
IReporterFactory? reporterFactory = null)
6061
{
6162
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
6263
_commandLineOptions = commandLineOptions ?? throw new ArgumentNullException(nameof(commandLineOptions));
6364
_platformConfiguration = configuration ?? throw new ArgumentNullException(nameof(configuration));
6465
_outputDisplay = outputDevice ?? throw new ArgumentNullException(nameof(outputDevice));
6566
_fileSystem = fileSystem ?? new FileSystem(); // Use provided or create default
67+
_reporterFactory = reporterFactory ?? new DefaultReporterFactory();
6668
_configuration = new CoverletExtensionConfiguration();
6769
_logger = new CoverletLoggerAdapter(_loggerFactory);
6870

@@ -324,27 +326,26 @@ private void InitializeCoverage()
324326
cecilSymbolHelper);
325327
}
326328

327-
private async Task GenerateReportsAsync(CoverageResult result, CancellationToken cancellation)
328-
{
329-
string outputDirectory = _platformConfiguration!.GetTestResultDirectory() ??
330-
Path.GetDirectoryName(_testModulePath) + Path.DirectorySeparatorChar;
331-
332-
_logger.LogVerbose($"Coverage output directory: {outputDirectory}");
333-
334-
// Ensure directory exists
335-
if (!_fileSystem.Exists(outputDirectory))
336-
{
337-
Directory.CreateDirectory(outputDirectory);
338-
}
339-
340-
ISourceRootTranslator sourceRootTranslator = _serviceProvider!.GetRequiredService<ISourceRootTranslator>();
341-
IFileSystem fileSystem = _serviceProvider!.GetRequiredService<IFileSystem>();
329+
// Add internal property for testing
330+
internal IReporterFactory? ReporterFactoryOverride { get; set; }
342331

332+
/// <summary>
333+
/// Generates coverage report files. Separated for testability.
334+
/// </summary>
335+
internal List<string> GenerateCoverageReportFiles(
336+
CoverageResult result,
337+
ISourceRootTranslator sourceRootTranslator,
338+
IFileSystem fileSystem,
339+
string outputDirectory,
340+
string[] formats)
341+
{
343342
var generatedReports = new List<string>();
344343

345-
foreach (string format in _configuration.formats)
344+
foreach (string format in formats)
346345
{
347-
IReporter reporter = new ReporterFactory(format).CreateReporter() ??
346+
// Use override if set (for testing), otherwise use injected factory
347+
IReporterFactory factory = ReporterFactoryOverride ?? _reporterFactory;
348+
IReporter reporter = factory.CreateReporter(format) ??
348349
throw new InvalidOperationException($"Specified output format '{format}' is not supported");
349350

350351
if (reporter.OutputType == ReporterOutputType.Console)
@@ -355,34 +356,71 @@ private async Task GenerateReportsAsync(CoverageResult result, CancellationToken
355356
{
356357
string filename = $"coverage.{reporter.Extension}";
357358
string reportPath = Path.Combine(outputDirectory, filename);
358-
await Task.Run(() => fileSystem.WriteAllText(reportPath, reporter.Report(result, sourceRootTranslator)), cancellation);
359+
fileSystem.WriteAllText(reportPath, reporter.Report(result, sourceRootTranslator));
359360
generatedReports.Add(reportPath);
360361
}
361362
}
362363

363-
// Output successfully generated reports to console
364-
if (generatedReports.Count > 0)
364+
return generatedReports;
365+
}
366+
367+
/// <summary>
368+
/// Displays generated report paths to output device.
369+
/// </summary>
370+
private async Task DisplayGeneratedReportsAsync(List<string> generatedReports, CancellationToken cancellation)
371+
{
372+
if (generatedReports.Count == 0)
365373
{
366-
_logger.LogInformation("Coverage reports generated:", important: true);
367-
foreach (string reportPath in generatedReports)
368-
{
369-
_logger.LogInformation($" {reportPath}", important: true);
370-
}
371-
// Output successfully generated reports to console (like TRX report extension does)
372-
// Use FormattedTextOutputDeviceData for proper display like TRX extension
373-
var outputBuilder = new StringBuilder();
374-
outputBuilder.AppendLine();
375-
outputBuilder.AppendLine(" Out of process file artifacts produced:");
376-
foreach (string reportPath in generatedReports)
377-
{
378-
outputBuilder.AppendLine($" - {reportPath}");
379-
}
374+
return;
375+
}
376+
377+
_logger.LogInformation("Coverage reports generated:", important: true);
378+
foreach (string reportPath in generatedReports)
379+
{
380+
_logger.LogInformation($" {reportPath}", important: true);
381+
}
382+
383+
var outputBuilder = new StringBuilder();
384+
outputBuilder.AppendLine();
385+
outputBuilder.AppendLine(" Out of process file artifacts produced:");
386+
foreach (string reportPath in generatedReports)
387+
{
388+
outputBuilder.AppendLine($" - {reportPath}");
389+
}
390+
391+
await _outputDisplay.DisplayAsync(
392+
this,
393+
new TextOutputDeviceData(outputBuilder.ToString()),
394+
cancellation).ConfigureAwait(false);
395+
}
396+
397+
// Refactor GenerateReportsAsync to use extracted methods
398+
private async Task GenerateReportsAsync(CoverageResult result, CancellationToken cancellation)
399+
{
400+
string outputDirectory = _platformConfiguration!.GetTestResultDirectory() ??
401+
Path.GetDirectoryName(_testModulePath) + Path.DirectorySeparatorChar;
402+
403+
_logger.LogVerbose($"Coverage output directory: {outputDirectory}");
380404

381-
await _outputDisplay.DisplayAsync(
382-
this,
383-
new TextOutputDeviceData(outputBuilder.ToString()),
384-
cancellation).ConfigureAwait(false);
405+
// Ensure directory exists
406+
if (!_fileSystem.Exists(outputDirectory))
407+
{
408+
_fileSystem.CreateDirectory(outputDirectory);
385409
}
410+
411+
ISourceRootTranslator sourceRootTranslator = _serviceProvider!.GetRequiredService<ISourceRootTranslator>();
412+
IFileSystem fileSystem = _serviceProvider!.GetRequiredService<IFileSystem>();
413+
414+
// Generate reports (now testable separately)
415+
List<string> generatedReports = GenerateCoverageReportFiles(
416+
result,
417+
sourceRootTranslator,
418+
fileSystem,
419+
outputDirectory,
420+
_configuration.formats);
421+
422+
// Display results
423+
await DisplayGeneratedReportsAsync(generatedReports, cancellation);
386424
}
387425

388426
private string GetHitsFilePath()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Toni Solarin-Sodara
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Coverlet.Core.Abstractions;
5+
using Coverlet.Core.Reporters;
6+
7+
namespace Coverlet.MTP.Collector;
8+
9+
/// <summary>
10+
/// Factory abstraction for creating reporters. Enables testing.
11+
/// </summary>
12+
internal interface IReporterFactory
13+
{
14+
IReporter? CreateReporter(string format);
15+
}
16+
17+
/// <summary>
18+
/// Default implementation that delegates to Coverlet's ReporterFactory.
19+
/// </summary>
20+
internal sealed class DefaultReporterFactory : IReporterFactory
21+
{
22+
public IReporter? CreateReporter(string format)
23+
{
24+
return new ReporterFactory(format).CreateReporter();
25+
}
26+
}

src/coverlet.MTP/Configuration/CoverageConfiguration.cs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,38 +158,17 @@ public string[] GetDoesNotReturnAttributes()
158158
}
159159

160160
/// <summary>
161-
/// Gets the test assembly path using multiple fallback strategies.
161+
/// Gets the test assembly path.
162162
/// </summary>
163163
public static string GetTestAssemblyPath()
164164
{
165-
// Try multiple methods to get the test assembly path
166-
// 1. Entry assembly (most reliable for test scenarios)
165+
// In test scenarios, the entry assembly is always the test assembly
167166
string? path = System.Reflection.Assembly.GetEntryAssembly()?.Location;
168-
if (!string.IsNullOrEmpty(path) && File.Exists(path))
169-
{
170-
return path!;
171-
}
172-
173-
// 2. Current process main module
174-
path = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
175-
if (!string.IsNullOrEmpty(path) && File.Exists(path))
176-
{
177-
return path!;
178-
}
179-
180-
// 3. Base directory + first command line argument
181-
string[] args = Environment.GetCommandLineArgs();
182-
if (args.Length > 0)
183-
{
184-
string fullPath = Path.GetFullPath(args[0]);
185-
if (File.Exists(fullPath))
186-
{
187-
return fullPath;
188-
}
189-
}
190167

191-
// 4. Fallback to base directory
192-
return AppContext.BaseDirectory;
168+
return string.IsNullOrEmpty(path)
169+
? throw new InvalidOperationException(
170+
"Unable to determine test assembly path. Entry assembly location is not available.")
171+
: path!;
193172
}
194173

195174
/// <summary>

src/coverlet.MTP/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88

99
[assembly: InternalsVisibleTo("coverlet.MTP.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd2d5ae566fc3eb2d1837ec8706f78606b38d3bef1cdd80fc0c5f82cf9f2f221c60424274cbc70850c71e67f8695a74d26aad15395fa7473ecaa07c3048f32f5b83003eb9a927d9b0b688ebc423954dd883800b63a51091e355d1758d6360f7085a34606a18236a85f085cad80ec388ac340bb22770c3a01f39fcd7fa4d53cc6")]
1010
[assembly: InternalsVisibleTo("coverlet.MTP.validation.tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001001575e172562f7b3ca4e105ef1539802ddf92de5f3d3a76937f99c73b1c64f46ec6ed1c5de46f2d39bc8916050376f749507bb5082958890e6e3ba9c68b4c6e4a56f16c1401f21f908c437a2b0b4dc263ef3bc1bc15d6a02a8e6cbf26a077f1590f91e248cf5ccd4642b7493b31cfa2bd9f921b662cd2cb8bcc66fc2cb533e2a8")]
11+
// Needed to mock internal type https://github.com/Moq/moq4/wiki/Quickstart#advanced-features
12+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

src/coverlet.core/Abstractions/IFileSystem.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Coverlet.Core.Abstractions
77
{
88
internal interface IFileSystem
99
{
10+
void CreateDirectory(string path); // Add this method
1011
bool Exists(string path);
1112

1213
void WriteAllText(string path, string contents);

src/coverlet.core/Helpers/FileSystem.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ namespace Coverlet.Core.Helpers
88
{
99
internal class FileSystem : IFileSystem
1010
{
11+
12+
// We need to partial mock this method on tests
13+
public virtual void CreateDirectory(string path)
14+
{
15+
Directory.CreateDirectory(path);
16+
}
17+
1118
// We need to partial mock this method on tests
1219
public virtual bool Exists(string path)
1320
{

0 commit comments

Comments
 (0)