Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions docs/DEFECT-BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ This is the current ledger for defects and review findings in EncodingChecker.
It is organised by status, not discovery date, so the open work is visible in
one place. Longer evidence and history follow the ledger.

<!-- backlog-counts total=83 fixed=73 open=6 not-reproduced=1 withdrawn=1 intentional-behavior=1 decision=1 -->
<!-- backlog-counts total=84 fixed=74 open=6 not-reproduced=1 withdrawn=1 intentional-behavior=1 decision=1 -->

**Derived count: 83 findings — 73 fixed, 6 open, 1 not reproduced, 1 withdrawn,
**Derived count: 84 findings — 74 fixed, 6 open, 1 not reproduced, 1 withdrawn,
1 intentional behavior, and 1 design decision.** Recompute and check these
figures with:

Expand Down Expand Up @@ -90,6 +90,7 @@ the 2026-09-08 reformat to findings that previously had only a sentence.
| BL-37 | A contradicting source choice that was also the target was not refused | Fixed | — | — | [BL-37](#bl-37) |
| BL-38 | `-Validate utf-8` rejected ASCII files that conversion treats as already UTF-8 | Fixed | — | — | [BL-38](#bl-38) |
| BL-39 | A source chosen in a cancelled GUI review carried into the next review | Fixed | — | — | [BL-39](#bl-39) |
| BL-40 | The GUI's Validate status read the same when every file passed and when none was checked | Fixed | — | — | [BL-40](#bl-40) |
| EC-32 | A cancellation timeout could hide repeated refusals to press Cancel | Fixed | — | — | [EC-32](#ec-32) |
| EC-15 | Plans and journals showed fixed safety flags as if they recorded checks | Fixed | — | — | [EC-15](#ec-15) |
| BL-18 | BOM-less UTF-16 could be detected and converted as UTF-32 | Fixed | — | — | [BL-18](#bl-18) |
Expand Down Expand Up @@ -1422,6 +1423,24 @@ and run the same rows again, keep an earlier choice, and cover a review that wen
stale; removing the restore, clearing every choice, or restoring only on
cancellation each fails them.

### BL-40

**The GUI's Validate status now says how many files it checked.** Files that pass
are not added to the list, and the status was built from the row count, so it read
"0 files do not have the correct encoding" both when every file passed and when a
mistyped mask matched nothing. A clean result could stand in for a check that never
ran. Found by a 2026-09-20 review from the source.

The window now counts every file as the scan reports it. The status reads "No
matching files were examined", "Checked N files: all valid", or "Checked N files:
X do not have the correct encoding", adding "Y could not be read" for files the
scan could not open. Files skipped by exclusions are still reported after it, as
before. Tests feed real Validate scans into the same count: a mask that matches
nothing, an empty folder, all valid, and a mix with a file held open; three
mutations to the counting fail them. The two lines that connect the count to the
window are checked by reading, not by a test, and the GUI smoke suite does not
run Validate.

## Decisions and mistakes that must remain visible

### A known defect shipped after being reported closed
Expand Down
107 changes: 107 additions & 0 deletions sources/EncodingChecker.Tests/ValidationStatusTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Text;

namespace EncodingChecker.Tests;

/// <summary>
/// The window's Validate status tells "every file passed" apart from "no file was checked".
/// </summary>
/// <remarks>
/// Files that pass validation get no row, so the status built from the row count read
/// "0 files do not have the correct encoding" both when every file passed and when a mistyped
/// mask matched nothing. The cases feed a real Validate scan into the tally the way the window
/// does, then read the status the window shows.
/// </remarks>
public sealed class ValidationStatusTests : IDisposable
{
private readonly string _root =
Directory.CreateTempSubdirectory("ec_validate_status_").FullName;

public void Dispose()
{
try
{
Directory.Delete(_root, recursive: true);
}
catch (IOException)
{
// Best-effort cleanup.
}
}

private string Write(string name, byte[] bytes)
{
string path = Path.Combine(_root, name);
File.WriteAllBytes(path, bytes);

return path;
}

private string Validate(string mask = "*")
{
var tally = new MainForm.ValidationTally();

ScanEngine.ScanDirectory(
new ScanDirectoryOptions
{
BaseDirectory = _root,
IncludePatterns = [mask],
Action = ScanAction.Validate,
ValidCharsets = ["utf-8"],
},
entry => tally.Count(entry.Result),
CancellationToken.None);

return tally.Describe();
}

private static byte[] Utf8(string text) => new UTF8Encoding(false).GetBytes(text);

[Fact]
public void AMaskThatMatchesNothingSaysNoFileWasExamined()
{
Write("a.txt", Utf8("valid 世界\n"));

Assert.Equal("No matching files were examined", Validate(mask: "*.tx"));
}

[Fact]
public void AnEmptyFolderSaysNoFileWasExamined()
{
Assert.Equal("No matching files were examined", Validate());
}

[Fact]
public void FilesThatAllPassSayHowManyWereChecked()
{
Write("a.txt", Utf8("valid 世界\n"));
Write("b.txt", Utf8("also valid\n"));

Assert.Equal("Checked 2 files: all valid", Validate());
}

[Fact]
public void InvalidAndUnreadableFilesAreCountedSeparately()
{
Write("valid.txt", Utf8("valid 世界\n"));
Write("legacy.txt", Encoding.GetEncoding("windows-1252").GetBytes("Le café était déjà prêt"));
string locked = Write("locked.txt", Utf8("held open\n"));

string status;

using (new FileStream(locked, FileMode.Open, FileAccess.Read, FileShare.None))
status = Validate();

Assert.Equal(
"Checked 3 files: 1 do not have the correct encoding, 1 could not be read",
status);
}

[Fact]
public void InvalidFilesAloneKeepTheFamiliarWording()
{
Write("valid.txt", Utf8("valid 世界\n"));
Write("legacy.txt", Encoding.GetEncoding("windows-1252").GetBytes("Le café était déjà prêt"));

Assert.Equal("Checked 2 files: 1 do not have the correct encoding", Validate());
}
}
55 changes: 50 additions & 5 deletions sources/EncodingChecker/MainForm.Execution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ private void StartAction(CurrentAction action)
_actionCancellation = new CancellationTokenSource();

var counters = new DirectoryTraversal.TraversalCounters();
var tally = new ValidationTally();

var args = new WorkerArgs
{
Expand All @@ -85,10 +86,12 @@ private void StartAction(CurrentAction action)
FileMasks = txtFileMasks.Text,
ValidCharsets = validCharsets,
Counters = counters,
Tally = tally,
CancellationToken = _actionCancellation.Token,
};

_scanCounters = counters;
_validationTally = tally;

_actionWorker.RunWorkerAsync(args);
}
Expand Down Expand Up @@ -227,6 +230,8 @@ private static void ActionWorkerDoWork(
scanOptions,
onEntry: entry =>
{
args.Tally.Count(entry.Result);

// Hide files that already pass validation.
if (args.Action == CurrentAction.Validate &&
entry.Result == ConversionRowResult.Unchanged)
Expand Down Expand Up @@ -351,13 +356,12 @@ private void ScanWorkerCompleted(RunWorkerCompletedEventArgs e)
lstResults.Sort();
lstResults.EndUpdate();

string statusMessage = e.Cancelled
? "Cancelled - {0} files processed"
string completedStatus = e.Cancelled
? $"Cancelled - {lstResults.Items.Count} files processed"
: _currentAction == CurrentAction.View
? "{0} files processed"
: "{0} files do not have the correct encoding";
? $"{lstResults.Items.Count} files processed"
: (_validationTally ?? new ValidationTally()).Describe();

string completedStatus = string.Format(statusMessage, lstResults.Items.Count);
string coverage = FormatCoverage(_scanCounters);

UpdateControlsOnActionDone(
Expand Down Expand Up @@ -503,6 +507,47 @@ or OrchestrationOutcome.Previewed
outcome?.Outcome == OrchestrationOutcome.Interrupted));
}

/// <summary>
/// Counts what a validation examined, and says it.
/// </summary>
/// <remarks>
/// Valid files are not added as rows, so the row count alone read "0 files do not have
/// the correct encoding" both when every file passed and when no file matched at all.
/// Counted as the scan reports each file, from several threads at once.
/// </remarks>
internal sealed class ValidationTally
{
private int _examined;
private int _invalid;
private int _unreadable;

internal void Count(ConversionRowResult result)
{
Interlocked.Increment(ref _examined);

if (result == ConversionRowResult.Invalid)
Interlocked.Increment(ref _invalid);
else if (result == ConversionRowResult.Error)
Interlocked.Increment(ref _unreadable);
}

internal string Describe()
{
if (_examined == 0)
return "No matching files were examined";

if (_invalid == 0 && _unreadable == 0)
return $"Checked {_examined} files: all valid";

string status =
$"Checked {_examined} files: {_invalid} do not have the correct encoding";

return _unreadable == 0
? status
: status + $", {_unreadable} could not be read";
}
}

/// <summary>
/// Counts what a conversion did, and says it.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions sources/EncodingChecker/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private sealed class WorkerArgs
internal required string FileMasks;
internal required List<string> ValidCharsets;
internal required DirectoryTraversal.TraversalCounters Counters;
internal required ValidationTally Tally;
internal CancellationToken CancellationToken;
}

Expand Down Expand Up @@ -76,6 +77,9 @@ private enum CurrentAction
// The completed scan's coverage is shown with its result count.
private DirectoryTraversal.TraversalCounters? _scanCounters;

// What the completed validation examined, including files that passed and so have no row.
private ValidationTally? _validationTally;

// Indices into imgsResults (see SetKeyName calls in MainForm.Designer.cs).
// Reuses the existing Failed and Warning icons; Warning marks preview rows.
private const int ResultIconSuccess = 0;
Expand Down
Loading