nc_program_manager/NcProgramManager.Tests/Integration/MazakMachineIntegrationTests.cs
dtrentin 19da0e598e feat(mazak): add Mazak CNC support via FTP (EIA/ISO)
New Cnc/Mazak/ machine integration mirroring the Fagor/Mitsubishi FTP
pattern: MazakConnectionConfig, IMazakFtpClient, MazakFtpClient,
MazakMachine (ICncMachine). Transfers EIA/ISO G-code text over FTP to
Smooth-family controllers. Mazatrol CMT (proprietary binary) out of scope.

Design (verified against Mazak Matrix EIA manual + field reports):
- Filenames preserved verbatim; no O#### rename, no extension append.
- Symmetric ResolvePath for Read/Write/Delete (avoids ISS-020 class;
  Delete resolved too, unlike copied Mitsubishi source).
- Validator = full passthrough for Mazak (EIA permits ';' EOB, '[ ]',
  '#'; restrictive Fanuc checks would false-reject valid programs).
- Port 21 default, -porta=23 for Smooth Ai IIS variant.
- ProgramDirectory site-specific, no default.

Wiring: CncManufacturer.Mazak, ArgParser 'mazak' token, CncMachineFactory.
Tests: MazakMachineTests (unit), MazakMachineIntegrationTests (FTP-stub
roundtrip + symmetry guard), Mazak cases in validator/factory tests.
Docs: README Controller Mazak section + table row; ISS-021 (constraints,
EIA-option requirement), ISS-022 (Mitsubishi delete asymmetry logged).

NOT build-verified (no .NET toolchain in build env) — verify on Windows.

Projects: NcProgramManager, NcProgramManager.Tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:12:07 +02:00

132 lines
5.2 KiB
C#

using System;
using NUnit.Framework;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Mazak;
using NcProgramManager.Tests.Integration.Stubs;
namespace NcProgramManager.Tests.Integration
{
[TestFixture]
[Category("Integration")]
public class MazakMachineIntegrationTests
{
private FtpServerStub _stub;
private MazakMachine _machine;
private const string ProgramDir = "/PRG/";
[SetUp]
public void SetUp()
{
_stub = new FtpServerStub();
var config = new MazakConnectionConfig
{
Name = "MazakIntegration",
IpAddress = "127.0.0.1",
Port = _stub.Port,
Username = "user",
Password = "pass",
ProgramDirectory = ProgramDir,
RequestTimeout = TimeSpan.FromSeconds(5)
};
_machine = new MazakMachine(config);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
_stub?.Dispose();
}
[Test]
public void ConnectAsync_StubListens_ResultOk()
{
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void WriteThenRead_ProgramRoundTripsSamePathNoExtension()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
// Bare identifier "1234"; Mazak validator is passthrough but use clean EIA text.
const string identifier = "1234";
string content = "O1234\nG00 X10\nM30";
var prog = new CncProgram { Name = identifier, Content = content };
// Write target == ResolvePath(identifier) == ProgramDir + identifier (verbatim, no extension)
var writeResult = _machine.WriteProgramAsync(identifier, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
string storedPath = ProgramDir + identifier;
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True,
"File should be stored verbatim at " + storedPath);
Assert.That(_stub.Files.ContainsKey(storedPath + ".EIA"), Is.False,
"File must NOT be stored with an appended extension");
// Read SAME bare identifier -> ResolvePath resolves to the same target (ISS-020 symmetry)
var readResult = _machine.ReadProgramAsync(identifier).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.True);
Assert.That(readResult.Value, Is.EqualTo(content),
"Read content must match written content for the same identifier (ResolvePath symmetry)");
}
[Test]
public void WriteThenDelete_ProgramRemoved_SubsequentReadFails()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
const string identifier = "1234";
string content = "O1234\nG00 X10\nM30";
var prog = new CncProgram { Name = identifier, Content = content };
var writeResult = _machine.WriteProgramAsync(identifier, prog).GetAwaiter().GetResult();
Assert.That(writeResult.Success, Is.True);
string storedPath = ProgramDir + identifier;
Assert.That(_stub.Files.ContainsKey(storedPath), Is.True);
// ISS-020 symmetry: delete the SAME bare identifier used for write.
// ResolvePath(identifier) -> storedPath, so write/delete/read all align.
var deleteResult = _machine.DeleteProgramAsync(identifier).GetAwaiter().GetResult();
Assert.That(deleteResult.Success, Is.True);
Assert.That(_stub.Files.ContainsKey(storedPath), Is.False);
var readResult = _machine.ReadProgramAsync(identifier).GetAwaiter().GetResult();
Assert.That(readResult.Success, Is.False, "Read after delete must fail / not found");
}
[Test]
public void ReadProgramAsync_FileNotInStub_ResultFail()
{
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("9999").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
[Test]
[Category("Hardware")]
public void Hardware_ConnectAsync_RealMachine()
{
string ip = HardwareTestHelper.GetIpOrSkip("MAZAK_TEST_IP");
var config = new MazakConnectionConfig
{
Name = "MazakHardware",
IpAddress = ip,
Port = 21,
Username = Environment.GetEnvironmentVariable("MAZAK_USER") ?? "user",
Password = Environment.GetEnvironmentVariable("MAZAK_PASS") ?? "pass",
ProgramDirectory = "/PRG/",
RequestTimeout = TimeSpan.FromSeconds(10)
};
using (var machine = new MazakMachine(config))
{
var result = machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True, "ConnectAsync failed: " + string.Join("; ", result.Errors));
}
}
}
}