From c97ba8a7ae4d5837f082af902602f26ea1eda175 Mon Sep 17 00:00:00 2001 From: amrali-eg <32075105+amrali-eg@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:18:28 +0300 Subject: [PATCH] Show each GUI row's current reason and clear stale icons Rows survive between runs. A file that failed or was previewed as "would convert" and was then left unchanged or skipped kept the earlier icon, and the reason a file failed, was refused or was left unchanged went only to debug output. A row left unchanged or skipped now has its icon cleared, and every row shows its current reason as a tooltip, set on each run and emptied when the run gives none. No column was added; exported formats are unchanged. Tests cover cleared icons after a failed and a previewed run, the tooltip following each result, and a real window showing a scanned row's reason; four mutations fail them. Backlog: BL-41. Co-Authored-By: Claude Opus 5.5 --- docs/DEFECT-BACKLOG.md | 21 +++++- .../PreviewResultPresentationTests.cs | 71 +++++++++++++++++++ sources/EncodingChecker/MainForm.Designer.cs | 1 + sources/EncodingChecker/MainForm.Execution.cs | 16 +++-- sources/EncodingChecker/MainForm.cs | 3 + 5 files changed, 105 insertions(+), 7 deletions(-) diff --git a/docs/DEFECT-BACKLOG.md b/docs/DEFECT-BACKLOG.md index afaa25c..7a5e2ec 100644 --- a/docs/DEFECT-BACKLOG.md +++ b/docs/DEFECT-BACKLOG.md @@ -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. - + -**Derived count: 84 findings — 74 fixed, 6 open, 1 not reproduced, 1 withdrawn, +**Derived count: 85 findings — 75 fixed, 6 open, 1 not reproduced, 1 withdrawn, 1 intentional behavior, and 1 design decision.** Recompute and check these figures with: @@ -91,6 +91,7 @@ the 2026-09-08 reformat to findings that previously had only a sentence. | 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) | +| BL-41 | GUI rows kept an earlier run's icon and never showed why a file failed | Fixed | — | — | [BL-41](#bl-41) | | 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) | @@ -1441,6 +1442,22 @@ 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. +### BL-41 + +**GUI rows now show this run's state and reason.** Rows survive between runs. A +file that failed or was previewed as "would convert" in one run and was then left +unchanged or skipped in the next kept the earlier run's icon, so the list described +a run that was over. The reason a file failed, was refused or was left unchanged +went only to the debug output, so the window never said why; the CSV export was the +only place to read it. Found by the 2026-09-20 review from the source. + +A row left unchanged or skipped now has its icon cleared. Every row shows its +current reason as a tooltip, set on each run and emptied when the run gives none, +for example "ASCII is already valid UTF-8 without a BOM." on a plain-English file +converted to UTF-8, or a validation failure's reason. No column was added. Tests +cover cleared icons after a failed and a previewed run, the tooltip following each +result, and the window showing a scanned row's reason; four mutations fail them. + ## Decisions and mistakes that must remain visible ### A known defect shipped after being reported closed diff --git a/sources/EncodingChecker.Tests/PreviewResultPresentationTests.cs b/sources/EncodingChecker.Tests/PreviewResultPresentationTests.cs index d4255fe..93a7cf4 100644 --- a/sources/EncodingChecker.Tests/PreviewResultPresentationTests.cs +++ b/sources/EncodingChecker.Tests/PreviewResultPresentationTests.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; +using System.Reflection; using System.Windows.Forms; namespace EncodingChecker.Tests; @@ -20,6 +22,8 @@ public sealed class PreviewResultPresentationTests // form-less test can only address these images by index. private const int SuccessIcon = 0; private const int FailedIcon = 1; + private const int WouldChangeIcon = 2; + private const int NoIcon = -1; private static ListViewItem Row(string charset) => new([charset, "f.txt", ".txt", @"C:\dir"]); @@ -158,4 +162,71 @@ public void TheWouldChangeIconIndexActuallyExistsInTheResultsImageList() imageList.Images.Count >= 3, $"imgsResults must contain at least 3 images; found {imageList.Images.Count}."); } + + // Rows survive between runs, so an icon or reason from an earlier run must not stay on a + // row whose file this run left alone. + [Theory] + [InlineData(nameof(ConversionRowResult.Unchanged), FailedIcon)] + [InlineData(nameof(ConversionRowResult.Unchanged), WouldChangeIcon)] + [InlineData(nameof(ConversionRowResult.Skipped), FailedIcon)] + public void ARowThisRunLeftAloneLosesAnEarlierRunsIcon(string resultName, int earlierIcon) + { + var result = Enum.Parse(resultName); + ListViewItem item = Row("us-ascii"); + item.ImageIndex = earlierIcon; + item.ToolTipText = "an earlier run's failure"; + + MainForm.UpdateResultItem(item, Entry(result), "utf-8", wasPreview: false); + + Assert.Equal(NoIcon, item.ImageIndex); + Assert.Equal(string.Empty, item.ToolTipText); + } + + [Fact] + public void TheRowTooltipShowsThisRunsReason() + { + ListViewItem item = Row("us-ascii"); + + ConversionReportEntry unchanged = Entry(ConversionRowResult.Unchanged); + unchanged.Diagnostic = "ASCII is already valid UTF-8 without a BOM."; + MainForm.UpdateResultItem(item, unchanged, "utf-8", wasPreview: false); + Assert.Equal("ASCII is already valid UTF-8 without a BOM.", item.ToolTipText); + + ConversionReportEntry failed = Entry(ConversionRowResult.Error); + failed.Diagnostic = "SourceChangedDuringConversion: the file changed."; + MainForm.UpdateResultItem(item, failed, "utf-8", wasPreview: false); + Assert.Equal("SourceChangedDuringConversion: the file changed.", item.ToolTipText); + Assert.Equal(FailedIcon, item.ImageIndex); + + // A later success carries no reason, so the failure text goes with it. + MainForm.UpdateResultItem(item, Entry(ConversionRowResult.Converted), "utf-8", wasPreview: false); + Assert.Equal(string.Empty, item.ToolTipText); + Assert.Equal(SuccessIcon, item.ImageIndex); + } + + [Fact] + public void TheWindowShowsEachScannedRowsReasonAsATooltip() + { + UiTest.OnStaThread(() => + { + using var form = new MainForm(); + + var list = (ListView)typeof(MainForm) + .GetField("lstResults", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(form)!; + + Assert.True(list.ShowItemToolTips); + + ConversionReportEntry invalid = Entry(ConversionRowResult.Invalid); + invalid.Diagnostic = "The file is us-ascii, which is not in the allowed list."; + + typeof(MainForm) + .GetMethod("ActionWorkerProgressChanged", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(form, [form, new ProgressChangedEventArgs(0, invalid)]); + + ListViewItem row = Assert.Single(list.Items.Cast()); + Assert.Equal(invalid.Diagnostic, row.ToolTipText); + Assert.Equal(NoIcon, row.ImageIndex); + }); + } } diff --git a/sources/EncodingChecker/MainForm.Designer.cs b/sources/EncodingChecker/MainForm.Designer.cs index 8578136..f8ab3cb 100644 --- a/sources/EncodingChecker/MainForm.Designer.cs +++ b/sources/EncodingChecker/MainForm.Designer.cs @@ -142,6 +142,7 @@ private void InitializeComponent() lstResults.FullRowSelect = true; lstResults.GridLines = true; lstResults.Name = "lstResults"; + lstResults.ShowItemToolTips = true; lstResults.SmallImageList = imgsResults; lstResults.UseCompatibleStateImageBehavior = false; lstResults.View = System.Windows.Forms.View.Details; diff --git a/sources/EncodingChecker/MainForm.Execution.cs b/sources/EncodingChecker/MainForm.Execution.cs index 0b2d50e..87ccd31 100644 --- a/sources/EncodingChecker/MainForm.Execution.cs +++ b/sources/EncodingChecker/MainForm.Execution.cs @@ -309,9 +309,12 @@ private void ActionWorkerProgressChanged( Path.GetExtension(entry.FilePath), Path.GetDirectoryName(entry.FilePath) ?? string.Empty ], - -1) + ResultIconNone) { Tag = entry, + + // The reason a scan gave, such as why a file failed validation. + ToolTipText = entry.Diagnostic ?? string.Empty, }; lstResults.Items.Add(resultItem); @@ -641,6 +644,9 @@ internal static void UpdateResultItem( // Keep the row tied to the entry that produced its current state. item.Tag = entry; + // This run's reason, so an earlier run's failure text does not stay on the row. + item.ToolTipText = entry.Diagnostic ?? string.Empty; + if (entry.Result == ConversionRowResult.Converted) { // Preview leaves the file unchanged, so only the icon changes. @@ -662,12 +668,12 @@ internal static void UpdateResultItem( Debug.WriteLine( $"Conversion failed for {entry.FilePath}: {entry.Diagnostic}"); } - else if (entry.Result == ConversionRowResult.Skipped) + else { - Debug.WriteLine( - $"Conversion skipped for {entry.FilePath}: encoding could not be determined."); + // Unchanged or skipped: nothing to mark. Clearing the icon stops an earlier + // run's failure or "would convert" mark describing this run. + item.ImageIndex = ResultIconNone; } - // Unchanged: already matches the target; leave the row unchanged. } #endregion diff --git a/sources/EncodingChecker/MainForm.cs b/sources/EncodingChecker/MainForm.cs index dbd7625..4e7823a 100644 --- a/sources/EncodingChecker/MainForm.cs +++ b/sources/EncodingChecker/MainForm.cs @@ -86,6 +86,9 @@ private enum CurrentAction private const int ResultIconFailed = 1; private const int ResultIconWouldChange = 2; + // A row with nothing to mark: already in the target, or left alone. + private const int ResultIconNone = -1; + private const int ResultsColumnCharset = 0; private const int ResultsColumnFileName = 1; private const int ResultsColumnFileExt = 2;