Files
OpenNest/OpenNest.IO.Tests/CuttingParametersSerializerTests.cs
T

409 lines
15 KiB
C#

using System.Text.Json;
using OpenNest.CNC.CuttingStrategy;
using CuttingParametersSerializer = OpenNest.IO.CuttingParametersSerializer;
namespace OpenNest.IO.Tests;
public class CuttingParametersSerializerTests
{
[Theory]
[InlineData("None")]
[InlineData("Line")]
[InlineData("Arc")]
[InlineData("LineArc")]
[InlineData("CleanHole")]
[InlineData("LineLine")]
public void SettingsRoundTrip_PreservesEveryLeadInType(string type)
{
var leadIn = CreateLeadIn(type);
var original = new CuttingParameters
{
ExternalLeadIn = leadIn,
InternalLeadIn = leadIn,
ArcCircleLeadIn = leadIn,
TabConfig = new NormalTab { Size = 0.42, TabLeadIn = leadIn },
};
var restored = RoundTrip(original);
AssertEquivalent(leadIn, restored.ExternalLeadIn);
AssertEquivalent(leadIn, restored.InternalLeadIn);
AssertEquivalent(leadIn, restored.ArcCircleLeadIn);
AssertEquivalent(leadIn, restored.TabConfig.TabLeadIn);
}
[Theory]
[InlineData("None")]
[InlineData("Line")]
[InlineData("Arc")]
public void SettingsRoundTrip_PreservesEveryLeadOutType(string type)
{
var leadOut = CreateLeadOut(type);
var original = new CuttingParameters
{
ExternalLeadOut = leadOut,
InternalLeadOut = leadOut,
ArcCircleLeadOut = leadOut,
TabConfig = new NormalTab { Size = 0.42, TabLeadOut = leadOut },
};
var restored = RoundTrip(original);
AssertEquivalent(leadOut, restored.ExternalLeadOut);
AssertEquivalent(leadOut, restored.InternalLeadOut);
AssertEquivalent(leadOut, restored.ArcCircleLeadOut);
AssertEquivalent(leadOut, restored.TabConfig.TabLeadOut);
}
[Theory]
[InlineData("Normal")]
[InlineData("Machine")]
[InlineData("Breaker")]
public void SettingsRoundTrip_PreservesEveryTabType(string type)
{
var original = new CuttingParameters
{
TabsEnabled = true,
TabConfig = CreateTab(type),
};
AssertEquivalent(original, RoundTrip(original));
}
[Theory]
[InlineData(SequenceMethod.RightSide)]
[InlineData(SequenceMethod.LeastCode)]
[InlineData(SequenceMethod.Advanced)]
[InlineData(SequenceMethod.BottomSide)]
[InlineData(SequenceMethod.EdgeStart)]
[InlineData(SequenceMethod.LeftSide)]
[InlineData(SequenceMethod.RightSideAlt)]
public void SettingsRoundTrip_PreservesAllFieldsAndSequenceMethods(SequenceMethod method)
{
var original = CreateParameters();
original.Assignment.Method = method;
original.Sequencing.Method = method;
AssertEquivalent(original, RoundTrip(original));
}
[Fact]
public void Deserialize_LegacySettings_PreservesValuesAndUsesMissingFieldDefaults()
{
const string json = """
{
"externalLeadIn": { "type": "LineLine", "length1": 0.2, "angle1": 31,
"length2": 0.4, "angle2": 62 },
"externalLeadOut": { "type": "Line", "length": 0.5, "approachAngle": 43 },
"internalLeadIn": { "type": "Arc", "radius": 0.12 },
"internalLeadOut": { "type": "Arc", "radius": 0.09, "gapSize": 0 },
"arcCircleLeadIn": { "type": "CleanHole", "lineLength": 0.8,
"arcRadius": 0.3, "kerf": 0.04 },
"arcCircleLeadOut": { "type": "None" },
"tabsEnabled": true,
"tabWidth": 0.375,
"pierceClearance": 0.0625
}
""";
var restored = CuttingParametersSerializer.Deserialize(json);
var lineLine = Assert.IsType<LineLineLeadIn>(restored.ExternalLeadIn);
Assert.Equal(0.2, lineLine.Length1);
Assert.Equal(31, lineLine.ApproachAngle1);
Assert.Equal(0.4, lineLine.Length2);
Assert.Equal(62, lineLine.ApproachAngle2);
var lineOut = Assert.IsType<LineLeadOut>(restored.ExternalLeadOut);
Assert.Equal(0.5, lineOut.Length);
Assert.Equal(43, lineOut.ApproachAngle);
Assert.Equal(0.12, Assert.IsType<ArcLeadIn>(restored.InternalLeadIn).Radius);
Assert.Equal(0.09, Assert.IsType<ArcLeadOut>(restored.InternalLeadOut).Radius);
var cleanHole = Assert.IsType<CleanHoleLeadIn>(restored.ArcCircleLeadIn);
Assert.Equal(0.8, cleanHole.LineLength);
Assert.Equal(0.3, cleanHole.ArcRadius);
Assert.Equal(0.04, cleanHole.Kerf);
Assert.IsType<NoLeadOut>(restored.ArcCircleLeadOut);
Assert.True(restored.TabsEnabled);
Assert.Equal(0.375, Assert.IsType<NormalTab>(restored.TabConfig).Size);
Assert.Equal(0.0625, restored.PierceClearance);
Assert.False(restored.RoundLeadInAngles);
Assert.Equal(5, restored.LeadInAngleIncrement);
Assert.Equal(0, restored.AutoTabMinSize);
Assert.Equal(0, restored.AutoTabMaxSize);
AssertEquivalent(new AssignmentParameters(), restored.Assignment);
AssertEquivalent(new SequenceParameters(), restored.Sequencing);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Deserialize_LegacyNonpositiveAngleIncrement_UsesFiveDegrees(double increment)
{
var json = JsonSerializer.Serialize(new { leadInAngleIncrement = increment });
Assert.Equal(5, CuttingParametersSerializer.Deserialize(json).LeadInAngleIncrement);
}
[Fact]
public void Deserialize_EmptyObject_PreservesLegacyDefaults()
{
var restored = CuttingParametersSerializer.Deserialize("{}");
Assert.IsType<NoLeadIn>(restored.ExternalLeadIn);
Assert.IsType<NoLeadIn>(restored.InternalLeadIn);
Assert.IsType<NoLeadIn>(restored.ArcCircleLeadIn);
Assert.IsType<NoLeadOut>(restored.ExternalLeadOut);
Assert.IsType<NoLeadOut>(restored.InternalLeadOut);
Assert.IsType<NoLeadOut>(restored.ArcCircleLeadOut);
Assert.Equal(0, Assert.IsType<NormalTab>(restored.TabConfig).Size);
Assert.Equal(0, restored.PierceClearance);
Assert.Equal(5, restored.LeadInAngleIncrement);
}
[Fact]
public void Deserialize_JsonNull_ReturnsDomainDefaults()
{
AssertEquivalent(new CuttingParameters(), CuttingParametersSerializer.Deserialize("null"));
}
[Theory]
[InlineData("not-json")]
[InlineData("{\"externalLeadIn\":17}")]
public void Deserialize_MalformedSettings_ThrowsJsonException(string json)
{
Assert.Throws<JsonException>(() => CuttingParametersSerializer.Deserialize(json));
}
[Fact]
public void Deserialize_UnknownLeadTypes_UsesNoLead()
{
var restored = CuttingParametersSerializer.Deserialize("""
{ "externalLeadIn": { "type": "FutureLead" },
"externalLeadOut": { "type": "FutureLead" } }
""");
Assert.IsType<NoLeadIn>(restored.ExternalLeadIn);
Assert.IsType<NoLeadOut>(restored.ExternalLeadOut);
}
[Fact]
public void SettingsRoundTrip_NullTab_PreservesLegacyWidthFallback()
{
var original = new CuttingParameters { TabConfig = null };
var json = CuttingParametersSerializer.Serialize(original);
using var document = JsonDocument.Parse(json);
Assert.Equal(0.25, document.RootElement.GetProperty("tabWidth").GetDouble());
Assert.DoesNotContain('\n', json);
Assert.Equal(0.25, Assert.IsType<NormalTab>(
CuttingParametersSerializer.Deserialize(json).TabConfig).Size);
}
[Theory]
[InlineData("Normal")]
[InlineData("Machine")]
[InlineData("Breaker")]
public void DtoRoundTrip_UsesNestJsonOptionsAndPreservesAllFields(string tabType)
{
var original = CreateParameters();
original.TabConfig = CreateTab(tabType);
original.LeadInAngleIncrement = 0;
var dto = CuttingParametersSerializer.ToDto(original);
var json = JsonSerializer.Serialize(dto, NestFormat.JsonOptions);
var parsed = JsonSerializer.Deserialize<CuttingParametersDto>(json, NestFormat.JsonOptions);
var restored = CuttingParametersSerializer.FromDto(parsed);
AssertEquivalent(original, restored);
}
[Fact]
public void DtoRoundTrip_NullParameters_RemainNull()
{
Assert.Null(CuttingParametersSerializer.ToDto(null));
Assert.Null(CuttingParametersSerializer.FromDto(null));
}
[Fact]
public void DtoRoundTrip_DefaultParameters_PreservesNullTab()
{
var original = new CuttingParameters();
var dto = CuttingParametersSerializer.ToDto(original);
var json = JsonSerializer.Serialize(dto, NestFormat.JsonOptions);
var parsed = JsonSerializer.Deserialize<CuttingParametersDto>(json, NestFormat.JsonOptions);
AssertEquivalent(original, CuttingParametersSerializer.FromDto(parsed));
}
[Fact]
public void DtoRoundTrip_NullAssignmentAndSequencing_RemainNull()
{
var original = new CuttingParameters { Assignment = null, Sequencing = null };
var dto = CuttingParametersSerializer.ToDto(original);
var json = JsonSerializer.Serialize(dto, NestFormat.JsonOptions);
var parsed = JsonSerializer.Deserialize<CuttingParametersDto>(json, NestFormat.JsonOptions);
AssertEquivalent(original, CuttingParametersSerializer.FromDto(parsed));
}
[Fact]
public void DtoMapping_SourceSnapshotAndRestoredParameters_AreIndependent()
{
var original = CreateParameters();
var dto = CuttingParametersSerializer.ToDto(original);
var restored = CuttingParametersSerializer.FromDto(dto);
AssertEquivalent(original, restored);
original.Assignment.Preference = "changed source";
original.Sequencing.SmallCutoutWidth = 101;
((LineLineLeadIn)original.ExternalLeadIn).Length1 = 102;
((BreakerTab)original.TabConfig).BreakerDepth = 103;
((LineArcLeadIn)original.TabConfig.TabLeadIn).ArcRadius = 104;
((ArcLeadOut)original.TabConfig.TabLeadOut).Radius = 105;
AssertEquivalent(CreateParameters(), CuttingParametersSerializer.FromDto(dto));
dto.Assignment.Preference = "changed snapshot";
dto.Sequencing.SmallCutoutWidth = 201;
dto.ExternalLeadIn.Length1 = 202;
dto.TabConfig.BreakerDepth = 203;
dto.TabConfig.TabLeadIn.ArcRadius = 204;
dto.TabConfig.TabLeadOut.Radius = 205;
AssertEquivalent(CreateParameters(), restored);
}
[Fact]
public void Deserialize_UnknownTabType_DoesNotInventACutStrategy()
{
var restored = CuttingParametersSerializer.Deserialize("""
{ "tabConfig": { "type": "FutureTab" }, "tabWidth": 0.7 }
""");
Assert.Null(restored.TabConfig);
}
private static CuttingParameters RoundTrip(CuttingParameters original) =>
CuttingParametersSerializer.Deserialize(CuttingParametersSerializer.Serialize(original));
private static CuttingParameters CreateParameters() => new()
{
Id = 47,
MachineName = "Laser A",
MaterialName = "Steel",
Grade = "A36",
Thickness = 0.1875,
Kerf = 0.018,
PartSpacing = 0.23,
ExternalLeadIn = CreateLeadIn("LineLine"),
ExternalLeadOut = CreateLeadOut("Line"),
InternalLeadIn = CreateLeadIn("CleanHole"),
InternalLeadOut = CreateLeadOut("Arc"),
ArcCircleLeadIn = CreateLeadIn("LineArc"),
ArcCircleLeadOut = CreateLeadOut("None"),
PierceClearance = 0.17,
RoundLeadInAngles = true,
LeadInAngleIncrement = 13,
AutoTabMinSize = 0.62,
AutoTabMaxSize = 4.7,
TabConfig = CreateTab("Breaker"),
TabsEnabled = true,
Assignment = new AssignmentParameters
{
Method = SequenceMethod.EdgeStart,
Preference = "TAIL",
MinGeometryLength = 0.37,
},
Sequencing = new SequenceParameters
{
Method = SequenceMethod.LeftSide,
SmallCutoutWidth = 2.3,
SmallCutoutHeight = 3.4,
MediumCutoutWidth = 9.5,
MediumCutoutHeight = 10.6,
DistanceMediumSmall = 1.7,
AlternateRowsColumns = false,
AlternateCutoutsWithinRowColumn = false,
MinDistanceBetweenRowsColumns = 0.8,
},
};
private static LeadIn CreateLeadIn(string type) => type switch
{
"Line" => new LineLeadIn { Length = 0.37, ApproachAngle = 47 },
"Arc" => new ArcLeadIn { Radius = 0.53 },
"LineArc" => new LineArcLeadIn
{
LineLength = 0.43,
ArcRadius = 0.29,
ApproachAngle = 118,
},
"CleanHole" => new CleanHoleLeadIn { LineLength = 0.38, ArcRadius = 0.16, Kerf = 0.021 },
"LineLine" => new LineLineLeadIn
{
Length1 = 0.24,
ApproachAngle1 = 72,
Length2 = 0.48,
ApproachAngle2 = 36,
},
"None" => new NoLeadIn(),
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
private static LeadOut CreateLeadOut(string type) => type switch
{
"Line" => new LineLeadOut { Length = 0.21, ApproachAngle = 58 },
"Arc" => new ArcLeadOut { Radius = 0.19 },
"None" => new NoLeadOut(),
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
private static Tab CreateTab(string type)
{
var tab = type switch
{
"Normal" => (Tab)new NormalTab
{
CutoutMinWidth = 0.31,
CutoutMinHeight = 0.52,
CutoutMaxWidth = 3.7,
CutoutMaxHeight = 4.6,
},
"Machine" => new MachineTab { MachineTabId = 29 },
"Breaker" => new BreakerTab
{
BreakerDepth = 0.013,
BreakerLeadInLength = 0.09,
BreakerAngle = 26,
},
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
tab.Size = 0.41;
tab.TabLeadIn = CreateLeadIn("LineArc");
tab.TabLeadOut = CreateLeadOut("Arc");
return tab;
}
// Compare concrete runtime properties, including nested subtype fields, rather than
// using the serializer under test as the equality oracle. All model objects must be owned.
private static void AssertEquivalent(object? expected, object? actual)
{
if (expected == null)
{
Assert.Null(actual);
return;
}
Assert.NotNull(actual);
var type = expected.GetType();
Assert.Equal(type, actual.GetType());
if (type.IsValueType || expected is string)
{
Assert.Equal(expected, actual);
return;
}
Assert.NotSame(expected, actual);
foreach (var property in type.GetProperties())
AssertEquivalent(property.GetValue(expected), property.GetValue(actual));
}
}