mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 05:45:07 -05:00
The rules behind the WinForms MachineSettings and SharedSettings move into OpenNest.Desktop as MachineSettingsSource and SharedSettingsSource, so the Avalonia app reads machines, clamps and nest defaults the same way: in Database mode the nest server's copy once it holds one (machines as a whole, nest defaults and clamps as single documents), otherwise this PC's files; the copy is re-read after two minutes (one while offline), and while the server is unreachable the last copy read is used, read-only. Legacy clamps still move onto the defaults' or the only machine once, asking at most once per session, and cutting settings edited for a nest still save onto the machine's thickness, or this PC (stamped for that job when the machine cannot be saved). Nothing in the shared code shows UI: it returns the warning texts, takes the clamp-machine question as a delegate and reads the DefaultUnit and CuttingParametersJson settings through IMachineUserSettings (with a user-settings.json implementation under the WinForms names). Cut-off defaults take CutOffSettings instead of a PlateView. MachineSettings and SharedSettings stay in the WinForms project as thin wrappers with the same members, showing the same dialogs and reading Properties.Settings and NestStorage as before, so their callers are unchanged. Tests cover File vs Database mode precedence, the offline read-only copy and its once-per-session warning, staleness and retry timing, uploads, post settings, legacy clamp migration, the cutting-settings save and the user-settings file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
333 lines
13 KiB
C#
333 lines
13 KiB
C#
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Data;
|
|
|
|
namespace OpenNest.Desktop.Tests;
|
|
|
|
/// <summary>
|
|
/// The shared machine entry points: the legacy clamps moving onto a machine once, cutting
|
|
/// settings saved onto the nest's machine thickness or this PC, and the nest defaults.
|
|
/// </summary>
|
|
public sealed class MachineSettingsSourceTests : IDisposable
|
|
{
|
|
private readonly string directory = Path.Combine(Path.GetTempPath(), "OpenNestDesktopTests", Guid.NewGuid().ToString("N"));
|
|
private readonly FakeSettingsServer server = new();
|
|
private readonly MemoryUserSettings user = new();
|
|
|
|
public MachineSettingsSourceTests() => Directory.CreateDirectory(directory);
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(directory, recursive: true); } catch (IOException) { }
|
|
}
|
|
|
|
private string MachinesDirectory => Path.Combine(directory, "Machines");
|
|
|
|
private string DefaultsPath => Path.Combine(directory, "defaults.json");
|
|
|
|
private string ClampsPath => Path.Combine(directory, "clamps.json");
|
|
|
|
private static SharedSettingsSource FileMode() =>
|
|
new(() => new NestStorageSettings(), _ => throw new InvalidOperationException("File mode never reaches the server."));
|
|
|
|
private MachineSettingsSource Machines(SharedSettingsSource? shared = null) =>
|
|
new(shared ?? FileMode(), user, MachinesDirectory, DefaultsPath, ClampsPath, Path.Combine(directory, "grid.json"));
|
|
|
|
private MachineConfig SaveLocalMachine(string name)
|
|
{
|
|
var machine = new MachineConfig { Name = name };
|
|
new LocalJsonProvider(MachinesDirectory).SaveMachine(machine);
|
|
return machine;
|
|
}
|
|
|
|
private MachineConfig Reload(MachineConfig machine) => new LocalJsonProvider(MachinesDirectory).GetMachine(machine.Id)!;
|
|
|
|
private static ClampSettings Legacy() => new()
|
|
{
|
|
Enabled = true,
|
|
Units = Units.Inches,
|
|
Width = 2,
|
|
Depth = 0.5,
|
|
RemovalMinutes = 4,
|
|
Centers = [12, 24],
|
|
};
|
|
|
|
[Fact]
|
|
public void LegacyClamps_GoOntoTheOnlyMachineOnce()
|
|
{
|
|
var only = SaveLocalMachine("Only");
|
|
Legacy().Save(ClampsPath);
|
|
var machines = Machines();
|
|
|
|
Assert.Null(machines.MigrateLegacyClamps(_ => throw new InvalidOperationException("Nothing to ask.")));
|
|
|
|
Assert.Equal([12.0, 24.0], Reload(only).Clamps!.Centers);
|
|
// Once a machine has clamps the legacy file is never read again.
|
|
File.Delete(ClampsPath);
|
|
new ClampSettings { Enabled = true, Units = Units.Inches, Width = 1, Depth = 1, Centers = [99] }.Save(ClampsPath);
|
|
Assert.Null(machines.MigrateLegacyClamps(null!));
|
|
Assert.Equal([12.0, 24.0], Reload(only).Clamps!.Centers);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyClamps_GoOntoTheNestDefaultsMachine()
|
|
{
|
|
var a = SaveLocalMachine("A");
|
|
var b = SaveLocalMachine("B");
|
|
new NestDefaults { MachineId = b.Id }.Save(DefaultsPath);
|
|
Legacy().Save(ClampsPath);
|
|
|
|
Assert.Null(Machines().MigrateLegacyClamps(_ => throw new InvalidOperationException("Nothing to ask.")));
|
|
|
|
Assert.Null(Reload(a).Clamps);
|
|
Assert.NotNull(Reload(b).Clamps);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyClamps_WithSeveralMachinesAndNoDefault_AskOncePerSession()
|
|
{
|
|
var a = SaveLocalMachine("A");
|
|
var b = SaveLocalMachine("B");
|
|
Legacy().Save(ClampsPath);
|
|
var machines = Machines();
|
|
var asked = new List<IReadOnlyList<string>>();
|
|
|
|
machines.MigrateLegacyClamps(choices => { asked.Add(choices.Select(m => m.Name).ToList()); return null; });
|
|
machines.MigrateLegacyClamps(choices => { asked.Add(choices.Select(m => m.Name).ToList()); return choices[1]; });
|
|
|
|
Assert.Equal([["A", "B"]], asked);
|
|
Assert.Null(Reload(a).Clamps);
|
|
Assert.Null(Reload(b).Clamps);
|
|
|
|
// The next session asks again, and the machine picked gets the clamps.
|
|
Assert.Null(Machines().MigrateLegacyClamps(choices => choices[1]));
|
|
Assert.Null(Reload(a).Clamps);
|
|
Assert.NotNull(Reload(b).Clamps);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyClamps_ComeFromTheServersCopyInDatabaseMode()
|
|
{
|
|
var only = new MachineConfig { Name = "Shared Laser" };
|
|
server.Hold(SharedSettingKinds.Machine, only.Id.ToString("D"), SharedMachineProvider.Serialize(only));
|
|
server.Hold(SharedSettingKinds.Clamps, SharedSettingKinds.DefaultKey, Legacy().ToJson());
|
|
var machines = Machines(server.Source(Path.Combine(directory, "shared.json")));
|
|
|
|
Assert.Null(machines.MigrateLegacyClamps(null!));
|
|
|
|
var stored = server.Records.Single(r => r.Kind == SharedSettingKinds.Machine);
|
|
Assert.Equal(2, stored.Revision);
|
|
Assert.NotNull(machines.LoadMachines().Single().Clamps);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyClamps_ThatCannotBeSavedAreReported()
|
|
{
|
|
var only = new MachineConfig { Name = "Shared Laser" };
|
|
server.Hold(SharedSettingKinds.Machine, only.Id.ToString("D"), SharedMachineProvider.Serialize(only));
|
|
Legacy().Save(ClampsPath);
|
|
var shared = server.Source(Path.Combine(directory, "shared.json"));
|
|
var machines = Machines(shared);
|
|
shared.Current(refresh: true);
|
|
server.Online = false;
|
|
|
|
var warning = machines.MigrateLegacyClamps(null!);
|
|
|
|
Assert.Contains("could not be copied to a machine", warning);
|
|
Assert.Equal(1, server.Records.Single().Revision);
|
|
}
|
|
|
|
private static MachineConfig CuttingMachine() => new()
|
|
{
|
|
Name = "Fiber 1",
|
|
Materials =
|
|
{
|
|
new MaterialConfig
|
|
{
|
|
Name = "Mild Steel",
|
|
Grade = "A36",
|
|
Thicknesses = { new ThicknessConfig { Value = 0.25, LeadIn = new LeadConfig { Type = "Line", Length = 0.1, Angle = 60 } } },
|
|
},
|
|
},
|
|
};
|
|
|
|
private static Nest NestFor(MachineConfig machine, double thickness) => new()
|
|
{
|
|
Thickness = thickness,
|
|
Material = new Material("Mild Steel", "A36", 0.2836),
|
|
Machine = new NestMachine(machine.Id, machine.Name),
|
|
};
|
|
|
|
private static CuttingParameters Edited() => new()
|
|
{
|
|
ExternalLeadIn = new LineLeadIn { Length = 0.2, ApproachAngle = 45 },
|
|
SupportTabs = true,
|
|
TabBySize = true,
|
|
TabSizeMaxWidth = 2,
|
|
TabConfig = new NormalTab { Size = 0.03 },
|
|
};
|
|
|
|
[Fact]
|
|
public void CuttingSettings_SaveOntoTheMachinesThicknessAndDropThisPcsCopyForThatJob()
|
|
{
|
|
var machine = CuttingMachine();
|
|
var provider = new LocalJsonProvider(MachinesDirectory);
|
|
provider.SaveMachine(machine);
|
|
var machines = Machines();
|
|
var nest = NestFor(machine, 0.25);
|
|
var stale = new CuttingParameters();
|
|
MachineCuttingDefaults.ApplyTo(stale, machine, nest);
|
|
machines.SaveLastCuttingParameters(stale);
|
|
var warnings = new List<string>();
|
|
|
|
Assert.True(machines.SaveCuttingParameters(nest, Edited(), warnings.Add));
|
|
|
|
Assert.Empty(warnings);
|
|
var thickness = Reload(machine).Materials[0].Thicknesses[0];
|
|
Assert.Equal(0.2, thickness.LeadIn.Length);
|
|
Assert.True(thickness.Cutting!.TabBySize);
|
|
Assert.Equal(2, thickness.Cutting.TabSizeMaxWidth);
|
|
Assert.False(MachineCuttingDefaults.IsRecordedFor(machines.LastCuttingParameters(), machine, nest));
|
|
Assert.True(machines.HasMachineThickness(nest));
|
|
Assert.Equal("Saved for Fiber 1 · Mild Steel A36 0.25 in", machines.DescribeCuttingSource(nest));
|
|
// The machine's thickness is what the next plate starts from.
|
|
Assert.Equal(0.2, ((LineLeadIn)machines.DefaultCuttingParameters(nest).ExternalLeadIn).Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void CuttingSettings_SaveOnThisPcWhenTheMachineHasNoSuchThickness()
|
|
{
|
|
var machine = CuttingMachine();
|
|
new LocalJsonProvider(MachinesDirectory).SaveMachine(machine);
|
|
var machines = Machines();
|
|
var nest = NestFor(machine, 0.5);
|
|
|
|
Assert.False(machines.SaveCuttingParameters(nest, Edited(), _ => Assert.Fail("No warning expected.")));
|
|
|
|
Assert.Null(Reload(machine).Materials[0].Thicknesses[0].Cutting);
|
|
Assert.True(machines.LastCuttingParameters().TabBySize);
|
|
Assert.Null(machines.LastCuttingParameters().MachineName);
|
|
Assert.Equal("This PC's settings: Fiber 1 has no Mild Steel A36 0.5 in.", machines.DescribeCuttingSource(nest));
|
|
}
|
|
|
|
[Fact]
|
|
public void CuttingSettings_SaveOnThisPcWhenTheNestHasNoMachine()
|
|
{
|
|
var machines = Machines();
|
|
var nest = new Nest { Thickness = 0.25, Material = new Material("Mild Steel", "A36", 0.2836) };
|
|
|
|
Assert.False(machines.SaveCuttingParameters(nest, Edited(), _ => Assert.Fail("No warning expected.")));
|
|
|
|
Assert.True(machines.LastCuttingParameters().TabBySize);
|
|
Assert.Equal("This PC's settings: the nest has no machine.", machines.DescribeCuttingSource(nest));
|
|
}
|
|
|
|
[Fact]
|
|
public void CuttingSettings_KeptOnThisPcForThatJobWhenTheServerIsOffline()
|
|
{
|
|
var machine = CuttingMachine();
|
|
server.Hold(SharedSettingKinds.Machine, machine.Id.ToString("D"), SharedMachineProvider.Serialize(machine));
|
|
var shared = server.Source(Path.Combine(directory, "shared.json"));
|
|
var machines = Machines(shared);
|
|
var nest = NestFor(machine, 0.25);
|
|
shared.Current(refresh: true);
|
|
server.Online = false;
|
|
var warnings = new List<string>();
|
|
|
|
Assert.False(machines.SaveCuttingParameters(nest, Edited(), warnings.Add));
|
|
|
|
Assert.Contains("could not be saved to Fiber 1", Assert.Single(warnings));
|
|
Assert.Equal(1, server.Records.Single().Revision);
|
|
var last = machines.LastCuttingParameters();
|
|
Assert.True(MachineCuttingDefaults.IsRecordedFor(last, machine, nest));
|
|
Assert.True(last.TabBySize);
|
|
// Recorded for the job, so the machine's settings do not replace the edit here.
|
|
Assert.Equal(0.2, ((LineLeadIn)machines.DefaultCuttingParameters(nest).ExternalLeadIn).Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void CuttingParametersFor_UsesThePlatesOwnOnlyWhenNoneAreSaved()
|
|
{
|
|
var machines = Machines();
|
|
var plate = new Plate { CuttingParameters = new CuttingParameters { TabBySize = true } };
|
|
var nest = new Nest();
|
|
|
|
Assert.Same(plate.CuttingParameters, machines.CuttingParametersFor(plate, nest));
|
|
|
|
machines.SaveLastCuttingParameters(new CuttingParameters());
|
|
Assert.NotSame(plate.CuttingParameters, machines.CuttingParametersFor(plate, nest));
|
|
}
|
|
|
|
[Fact]
|
|
public void NestDefaults_FallBackToTheDefaultUnitAndAnUnreadableFileWarnsOnce()
|
|
{
|
|
user.DefaultUnit = Units.Millimeters;
|
|
var machines = Machines();
|
|
|
|
Assert.Equal(Units.Millimeters, machines.LoadNestDefaults(out var none).Units);
|
|
Assert.Null(none);
|
|
|
|
File.WriteAllText(DefaultsPath, "{ not json");
|
|
machines.LoadNestDefaults(out var first);
|
|
machines.LoadNestDefaults(out var second);
|
|
|
|
Assert.Contains(DefaultsPath, first);
|
|
Assert.Null(second);
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultMachine_IsTheDefaultsMachineElseTheFirstByName()
|
|
{
|
|
var b = new MachineConfig { Name = "b" };
|
|
var a = new MachineConfig { Name = "A" };
|
|
|
|
Assert.Same(a, MachineSettingsSource.DefaultMachine(new NestDefaults(), [b, a]));
|
|
Assert.Same(b, MachineSettingsSource.DefaultMachine(new NestDefaults { MachineId = b.Id }, [b, a]));
|
|
Assert.Null(MachineSettingsSource.DefaultMachine(null!, []));
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalMachines_StartWithTheBuiltInMachine()
|
|
{
|
|
var loaded = Machines().LoadMachines();
|
|
|
|
Assert.NotEmpty(loaded);
|
|
Assert.True(Directory.GetFiles(MachinesDirectory, "*.json").Length > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void UserSettingsFile_KeepsBothSettingsUnderTheWinFormsNames()
|
|
{
|
|
var path = Path.Combine(directory, "user-settings.json");
|
|
UserSettingsFile.Write(path, new Dictionary<string, string> { ["DefaultUnit"] = "Millimeters", ["Other"] = "kept" });
|
|
var settings = new MachineUserSettingsFile(path);
|
|
|
|
Assert.Equal(Units.Millimeters, settings.DefaultUnit);
|
|
Assert.Equal("", settings.CuttingParametersJson);
|
|
|
|
settings.CuttingParametersJson = "{\"a\":1}";
|
|
|
|
var values = UserSettingsFile.Read(path);
|
|
Assert.Equal("{\"a\":1}", values["CuttingParametersJson"]);
|
|
Assert.Equal("kept", values["Other"]);
|
|
Assert.Equal("{\"a\":1}", settings.CuttingParametersJson);
|
|
|
|
UserSettingsFile.Write(path, new Dictionary<string, string> { ["DefaultUnit"] = "Furlongs" });
|
|
Assert.Equal(Units.Inches, settings.DefaultUnit);
|
|
UserSettingsFile.Write(path, new Dictionary<string, string> { ["DefaultUnit"] = "7" });
|
|
Assert.Equal(Units.Inches, settings.DefaultUnit);
|
|
}
|
|
|
|
[Fact]
|
|
public void CutOffDefaults_ComeFromTheNestsMachineThickness()
|
|
{
|
|
var machine = CuttingMachine();
|
|
machine.Materials[0].Thicknesses[0].CutOff = new CutOffConfig { PartClearance = 0.75 };
|
|
new LocalJsonProvider(MachinesDirectory).SaveMachine(machine);
|
|
var settings = new CutOffSettings { PartClearance = 0.1 };
|
|
|
|
Machines().ApplyCutOffDefaults(settings, NestFor(machine, 0.25));
|
|
|
|
Assert.Equal(0.75, settings.PartClearance);
|
|
}
|
|
}
|