save progress
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// <copyright file="BranchCoverageEnforcer.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
// Task: CCUT-014
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Testing.Coverage;
|
||||
|
||||
/// <summary>
|
||||
/// Enforces minimum branch coverage and detects dead paths.
|
||||
/// </summary>
|
||||
public sealed class BranchCoverageEnforcer
|
||||
{
|
||||
private readonly CoverageReport _report;
|
||||
private readonly BranchCoverageConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BranchCoverageEnforcer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="report">Coverage report to analyze.</param>
|
||||
/// <param name="config">Enforcement configuration.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public BranchCoverageEnforcer(
|
||||
CoverageReport report,
|
||||
BranchCoverageConfig? config = null,
|
||||
ILogger? logger = null)
|
||||
{
|
||||
_report = report;
|
||||
_config = config ?? new BranchCoverageConfig();
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify branch coverage meets minimum threshold.
|
||||
/// </summary>
|
||||
/// <returns>Validation result.</returns>
|
||||
public CoverageValidationResult Validate()
|
||||
{
|
||||
var violations = new List<CoverageViolation>();
|
||||
|
||||
foreach (var file in _report.Files)
|
||||
{
|
||||
// Skip excluded files
|
||||
if (IsExcluded(file.Path))
|
||||
{
|
||||
_logger.LogDebug("Skipping excluded file: {Path}", file.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check file-level coverage
|
||||
if (file.BranchCoverage < _config.MinBranchCoverage)
|
||||
{
|
||||
var uncoveredLines = GetUncoveredBranches(file);
|
||||
|
||||
violations.Add(new CoverageViolation(
|
||||
FilePath: file.Path,
|
||||
Type: ViolationType.InsufficientCoverage,
|
||||
ActualCoverage: file.BranchCoverage,
|
||||
RequiredCoverage: _config.MinBranchCoverage,
|
||||
UncoveredBranches: uncoveredLines));
|
||||
|
||||
_logger.LogWarning(
|
||||
"Insufficient coverage in {Path}: {Actual:P1} < {Required:P1}",
|
||||
file.Path, file.BranchCoverage, _config.MinBranchCoverage);
|
||||
}
|
||||
|
||||
// Detect completely uncovered branches (dead paths)
|
||||
if (_config.FailOnDeadPaths)
|
||||
{
|
||||
var deadPaths = file.Branches
|
||||
.Where(b => b.HitCount == 0 && !IsExempt(file.Path, b.Line))
|
||||
.ToList();
|
||||
|
||||
if (deadPaths.Count > 0)
|
||||
{
|
||||
violations.Add(new CoverageViolation(
|
||||
FilePath: file.Path,
|
||||
Type: ViolationType.DeadPath,
|
||||
ActualCoverage: file.BranchCoverage,
|
||||
RequiredCoverage: _config.MinBranchCoverage,
|
||||
UncoveredBranches: [.. deadPaths.Select(b => b.Line)]));
|
||||
|
||||
_logger.LogWarning(
|
||||
"Dead paths found in {Path}: {Count} uncovered branches",
|
||||
file.Path, deadPaths.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CoverageValidationResult(
|
||||
IsValid: violations.Count == 0,
|
||||
Violations: [.. violations],
|
||||
OverallBranchCoverage: _report.OverallBranchCoverage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate report of dead paths for review.
|
||||
/// </summary>
|
||||
/// <returns>Dead path report.</returns>
|
||||
public DeadPathReport GenerateDeadPathReport()
|
||||
{
|
||||
var deadPaths = new List<DeadPathEntry>();
|
||||
|
||||
foreach (var file in _report.Files)
|
||||
{
|
||||
if (IsExcluded(file.Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var branch in file.Branches.Where(b => b.HitCount == 0))
|
||||
{
|
||||
var isExempt = IsExempt(file.Path, branch.Line);
|
||||
var exemptionReason = isExempt ? GetExemptionReason(file.Path, branch.Line) : null;
|
||||
|
||||
deadPaths.Add(new DeadPathEntry(
|
||||
FilePath: file.Path,
|
||||
Line: branch.Line,
|
||||
BranchType: branch.Type,
|
||||
IsExempt: isExempt,
|
||||
ExemptionReason: exemptionReason));
|
||||
}
|
||||
}
|
||||
|
||||
return new DeadPathReport(
|
||||
TotalDeadPaths: deadPaths.Count,
|
||||
ExemptDeadPaths: deadPaths.Count(p => p.IsExempt),
|
||||
ActiveDeadPaths: deadPaths.Count(p => !p.IsExempt),
|
||||
Entries: [.. deadPaths]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a summary of coverage by directory.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of directory to coverage percentage.</returns>
|
||||
public IReadOnlyDictionary<string, decimal> GetCoverageByDirectory()
|
||||
{
|
||||
var byDirectory = new Dictionary<string, List<decimal>>();
|
||||
|
||||
foreach (var file in _report.Files)
|
||||
{
|
||||
if (IsExcluded(file.Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(file.Path) ?? ".";
|
||||
|
||||
if (!byDirectory.TryGetValue(directory, out var coverages))
|
||||
{
|
||||
coverages = [];
|
||||
byDirectory[directory] = coverages;
|
||||
}
|
||||
|
||||
coverages.Add(file.BranchCoverage);
|
||||
}
|
||||
|
||||
return byDirectory.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value.Count > 0 ? kvp.Value.Average() : 0m);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get files below minimum coverage threshold.
|
||||
/// </summary>
|
||||
/// <returns>List of files below threshold.</returns>
|
||||
public IReadOnlyList<FileCoverage> GetFilesBelowThreshold()
|
||||
{
|
||||
return _report.Files
|
||||
.Where(f => !IsExcluded(f.Path) && f.BranchCoverage < _config.MinBranchCoverage)
|
||||
.OrderBy(f => f.BranchCoverage)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private ImmutableArray<int> GetUncoveredBranches(FileCoverage file)
|
||||
{
|
||||
return [.. file.Branches
|
||||
.Where(b => b.HitCount == 0)
|
||||
.Select(b => b.Line)
|
||||
.Distinct()
|
||||
.OrderBy(l => l)];
|
||||
}
|
||||
|
||||
private bool IsExcluded(string filePath)
|
||||
{
|
||||
return _config.ExcludePatterns.Any(p => p.IsMatch(filePath));
|
||||
}
|
||||
|
||||
private bool IsExempt(string filePath, int line)
|
||||
{
|
||||
return _config.Exemptions.Any(e =>
|
||||
e.FilePattern.IsMatch(filePath) &&
|
||||
(e.Lines.IsDefaultOrEmpty || e.Lines.Contains(line)));
|
||||
}
|
||||
|
||||
private string? GetExemptionReason(string filePath, int line)
|
||||
{
|
||||
var exemption = _config.Exemptions.FirstOrDefault(e =>
|
||||
e.FilePattern.IsMatch(filePath) &&
|
||||
(e.Lines.IsDefaultOrEmpty || e.Lines.Contains(line)));
|
||||
|
||||
return exemption?.Reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// <copyright file="CoberturaParser.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
// Task: CCUT-014
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace StellaOps.Testing.Coverage;
|
||||
|
||||
/// <summary>
|
||||
/// Parses Cobertura XML coverage reports.
|
||||
/// </summary>
|
||||
public static class CoberturaParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse a Cobertura XML file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to Cobertura XML file.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Parsed coverage report.</returns>
|
||||
public static async Task<CoverageReport> ParseFileAsync(string filePath, CancellationToken ct = default)
|
||||
{
|
||||
var xml = await File.ReadAllTextAsync(filePath, ct);
|
||||
return Parse(xml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Cobertura XML string.
|
||||
/// </summary>
|
||||
/// <param name="xml">Cobertura XML content.</param>
|
||||
/// <returns>Parsed coverage report.</returns>
|
||||
public static CoverageReport Parse(string xml)
|
||||
{
|
||||
var doc = XDocument.Parse(xml);
|
||||
var coverage = doc.Root ?? throw new InvalidOperationException("Invalid Cobertura XML: no root element");
|
||||
|
||||
var files = new List<FileCoverage>();
|
||||
|
||||
// Parse overall coverage
|
||||
var lineCoverage = ParseDecimal(coverage.Attribute("line-rate")?.Value ?? "0");
|
||||
var branchCoverage = ParseDecimal(coverage.Attribute("branch-rate")?.Value ?? "0");
|
||||
|
||||
// Parse timestamp
|
||||
var timestamp = coverage.Attribute("timestamp")?.Value;
|
||||
var generatedAt = timestamp != null
|
||||
? DateTimeOffset.FromUnixTimeSeconds(long.Parse(timestamp, CultureInfo.InvariantCulture))
|
||||
: DateTimeOffset.UtcNow;
|
||||
|
||||
// Parse packages -> classes -> files
|
||||
foreach (var package in coverage.Descendants("package"))
|
||||
{
|
||||
foreach (var cls in package.Descendants("class"))
|
||||
{
|
||||
var fileCoverage = ParseClass(cls);
|
||||
if (fileCoverage != null)
|
||||
{
|
||||
files.Add(fileCoverage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CoverageReport(
|
||||
Files: [.. files],
|
||||
OverallLineCoverage: lineCoverage,
|
||||
OverallBranchCoverage: branchCoverage,
|
||||
GeneratedAt: generatedAt);
|
||||
}
|
||||
|
||||
private static FileCoverage? ParseClass(XElement cls)
|
||||
{
|
||||
var filename = cls.Attribute("filename")?.Value;
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lineCoverage = ParseDecimal(cls.Attribute("line-rate")?.Value ?? "0");
|
||||
var branchCoverage = ParseDecimal(cls.Attribute("branch-rate")?.Value ?? "0");
|
||||
|
||||
var lines = new List<LineCoverageData>();
|
||||
var branches = new List<BranchCoverageData>();
|
||||
|
||||
var linesElement = cls.Element("lines");
|
||||
if (linesElement != null)
|
||||
{
|
||||
foreach (var line in linesElement.Elements("line"))
|
||||
{
|
||||
var lineNumber = int.Parse(line.Attribute("number")?.Value ?? "0", CultureInfo.InvariantCulture);
|
||||
var hits = int.Parse(line.Attribute("hits")?.Value ?? "0", CultureInfo.InvariantCulture);
|
||||
var isBranch = line.Attribute("branch")?.Value == "true";
|
||||
|
||||
lines.Add(new LineCoverageData(
|
||||
LineNumber: lineNumber,
|
||||
HitCount: hits,
|
||||
IsCoverable: true));
|
||||
|
||||
// Parse branch conditions if present
|
||||
if (isBranch)
|
||||
{
|
||||
var conditionCoverage = line.Attribute("condition-coverage")?.Value;
|
||||
var conditions = line.Element("conditions");
|
||||
|
||||
if (conditions != null)
|
||||
{
|
||||
var branchIndex = 0;
|
||||
foreach (var condition in conditions.Elements("condition"))
|
||||
{
|
||||
var coverage = int.Parse(
|
||||
condition.Attribute("coverage")?.Value ?? "0",
|
||||
CultureInfo.InvariantCulture);
|
||||
|
||||
branches.Add(new BranchCoverageData(
|
||||
Line: lineNumber,
|
||||
BranchId: $"{lineNumber}-{branchIndex}",
|
||||
Type: condition.Attribute("type")?.Value ?? "branch",
|
||||
HitCount: coverage > 0 ? 1 : 0));
|
||||
|
||||
branchIndex++;
|
||||
}
|
||||
}
|
||||
else if (conditionCoverage != null)
|
||||
{
|
||||
// Parse condition-coverage like "50% (1/2)"
|
||||
var parts = conditionCoverage.Split(['(', '/', ')'], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
var covered = int.Parse(parts[0].TrimEnd('%'), CultureInfo.InvariantCulture);
|
||||
var total = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
branches.Add(new BranchCoverageData(
|
||||
Line: lineNumber,
|
||||
BranchId: $"{lineNumber}-{i}",
|
||||
Type: "branch",
|
||||
HitCount: i < (covered * total / 100) ? 1 : 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new FileCoverage(
|
||||
Path: filename,
|
||||
LineCoverage: lineCoverage,
|
||||
BranchCoverage: branchCoverage,
|
||||
Lines: [.. lines],
|
||||
Branches: [.. branches]);
|
||||
}
|
||||
|
||||
private static decimal ParseDecimal(string value)
|
||||
{
|
||||
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0m;
|
||||
}
|
||||
}
|
||||
181
src/__Tests/__Libraries/StellaOps.Testing.Coverage/Models.cs
Normal file
181
src/__Tests/__Libraries/StellaOps.Testing.Coverage/Models.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
// <copyright file="Models.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
// Sprint: SPRINT_20260105_002_005_TEST_cross_cutting
|
||||
// Task: CCUT-013, CCUT-014
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Testing.Coverage;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage report for analysis.
|
||||
/// </summary>
|
||||
/// <param name="Files">Files with coverage data.</param>
|
||||
/// <param name="OverallLineCoverage">Overall line coverage percentage.</param>
|
||||
/// <param name="OverallBranchCoverage">Overall branch coverage percentage.</param>
|
||||
/// <param name="GeneratedAt">When the report was generated.</param>
|
||||
public sealed record CoverageReport(
|
||||
ImmutableArray<FileCoverage> Files,
|
||||
decimal OverallLineCoverage,
|
||||
decimal OverallBranchCoverage,
|
||||
DateTimeOffset GeneratedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Coverage data for a single file.
|
||||
/// </summary>
|
||||
/// <param name="Path">File path.</param>
|
||||
/// <param name="LineCoverage">Line coverage percentage (0-1).</param>
|
||||
/// <param name="BranchCoverage">Branch coverage percentage (0-1).</param>
|
||||
/// <param name="Lines">Individual line coverage data.</param>
|
||||
/// <param name="Branches">Individual branch coverage data.</param>
|
||||
public sealed record FileCoverage(
|
||||
string Path,
|
||||
decimal LineCoverage,
|
||||
decimal BranchCoverage,
|
||||
ImmutableArray<LineCoverageData> Lines,
|
||||
ImmutableArray<BranchCoverageData> Branches);
|
||||
|
||||
/// <summary>
|
||||
/// Coverage data for a single line.
|
||||
/// </summary>
|
||||
/// <param name="LineNumber">Line number.</param>
|
||||
/// <param name="HitCount">Number of times line was executed.</param>
|
||||
/// <param name="IsCoverable">Whether line is coverable.</param>
|
||||
public sealed record LineCoverageData(
|
||||
int LineNumber,
|
||||
int HitCount,
|
||||
bool IsCoverable);
|
||||
|
||||
/// <summary>
|
||||
/// Coverage data for a single branch.
|
||||
/// </summary>
|
||||
/// <param name="Line">Line number where branch occurs.</param>
|
||||
/// <param name="BranchId">Branch identifier.</param>
|
||||
/// <param name="Type">Type of branch (if/else, switch, etc.).</param>
|
||||
/// <param name="HitCount">Number of times branch was taken.</param>
|
||||
public sealed record BranchCoverageData(
|
||||
int Line,
|
||||
string BranchId,
|
||||
string Type,
|
||||
int HitCount);
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for branch coverage enforcement.
|
||||
/// </summary>
|
||||
/// <param name="MinBranchCoverage">Minimum required branch coverage (0-1).</param>
|
||||
/// <param name="FailOnDeadPaths">Whether to fail on dead paths.</param>
|
||||
/// <param name="Exemptions">Coverage exemptions.</param>
|
||||
/// <param name="ExcludePatterns">File patterns to exclude from coverage analysis.</param>
|
||||
public sealed record BranchCoverageConfig(
|
||||
decimal MinBranchCoverage = 0.80m,
|
||||
bool FailOnDeadPaths = true,
|
||||
ImmutableArray<CoverageExemption> Exemptions = default,
|
||||
ImmutableArray<Regex> ExcludePatterns = default)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets exemptions with default empty array.
|
||||
/// </summary>
|
||||
public ImmutableArray<CoverageExemption> Exemptions { get; init; } =
|
||||
Exemptions.IsDefault ? [] : Exemptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets exclude patterns with default empty array.
|
||||
/// </summary>
|
||||
public ImmutableArray<Regex> ExcludePatterns { get; init; } =
|
||||
ExcludePatterns.IsDefault ? GetDefaultExcludePatterns() : ExcludePatterns;
|
||||
|
||||
private static ImmutableArray<Regex> GetDefaultExcludePatterns()
|
||||
{
|
||||
return
|
||||
[
|
||||
new Regex(@"\.Tests\.cs$", RegexOptions.Compiled),
|
||||
new Regex(@"\.Generated\.cs$", RegexOptions.Compiled),
|
||||
new Regex(@"[\\/]obj[\\/]", RegexOptions.Compiled),
|
||||
new Regex(@"[\\/]bin[\\/]", RegexOptions.Compiled),
|
||||
new Regex(@"GlobalUsings\.cs$", RegexOptions.Compiled)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A coverage exemption.
|
||||
/// </summary>
|
||||
/// <param name="FilePattern">Regex pattern matching file paths.</param>
|
||||
/// <param name="Lines">Specific lines exempt (empty for all lines).</param>
|
||||
/// <param name="Reason">Reason for exemption.</param>
|
||||
public sealed record CoverageExemption(
|
||||
Regex FilePattern,
|
||||
ImmutableArray<int> Lines,
|
||||
string Reason);
|
||||
|
||||
/// <summary>
|
||||
/// Result of coverage validation.
|
||||
/// </summary>
|
||||
/// <param name="IsValid">Whether validation passed.</param>
|
||||
/// <param name="Violations">List of violations found.</param>
|
||||
/// <param name="OverallBranchCoverage">Overall branch coverage.</param>
|
||||
public sealed record CoverageValidationResult(
|
||||
bool IsValid,
|
||||
ImmutableArray<CoverageViolation> Violations,
|
||||
decimal OverallBranchCoverage);
|
||||
|
||||
/// <summary>
|
||||
/// A coverage violation.
|
||||
/// </summary>
|
||||
/// <param name="FilePath">File with violation.</param>
|
||||
/// <param name="Type">Type of violation.</param>
|
||||
/// <param name="ActualCoverage">Actual coverage percentage.</param>
|
||||
/// <param name="RequiredCoverage">Required coverage percentage.</param>
|
||||
/// <param name="UncoveredBranches">Lines with uncovered branches.</param>
|
||||
public sealed record CoverageViolation(
|
||||
string FilePath,
|
||||
ViolationType Type,
|
||||
decimal ActualCoverage,
|
||||
decimal RequiredCoverage,
|
||||
ImmutableArray<int> UncoveredBranches);
|
||||
|
||||
/// <summary>
|
||||
/// Type of coverage violation.
|
||||
/// </summary>
|
||||
public enum ViolationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Coverage below minimum threshold.
|
||||
/// </summary>
|
||||
InsufficientCoverage,
|
||||
|
||||
/// <summary>
|
||||
/// Dead path detected (branch never taken).
|
||||
/// </summary>
|
||||
DeadPath
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A dead path entry.
|
||||
/// </summary>
|
||||
/// <param name="FilePath">File containing dead path.</param>
|
||||
/// <param name="Line">Line number.</param>
|
||||
/// <param name="BranchType">Type of branch.</param>
|
||||
/// <param name="IsExempt">Whether this path is exempt.</param>
|
||||
/// <param name="ExemptionReason">Reason for exemption if applicable.</param>
|
||||
public sealed record DeadPathEntry(
|
||||
string FilePath,
|
||||
int Line,
|
||||
string BranchType,
|
||||
bool IsExempt,
|
||||
string? ExemptionReason);
|
||||
|
||||
/// <summary>
|
||||
/// Report of dead paths found in codebase.
|
||||
/// </summary>
|
||||
/// <param name="TotalDeadPaths">Total number of dead paths.</param>
|
||||
/// <param name="ExemptDeadPaths">Number of exempt dead paths.</param>
|
||||
/// <param name="ActiveDeadPaths">Number of active (non-exempt) dead paths.</param>
|
||||
/// <param name="Entries">Individual dead path entries.</param>
|
||||
public sealed record DeadPathReport(
|
||||
int TotalDeadPaths,
|
||||
int ExemptDeadPaths,
|
||||
int ActiveDeadPaths,
|
||||
ImmutableArray<DeadPathEntry> Entries);
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseAppHost>true</UseAppHost>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Description>Branch coverage enforcement and dead-path detection framework</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="StellaOps.Testing.Coverage.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="xunit.v3.assert" PrivateAssets="all" />
|
||||
<PackageReference Include="xunit.v3.core" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user