fix(ui): format stock sizes invariantly and bound validation notices

Address Task 3b quality review:
- FormatSize now emits invariant decimal notation so a comma-decimal
  locale cannot produce stock text its own invariant parser rejects.
- Validation notices embed a bounded single-line preview instead of the
  full cell value.
- Documented GetPlateOptions' all-or-nothing throwing contract.
- STA test helpers run the worker as a background thread with a
  diagnostic timeout message; added a de-DE fractional stock round-trip
  regression.
This commit is contained in:
aj
2026-09-29 23:29:20 -04:00
parent ab1f3a28d1
commit fc53c7732e
3 changed files with 52 additions and 7 deletions
+3 -2
View File
@@ -55,10 +55,11 @@ public class ArchUnitsTests
{ {
try { action(); } try { action(); }
catch (Exception ex) { error = ex; } catch (Exception ex) { error = ex; }
}); })
{ IsBackground = true };
thread.SetApartmentState(ApartmentState.STA); thread.SetApartmentState(ApartmentState.STA);
thread.Start(); thread.Start();
Assert.True(thread.Join(TimeSpan.FromSeconds(15))); Assert.True(thread.Join(TimeSpan.FromSeconds(15)), "The STA test did not complete.");
if (error != null) if (error != null)
ExceptionDispatchInfo.Capture(error).Throw(); ExceptionDispatchInfo.Capture(error).Throw();
} }
@@ -1,5 +1,6 @@
using System.Collections; using System.Collections;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -160,6 +161,31 @@ public class AutoNestFormStockOptionsTests
grid.CancelEdit(); grid.CancelEdit();
}); });
[Fact]
public void FractionalStockSizesRoundTripUnderCommaDecimalCulture() => RunSta(() =>
{
var original = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de-DE");
using var form = new StockTestForm();
// FormatSize must emit invariant dots or the reloaded collection would be
// rejected as invalid under the invariant parser.
form.LoadPlateOptions(new List<PlateOption>
{
new() { Width = 48.5, Length = 96.25, Cost = 25 },
}, 0.5);
Assert.True(form.TryGetPlateOptions(out var options, out var error));
Assert.Null(error);
Assert.Equal(48.5, options[0].Width);
Assert.Equal(96.25, options[0].Length);
}
finally
{
CultureInfo.CurrentCulture = original;
}
});
private sealed class StockTestForm : AutoNestForm private sealed class StockTestForm : AutoNestForm
{ {
public StockTestForm() : base(new Nest()) { } public StockTestForm() : base(new Nest()) { }
@@ -212,10 +238,11 @@ public class AutoNestFormStockOptionsTests
{ {
try { action(); } try { action(); }
catch (Exception ex) { error = ex; } catch (Exception ex) { error = ex; }
}); })
{ IsBackground = true };
thread.SetApartmentState(ApartmentState.STA); thread.SetApartmentState(ApartmentState.STA);
thread.Start(); thread.Start();
Assert.True(thread.Join(TimeSpan.FromSeconds(15))); Assert.True(thread.Join(TimeSpan.FromSeconds(15)), "The STA test did not complete.");
if (error != null) if (error != null)
ExceptionDispatchInfo.Capture(error).Throw(); ExceptionDispatchInfo.Capture(error).Throw();
} }
+20 -3
View File
@@ -198,6 +198,11 @@ namespace OpenNest.Forms
return nestItems; return nestItems;
} }
/// <summary>
/// Returns the validated stock collection. Throws <see cref="FormatException"/> when a
/// nonblank row is invalid or timed out; all-or-nothing, never a valid prefix. Prefer
/// <see cref="TryGetPlateOptions"/> for callers that must report the failure.
/// </summary>
public List<PlateOption> GetPlateOptions() public List<PlateOption> GetPlateOptions()
{ {
if (!TryGetPlateOptions(out var options, out var error)) if (!TryGetPlateOptions(out var options, out var error))
@@ -212,7 +217,7 @@ namespace OpenNest.Forms
if (!plateGrid.EndEdit()) if (!plateGrid.EndEdit())
{ {
var value = plateGrid.EditingControl?.Text ?? plateGrid.CurrentCell?.Value?.ToString(); var value = plateGrid.EditingControl?.Text ?? plateGrid.CurrentCell?.Value?.ToString();
error = $"Invalid stock size '{value}'. Enter positive dimensions as W x L."; error = $"Invalid stock size '{Preview(value)}'. Enter positive dimensions as W x L.";
return false; return false;
} }
@@ -228,7 +233,7 @@ namespace OpenNest.Forms
continue; continue;
if (!TryParseSize(item.Size, out var width, out var length)) if (!TryParseSize(item.Size, out var width, out var length))
{ {
error = $"Invalid stock size '{item.Size}' in row {index + 1}. Enter positive dimensions as W x L."; error = $"Invalid stock size '{Preview(item.Size)}' in row {index + 1}. Enter positive dimensions as W x L.";
if (index < plateGrid.Rows.Count) if (index < plateGrid.Rows.Count)
plateGrid.Rows[index].ErrorText = error; plateGrid.Rows[index].ErrorText = error;
return false; return false;
@@ -261,6 +266,16 @@ namespace OpenNest.Forms
MessageBox.Show(this, error, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning); MessageBox.Show(this, error, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning);
} }
// Bounded single-line preview so a long or multiline cell value cannot produce an
// unreadable message box; the full value stays in the editable cell.
private static string Preview(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
var line = value.Replace("\r", " ").Replace("\n", " ");
return line.Length <= 40 ? line : line[..40] + "...";
}
public void LoadPlateOptions(List<PlateOption> options, double salvageRate) public void LoadPlateOptions(List<PlateOption> options, double salvageRate)
{ {
if (options != null && options.Count > 0) if (options != null && options.Count > 0)
@@ -396,7 +411,9 @@ namespace OpenNest.Forms
private static string FormatSize(double width, double length) private static string FormatSize(double width, double length)
{ {
return $"{width:G} x {length:G}"; // Invariant: TryParseSize requires invariant decimal notation; current-culture
// formatting would produce comma decimals that the parser then rejects.
return FormattableString.Invariant($"{width:G} x {length:G}");
} }
private void PartsGrid_DataError(object sender, DataGridViewDataErrorEventArgs e) private void PartsGrid_DataError(object sender, DataGridViewDataErrorEventArgs e)