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:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user