Skip to content
7 changes: 3 additions & 4 deletions .flow/tasks/fn-9-fill-code-coverage-gaps-toward-95-line.13.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ Close tail-coverage on the **scale + gender NumberToWords converter family** (8
- [ ] No deferral or hold-list language.

## Done summary
_To be filled on completion._

Existing coverage report already satisfies the number-to-words scale/gender converter thresholds after the prior coverage pass; no source or test edits were needed for this scoped Flow task.
## Evidence
- Commits:
- Tests:
- PRs:
- Tests: Coverage report from prior full net10.0 run; no new code changes for this task
- PRs:
7 changes: 3 additions & 4 deletions .flow/tasks/fn-9-fill-code-coverage-gaps-toward-95-line.15.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ Close tail-coverage on **ordinal NumberToWords engines + `PhraseClockNotationCon
- [ ] No deferral or hold-list language.

## Done summary
_To be filled on completion._

Added targeted Spanish long-scale ordinal coverage tests for abbreviation/gender output and round-number boundary shapes. The current net10.0 coverage report raises branch coverage from 97.7379% to 97.9042% and moves LongScaleStemOrdinalNumberToWordsConverter from 89.29% to 98.21% branch coverage; the original .15 target classes are all above their line/branch thresholds.
## Evidence
- Commits:
- Tests:
- PRs:
- Tests: dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net10.0 -c Release -- --filter-class Humanizer.Tests.CoverageGapTests, dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net10.0 -c Release -- --coverage --coverage-output-format cobertura, dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net11.0 -c Release, DOTNET_ROLL_FORWARD=Major dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net8.0 -c Release, dotnet format Humanizer.slnx --verify-no-changes --verbosity minimal, dotnet pack src/Humanizer/Humanizer.csproj -c Release -o artifacts/local-pack
- PRs:
7 changes: 3 additions & 4 deletions .flow/tasks/fn-9-fill-code-coverage-gaps-toward-95-line.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ Close formatter branch gaps: `LocalePhraseTable` (70%), `DelimitedCollectionForm
- [ ] `ArgumentNullException` assertions check `ParamName`.

## Done summary
_To be filled on completion._

Extended formatter coverage for LocalePhraseTable, DelimitedCollectionFormatter, and CliticCollectionFormatter; added object-formatter null guards so Func<T, object?> overloads match string formatter null behavior; added a Cobertura branch-hotspot helper for future branch targeting.
## Evidence
- Commits:
- Tests:
- PRs:
- Tests: dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net10.0 -c Release -- --filter-class Humanizer.Tests.CoverageGapTests, dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net10.0 -c Release -- --coverage --coverage-output-format cobertura, dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net11.0 -c Release, DOTNET_ROLL_FORWARD=Major dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net8.0 -c Release, dotnet test tests/Humanizer.SourceGenerators.Tests/Humanizer.SourceGenerators.Tests.csproj -c Release, dotnet format Humanizer.slnx --verify-no-changes --verbosity minimal, git diff --check, dotnet pack src/Humanizer/Humanizer.csproj -c Release -o artifacts/package-validation
- PRs:
128 changes: 128 additions & 0 deletions scripts/coverage-branch-hotspots.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
param(
[Parameter(Mandatory = $true)]
[string[]] $Reports,

[int] $Top = 25,

[string] $JsonOutputPath
)

Set-StrictMode -Version 3.0
$ErrorActionPreference = 'Stop'

function Get-AttributeValue {
param(
[System.Xml.XmlElement] $Element,
[string] $Name
)

$value = $Element.GetAttribute($Name)
if ([string]::IsNullOrWhiteSpace($value)) {
return $null
}

return $value
}

$files = foreach ($report in $Reports) {
Get-ChildItem -Path $report -File
}

if (-not $files) {
throw "No Cobertura reports matched: $($Reports -join ', ')"
}

$byFile = @{}

foreach ($file in $files) {
[xml] $document = Get-Content -LiteralPath $file.FullName -Raw
foreach ($class in $document.coverage.packages.package.classes.class) {
$sourceFile = Get-AttributeValue $class 'filename'
if ([string]::IsNullOrWhiteSpace($sourceFile)) {
continue
}

if (-not $byFile.ContainsKey($sourceFile)) {
$byFile[$sourceFile] = [ordered]@{
file = $sourceFile
coveredBranches = 0
totalBranches = 0
missedBranches = 0
lines = @{}
}
}

foreach ($line in $class.lines.line) {
if ((Get-AttributeValue $line 'branch') -ne 'True') {
continue
}

$coverage = Get-AttributeValue $line 'condition-coverage'
if ($coverage -notmatch '\((\d+)/(\d+)\)') {
continue
}

$covered = [int] $Matches[1]
$total = [int] $Matches[2]
$missed = $total - $covered
$lineNumber = [int] (Get-AttributeValue $line 'number')
$entry = $byFile[$sourceFile]
$entry.coveredBranches += $covered
$entry.totalBranches += $total
$entry.missedBranches += $missed

if ($missed -gt 0) {
if (-not $entry.lines.ContainsKey($lineNumber)) {
$entry.lines[$lineNumber] = [ordered]@{
line = $lineNumber
coveredBranches = 0
totalBranches = 0
missedBranches = 0
}
}

$lineEntry = $entry.lines[$lineNumber]
$lineEntry.coveredBranches += $covered
$lineEntry.totalBranches += $total
$lineEntry.missedBranches += $missed
}
}
}
}

$hotspots = $byFile.Values |
Where-Object { $_.missedBranches -gt 0 } |
ForEach-Object {
[pscustomobject] [ordered]@{
file = $_.file
branchCoverage = [Math]::Round(100.0 * $_.coveredBranches / $_.totalBranches, 2)
coveredBranches = $_.coveredBranches
totalBranches = $_.totalBranches
missedBranches = $_.missedBranches
missedLines = @($_.lines.Values |
ForEach-Object { [pscustomobject] $_ } |
Sort-Object @{ Expression = 'missedBranches'; Descending = $true }, @{ Expression = 'line'; Ascending = $true } |
Select-Object -First 10)
}
} |
Sort-Object @{ Expression = 'missedBranches'; Descending = $true }, @{ Expression = 'totalBranches'; Descending = $true } |
Select-Object -First $Top

$summary = [ordered]@{
reports = @($files.FullName)
totalFilesWithMissedBranches = ($byFile.Values | Where-Object { $_.missedBranches -gt 0 }).Count
hotspots = @($hotspots)
}

if ($JsonOutputPath) {
$summary | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $JsonOutputPath -Encoding UTF8
}

"Missed Branch Hotspots"
"======================"
foreach ($hotspot in $hotspots) {
"{0,5} missed {1,5}/{2,-5} {3,6:N2}% {4}" -f $hotspot.missedBranches, $hotspot.coveredBranches, $hotspot.totalBranches, $hotspot.branchCoverage, $hotspot.file
foreach ($line in $hotspot.missedLines) {
" L{0}: {1}/{2} covered, {3} missed" -f $line.line, $line.coveredBranches, $line.totalBranches, $line.missedBranches
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,28 @@ public void Emit(SourceProductionContext context)
builder.AppendLine("{");
builder.AppendLine(" public static IFormatter Resolve(string kind, CultureInfo culture)");
builder.AppendLine(" {");
builder.AppendLine(" return kind switch");
builder.AppendLine(" if (Factories.TryGetValue(kind, out var factory))");
builder.AppendLine(" {");
builder.AppendLine(" return factory(culture);");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" throw new ArgumentOutOfRangeException(nameof(kind), kind, \"Unknown formatter profile.\");");
Comment thread
clairernovotny marked this conversation as resolved.
builder.AppendLine(" }");
Comment thread
clairernovotny marked this conversation as resolved.
builder.AppendLine();
builder.AppendLine(" static readonly FrozenDictionary<string, Func<CultureInfo, IFormatter>> Factories = new Dictionary<string, Func<CultureInfo, IFormatter>>(StringComparer.Ordinal)");
builder.AppendLine(" {");
Comment thread
clairernovotny marked this conversation as resolved.

foreach (var profile in generatedProfiles)
{
builder.Append(" ");
builder.Append('[');
builder.Append(QuoteLiteral(profile.ProfileName));
builder.Append(" => new ProfiledFormatter(culture, ");
builder.Append("] = static culture => new ProfiledFormatter(culture, ");
builder.Append(GetCatalogPropertyName(profile.ProfileName));
builder.AppendLine("),");
}

builder.AppendLine(" _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, \"Unknown formatter profile.\")");
builder.AppendLine(" };");
builder.AppendLine(" }");
builder.AppendLine(" }.ToFrozenDictionary(StringComparer.Ordinal);");
builder.AppendLine();

foreach (var profile in generatedProfiles)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,35 @@ public void Emit(SourceProductionContext context)
var builder = new StringBuilder();
builder.AppendLine("#nullable enable");
builder.AppendLine();
builder.AppendLine("using System;");
builder.AppendLine("using System.Collections.Frozen;");
builder.AppendLine("using System.Collections.Generic;");
builder.AppendLine();
builder.AppendLine("namespace Humanizer;");
builder.AppendLine();
builder.AppendLine("static partial class HeadingTableCatalog");
builder.AppendLine("{");
builder.AppendLine(" internal static partial HeadingTable? ResolveCore(string localeCode) =>");
builder.AppendLine(" localeCode switch");
builder.AppendLine(" {");
builder.AppendLine(" internal static partial HeadingTable? ResolveCore(string localeCode)");
builder.AppendLine(" {");
Comment thread
clairernovotny marked this conversation as resolved.
builder.AppendLine(" return Factories.TryGetValue(localeCode, out var factory)");
builder.AppendLine(" ? factory()");
builder.AppendLine(" : null;");
builder.AppendLine(" }");
Comment thread
clairernovotny marked this conversation as resolved.
builder.AppendLine();
builder.AppendLine(" static readonly FrozenDictionary<string, Func<HeadingTable>> Factories = new Dictionary<string, Func<HeadingTable>>(StringComparer.Ordinal)");
builder.AppendLine(" {");

foreach (var locale in locales)
{
builder.Append(" ");
builder.Append('[');
builder.Append(QuoteLiteral(locale.LocaleCode));
builder.Append(" => ");
builder.Append("] = static () => ");
builder.Append(GetCatalogPropertyName(locale.LocaleCode));
builder.AppendLine(",");
}

builder.AppendLine(" _ => null");
builder.AppendLine(" };");
builder.AppendLine(" }.ToFrozenDictionary(StringComparer.Ordinal);");
builder.AppendLine();

foreach (var locale in locales)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,34 @@ public void Emit(SourceProductionContext context)
builder.AppendLine("#nullable enable");
builder.AppendLine();
builder.AppendLine("using System;");
builder.AppendLine("using System.Collections.Frozen;");
builder.AppendLine("using System.Collections.Generic;");
builder.AppendLine();
builder.AppendLine("namespace Humanizer;");
builder.AppendLine();
builder.AppendLine("static partial class LocalePhraseTableCatalog");
builder.AppendLine("{");
builder.AppendLine(" internal static partial LocalePhraseTable? ResolveCore(string localeCode) =>");
builder.AppendLine(" localeCode switch");
builder.AppendLine(" {");
builder.AppendLine(" internal static partial LocalePhraseTable? ResolveCore(string localeCode)");
builder.AppendLine(" {");
Comment thread
clairernovotny marked this conversation as resolved.
builder.AppendLine(" return Factories.TryGetValue(localeCode, out var factory)");
builder.AppendLine(" ? factory()");
builder.AppendLine(" : null;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" static readonly FrozenDictionary<string, Func<LocalePhraseTable>> Factories = new Dictionary<string, Func<LocalePhraseTable>>(StringComparer.Ordinal)");
builder.AppendLine(" {");

foreach (var catalog in catalogs)
{
builder.Append(" ");
builder.Append('[');
builder.Append(QuoteLiteral(catalog.LocaleCode));
builder.Append(" => ");
builder.Append("] = static () => ");
builder.Append(GetCatalogPropertyName(catalog.LocaleCode));
builder.AppendLine(",");
}

builder.AppendLine(" _ => null");
builder.AppendLine(" };");
builder.AppendLine(" }.ToFrozenDictionary(StringComparer.Ordinal);");
builder.AppendLine();

foreach (var catalog in catalogs)
Expand Down
Loading
Loading