fix(ui): reject timed-out stock and shape input
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OpenNest.WinForms.Tests;
|
||||
|
||||
public class ArchUnitsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ArchitecturalRegexHasExplicitTimeout()
|
||||
{
|
||||
var regex = Assert.IsType<Regex>(typeof(ArchUnits)
|
||||
.GetField("UnitRegex", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(250), regex.MatchTimeout);
|
||||
Assert.Equal(RegexOptions.None, regex.Options);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("5' 6.5\"", 66.5)]
|
||||
[InlineData("2 feet 3 1/2 inches", 27.5)]
|
||||
[InlineData("1-1/2\"", 1.5)]
|
||||
[InlineData("48", 48)]
|
||||
[InlineData("", 0)]
|
||||
public void OrdinaryArchitecturalInputsKeepTheirMeaning(string input, double expected)
|
||||
{
|
||||
var original = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
Assert.Equal(expected, ArchUnits.ParseToInches(input));
|
||||
}
|
||||
finally { CultureInfo.CurrentCulture = original; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidFieldIsRedAndNaNAndCorrectionRestoresIt() => RunSta(() =>
|
||||
{
|
||||
using var field = new TextBox { Text = "invalid dimension" };
|
||||
Assert.True(double.IsNaN(ArchUnits.GetLengthInches(field)));
|
||||
Assert.Equal(Color.Red, field.ForeColor);
|
||||
field.Text = "12\"";
|
||||
Assert.Equal(12, ArchUnits.GetLengthInches(field));
|
||||
Assert.Equal(SystemColors.WindowText, field.ForeColor);
|
||||
});
|
||||
|
||||
// Actual timeout execution is covered by the shared Regex mechanism in FractionTests;
|
||||
// these desktop tests deliberately avoid timing-sensitive oversized-input fixtures.
|
||||
internal static void RunSta(Action action)
|
||||
{
|
||||
Exception? error = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception ex) { error = ex; }
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
Assert.True(thread.Join(TimeSpan.FromSeconds(15)));
|
||||
if (error != null)
|
||||
ExceptionDispatchInfo.Capture(error).Throw();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Forms;
|
||||
|
||||
@@ -73,6 +75,117 @@ public class AutoNestFormStockOptionsTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StockSizeRegexHasExplicitTimeout()
|
||||
{
|
||||
var regex = Assert.IsType<Regex>(typeof(AutoNestForm)
|
||||
.GetField("SizePattern", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(250), regex.MatchTimeout);
|
||||
Assert.Equal(RegexOptions.None, regex.Options);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("48 x")]
|
||||
[InlineData("abc")]
|
||||
[InlineData("0 x 96")]
|
||||
[InlineData("-1 x 96")]
|
||||
[InlineData("١ x 96")]
|
||||
public void NonblankInvalidRowRejectsWholeCollection(string invalid) => RunSta(() =>
|
||||
{
|
||||
using var form = new StockTestForm();
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96, Cost = 25 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||
var row = rows.AddNew()!;
|
||||
SetValue(row, "Size", invalid);
|
||||
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
Assert.Empty(options);
|
||||
Assert.Contains(invalid, error);
|
||||
Assert.Contains("row 2", error);
|
||||
Assert.Throws<FormatException>(() => form.GetPlateOptions());
|
||||
Assert.NotEmpty(grid.Rows[1].ErrorText);
|
||||
|
||||
SetValue(row, "Size", "72.5 × 144.25");
|
||||
Assert.True(form.TryGetPlateOptions(out options, out error));
|
||||
Assert.Null(error);
|
||||
Assert.Equal(2, options.Count);
|
||||
Assert.Equal(72.5, options[1].Width);
|
||||
Assert.Equal(144.25, options[1].Length);
|
||||
Assert.Equal(25, options[0].Cost);
|
||||
Assert.Empty(grid.Rows[1].ErrorText);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void TimeoutAfterValidRowPublishesNoPrefixAndDoesNotAcceptDialog() => RunSta(() =>
|
||||
{
|
||||
using var form = new StockTestForm();
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
var rows = Assert.IsAssignableFrom<IBindingList>(grid.DataSource);
|
||||
var row = rows.AddNew()!;
|
||||
SetValue(row, "Size", "timeout row");
|
||||
((ICancelAddNew)rows).EndNew(rows.Count - 1);
|
||||
form.FailOn = "timeout row";
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
Assert.Empty(options);
|
||||
Assert.Contains("timeout row", error);
|
||||
Assert.True(form.TryClose(DialogResult.OK));
|
||||
Assert.Equal(DialogResult.None, form.DialogResult);
|
||||
Assert.Contains("timeout row", Assert.Single(form.Notices));
|
||||
Assert.True(form.MatchCalls > 1);
|
||||
|
||||
form.Notices.Clear();
|
||||
Assert.False(form.TryClose(DialogResult.Cancel));
|
||||
Assert.Empty(form.Notices);
|
||||
Assert.False(Assert.IsType<Button>(form.Controls.Find("cancelButton", true).Single()).CausesValidation);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void InvalidPendingEditCannotPublishOldBoundValue() => RunSta(() =>
|
||||
{
|
||||
using var form = new StockTestForm();
|
||||
form.LoadPlateOptions(new List<PlateOption> { new() { Width = 48, Length = 96 } }, 0.5);
|
||||
var grid = GetGrid(form);
|
||||
form.Show();
|
||||
grid.CurrentCell = grid.Rows[0].Cells[0];
|
||||
Assert.True(grid.BeginEdit(false));
|
||||
grid.EditingControl.Text = "48 x";
|
||||
|
||||
Assert.False(form.TryGetPlateOptions(out var options, out var error));
|
||||
Assert.Empty(options);
|
||||
Assert.Contains("48 x", error);
|
||||
grid.CancelEdit();
|
||||
});
|
||||
|
||||
private sealed class StockTestForm : AutoNestForm
|
||||
{
|
||||
public StockTestForm() : base(new Nest()) { }
|
||||
public string? FailOn { get; set; }
|
||||
public int MatchCalls { get; private set; }
|
||||
public List<string> Notices { get; } = new();
|
||||
|
||||
internal override Match MatchStockSize(string value)
|
||||
{
|
||||
MatchCalls++;
|
||||
if (value == FailOn)
|
||||
throw new RegexMatchTimeoutException();
|
||||
return base.MatchStockSize(value);
|
||||
}
|
||||
|
||||
internal override void ReportStockValidationFailure(string error) => Notices.Add(error);
|
||||
|
||||
public bool TryClose(DialogResult result)
|
||||
{
|
||||
DialogResult = result;
|
||||
var closing = new FormClosingEventArgs(CloseReason.UserClosing, false);
|
||||
OnFormClosing(closing);
|
||||
return closing.Cancel;
|
||||
}
|
||||
}
|
||||
|
||||
private static DataGridView GetGrid(AutoNestForm form)
|
||||
{
|
||||
// Binding needs a context even when the test does not show the form.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using OpenNest.Controls;
|
||||
using OpenNest.Forms;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.WinForms.Tests.Forms;
|
||||
|
||||
public class ShapeLibraryFormTests
|
||||
{
|
||||
[Fact]
|
||||
public void FriendlyNameRegexHasExplicitTimeout()
|
||||
{
|
||||
var regex = Assert.IsType<Regex>(typeof(ShapeLibraryForm)
|
||||
.GetField("FriendlyNamePattern", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null));
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(100), regex.MatchTimeout);
|
||||
Assert.Equal(RegexOptions.None, regex.Options);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PipeFlangeShape", "Pipe Flange")]
|
||||
[InlineData("HolePatternDiameter", "Hole Pattern Diameter")]
|
||||
[InlineData("NgonShape", "Ngon")]
|
||||
[InlineData("ABCShape", "ABC")]
|
||||
public void FriendlyNamesKeepSuffixAndWordBoundaryBehavior(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ShapeLibraryForm.FriendlyName(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PipeFlangeShape", "PipeFlange")]
|
||||
[InlineData("HolePatternDiameter", "HolePatternDiameter")]
|
||||
public void FriendlyNameTimeoutDiscardsPartialFormatting(string input, string expected)
|
||||
{
|
||||
var notices = new List<string>();
|
||||
var result = ShapeLibraryForm.FriendlyName(input, name =>
|
||||
{
|
||||
Assert.Equal(expected, name);
|
||||
// Simulate a replacement failing after it has transformed a prefix internally.
|
||||
var partial = name.Insert(4, " ");
|
||||
Assert.NotEqual(expected, partial);
|
||||
throw new RegexMatchTimeoutException();
|
||||
}, notices.Add);
|
||||
Assert.Equal(expected, result);
|
||||
Assert.Contains(expected, Assert.Single(notices));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidShapeInputClearsOldPreviewAndBlocksAddUntilCorrected() => ArchUnitsTests.RunSta(() =>
|
||||
{
|
||||
using var form = new ShapeLibraryForm();
|
||||
var list = Find<ListBox>(form, "shapeListBox");
|
||||
list.SelectedItem = list.Items.Cast<object>().Single(entry =>
|
||||
(Type)entry.GetType().GetProperty("ShapeType")!.GetValue(entry)! == typeof(RectangleShape));
|
||||
var preview = Find<ShapePreviewControl>(form, "previewBox");
|
||||
var add = Find<Button>(form, "addButton");
|
||||
var input = Find<Panel>(form, "parametersPanel").Controls.OfType<TextBox>().First();
|
||||
var valid = input.Text;
|
||||
Assert.NotEmpty(preview.Plate.Parts);
|
||||
Assert.True(add.Enabled);
|
||||
|
||||
input.Text = "invalid dimension";
|
||||
Assert.Equal(Color.Red, input.ForeColor);
|
||||
Assert.Empty(preview.Plate.Parts);
|
||||
Assert.False(add.Enabled);
|
||||
InvokeAdd(form);
|
||||
Assert.Empty(form.GetDrawings());
|
||||
|
||||
input.Text = valid;
|
||||
Assert.Equal(SystemColors.WindowText, input.ForeColor);
|
||||
Assert.NotEmpty(preview.Plate.Parts);
|
||||
Assert.True(add.Enabled);
|
||||
InvokeAdd(form);
|
||||
Assert.Single(form.GetDrawings());
|
||||
});
|
||||
|
||||
private static T Find<T>(Control form, string name) where T : Control =>
|
||||
Assert.IsType<T>(form.Controls.Find(name, true).Single());
|
||||
|
||||
private static void InvokeAdd(ShapeLibraryForm form) => typeof(ShapeLibraryForm)
|
||||
.GetMethod("AddButton_Click", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.Invoke(form, new object?[] { null, EventArgs.Empty });
|
||||
}
|
||||
@@ -10,7 +10,9 @@ namespace OpenNest
|
||||
public static class ArchUnits
|
||||
{
|
||||
private static readonly Regex UnitRegex = new Regex(
|
||||
"^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$"
|
||||
"^(?<Feet>\\d+\\.?\\d*\\s*')?\\s*(?<Inches>\\d+\\.?\\d*\\s*\")?$",
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromMilliseconds(250)
|
||||
);
|
||||
|
||||
public static double ParseToInches(string input)
|
||||
|
||||
@@ -10,11 +10,16 @@ namespace OpenNest.Forms
|
||||
{
|
||||
public partial class AutoNestForm : Form
|
||||
{
|
||||
private static readonly Regex SizePattern = new(@"^(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)$");
|
||||
private static readonly Regex SizePattern = new(
|
||||
@"^(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)$",
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromMilliseconds(250)
|
||||
);
|
||||
|
||||
public AutoNestForm(Nest nest)
|
||||
{
|
||||
InitializeComponent();
|
||||
cancelButton.CausesValidation = false;
|
||||
SetupPartsGrid();
|
||||
SetupPlateGrid();
|
||||
LoadEngines();
|
||||
@@ -195,30 +200,65 @@ namespace OpenNest.Forms
|
||||
|
||||
public List<PlateOption> GetPlateOptions()
|
||||
{
|
||||
plateGrid.EndEdit();
|
||||
var result = new List<PlateOption>();
|
||||
if (!TryGetPlateOptions(out var options, out var error))
|
||||
throw new FormatException(error);
|
||||
return options;
|
||||
}
|
||||
|
||||
public bool TryGetPlateOptions(out List<PlateOption> options, out string error)
|
||||
{
|
||||
options = new List<PlateOption>();
|
||||
error = null;
|
||||
if (!plateGrid.EndEdit())
|
||||
{
|
||||
var value = plateGrid.EditingControl?.Text ?? plateGrid.CurrentCell?.Value?.ToString();
|
||||
error = $"Invalid stock size '{value}'. Enter positive dimensions as W x L.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var gridItems = plateGrid.DataSource as BindingList<PlateOptionItem>;
|
||||
if (gridItems == null)
|
||||
return result;
|
||||
return true;
|
||||
|
||||
foreach (var item in gridItems)
|
||||
var validated = new List<PlateOption>();
|
||||
for (var index = 0; index < gridItems.Count; index++)
|
||||
{
|
||||
var item = gridItems[index];
|
||||
if (string.IsNullOrWhiteSpace(item.Size))
|
||||
continue;
|
||||
if (!TryParseSize(item.Size, out var width, out var length))
|
||||
continue;
|
||||
if (width <= 0 || length <= 0)
|
||||
continue;
|
||||
|
||||
result.Add(
|
||||
new PlateOption
|
||||
{
|
||||
Width = width,
|
||||
Length = length,
|
||||
Cost = item.Cost,
|
||||
}
|
||||
);
|
||||
error = $"Invalid stock size '{item.Size}' in row {index + 1}. Enter positive dimensions as W x L.";
|
||||
if (index < plateGrid.Rows.Count)
|
||||
plateGrid.Rows[index].ErrorText = error;
|
||||
return false;
|
||||
}
|
||||
|
||||
return result;
|
||||
if (index < plateGrid.Rows.Count)
|
||||
plateGrid.Rows[index].ErrorText = "";
|
||||
validated.Add(new PlateOption { Width = width, Length = length, Cost = item.Cost });
|
||||
}
|
||||
|
||||
// Publish only a completely validated collection, never a valid prefix.
|
||||
options = validated;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
if (DialogResult == DialogResult.OK && OptimizePlateSize
|
||||
&& !TryGetPlateOptions(out _, out var error))
|
||||
{
|
||||
e.Cancel = true;
|
||||
DialogResult = DialogResult.None;
|
||||
ReportStockValidationFailure(error);
|
||||
}
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
internal virtual void ReportStockValidationFailure(string error)
|
||||
{
|
||||
MessageBox.Show(this, error, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
public void LoadPlateOptions(List<PlateOption> options, double salvageRate)
|
||||
@@ -300,7 +340,10 @@ namespace OpenNest.Forms
|
||||
|
||||
var value = e.FormattedValue?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
plateGrid.Rows[e.RowIndex].ErrorText = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryParseSize(value, out _, out _))
|
||||
{
|
||||
@@ -313,25 +356,43 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseSize(string value, out double width, out double length)
|
||||
private bool TryParseSize(string value, out double width, out double length)
|
||||
{
|
||||
width = 0;
|
||||
length = 0;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
var match = SizePattern.Match(value.Trim());
|
||||
try
|
||||
{
|
||||
var match = MatchStockSize(value.Trim());
|
||||
if (!match.Success)
|
||||
return false;
|
||||
width = double.Parse(
|
||||
var parsedWidth = double.Parse(
|
||||
match.Groups[1].Value,
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
length = double.Parse(
|
||||
var parsedLength = double.Parse(
|
||||
match.Groups[2].Value,
|
||||
System.Globalization.CultureInfo.InvariantCulture
|
||||
);
|
||||
if (!double.IsFinite(parsedWidth) || !double.IsFinite(parsedLength)
|
||||
|| parsedWidth <= 0 || parsedLength <= 0)
|
||||
return false;
|
||||
width = parsedWidth;
|
||||
length = parsedLength;
|
||||
return true;
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual Match MatchStockSize(string value) => SizePattern.Match(value);
|
||||
|
||||
private static string FormatSize(double width, double length)
|
||||
{
|
||||
|
||||
@@ -1381,10 +1381,15 @@ namespace OpenNest.Forms
|
||||
var items = form.GetNestItems();
|
||||
if (!items.Any(it => it.Quantity > 0))
|
||||
return;
|
||||
var plateOptions = default(List<PlateOption>);
|
||||
if (form.OptimizePlateSize && !form.TryGetPlateOptions(out plateOptions, out var stockError))
|
||||
{
|
||||
MessageBox.Show(this, stockError, "Auto Nest", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
var engineName = form.EngineName ?? EngineSelection.EngineName;
|
||||
EngineSelection.EngineName = engineName;
|
||||
engineComboBox.SelectedItem = engineName;
|
||||
var plateOptions = form.OptimizePlateSize ? form.GetPlateOptions() : null;
|
||||
using var cts = new CancellationTokenSource();
|
||||
nestingCts = cts;
|
||||
using var progressForm = new NestProgressForm(cts, showPlateRow: true)
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace OpenNest.Forms
|
||||
{
|
||||
public partial class ShapeLibraryForm : Form
|
||||
{
|
||||
private static readonly Regex FriendlyNamePattern = new(
|
||||
@"(?<=[a-z0-9])([A-Z])",
|
||||
RegexOptions.None,
|
||||
TimeSpan.FromMilliseconds(100)
|
||||
);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
@@ -272,9 +278,13 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var shape = CreateShapeFromInputs();
|
||||
if (shape == null)
|
||||
{
|
||||
InvalidatePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
var drawing = shape.GetDrawing();
|
||||
addButton.Enabled = drawing?.Program != null;
|
||||
nameTextBox.Text = shape.GenerateName();
|
||||
previewBox.ShowDrawing(drawing);
|
||||
|
||||
@@ -298,10 +308,17 @@ namespace OpenNest.Forms
|
||||
}
|
||||
catch
|
||||
{
|
||||
previewBox.ShowDrawing(null);
|
||||
InvalidatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidatePreview()
|
||||
{
|
||||
addButton.Enabled = false;
|
||||
previewBox.ShowDrawing(null);
|
||||
previewBox.SetInfo("Invalid shape input");
|
||||
}
|
||||
|
||||
private void UpdatePipeSizeFilter()
|
||||
{
|
||||
// Find the PipeSize combo and the numeric inputs it depends on.
|
||||
@@ -424,7 +441,10 @@ namespace OpenNest.Forms
|
||||
{
|
||||
var shape = CreateShapeFromInputs();
|
||||
if (shape == null)
|
||||
{
|
||||
InvalidatePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
var drawing = shape.GetDrawing();
|
||||
drawing.Name = GetUniqueName(drawing.Name);
|
||||
@@ -461,12 +481,28 @@ namespace OpenNest.Forms
|
||||
}
|
||||
}
|
||||
|
||||
private static string FriendlyName(string name)
|
||||
internal static string FriendlyName(
|
||||
string name,
|
||||
Func<string, string> replace = null,
|
||||
Action<string> report = null
|
||||
)
|
||||
{
|
||||
if (name.EndsWith("Shape"))
|
||||
name = name.Substring(0, name.Length - 5);
|
||||
|
||||
return Regex.Replace(name, @"(?<=[a-z0-9])([A-Z])", " $1");
|
||||
try
|
||||
{
|
||||
return replace != null ? replace(name) : FriendlyNamePattern.Replace(name, " $1");
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
var diagnostic = $"Friendly-name formatting timed out for '{name}'; using the unchanged name.";
|
||||
if (report != null)
|
||||
report(diagnostic);
|
||||
else
|
||||
System.Diagnostics.Trace.TraceWarning(diagnostic);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
private class ShapeEntry
|
||||
|
||||
Reference in New Issue
Block a user