nc_program_manager/NcProgramManager.Tests/Unit/MazakMachineTests.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

217 lines
8.6 KiB
C#

using System;
using NUnit.Framework;
using Moq;
using NcProgramManager.Cnc;
using NcProgramManager.Cnc.Models;
using NcProgramManager.Cnc.Mazak;
namespace NcProgramManager.Tests.Unit
{
[TestFixture]
public class MazakMachineTests
{
private MazakConnectionConfig _config;
private Mock<IMazakFtpClient> _ftpMock;
private Mock<INcProgramValidator> _validatorMock;
private MazakMachine _machine;
[SetUp]
public void SetUp()
{
_config = new MazakConnectionConfig
{
Name = "TestMazak",
IpAddress = "192.168.1.4",
Port = 21,
ProgramDirectory = "/PRG/"
};
_ftpMock = new Mock<IMazakFtpClient>(MockBehavior.Strict);
// Always-ok validator: Mazak addresses files verbatim (passthrough),
// so validation must not interfere with path/verbatim assertions.
_validatorMock = new Mock<INcProgramValidator>();
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
.Returns(ValidationResult.Ok());
_machine = new MazakMachine(_config, _ftpMock.Object, _validatorMock.Object);
}
[TearDown]
public void TearDown()
{
_machine?.Dispose();
}
// ConnectAsync
[Test]
public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.True);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
}
[Test]
public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
{
_ftpMock.Setup(f => f.Ping()).Returns(false);
var result = _machine.ConnectAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
}
// ReadProgramAsync - verbatim filename, NO extension appended
[Test]
public void ReadProgramAsync_FullPath_CallsDownloadFileWithExactPath()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("EIA_DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.ReadProgramAsync("/PRG/1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
Assert.That(result.Value, Is.EqualTo("EIA_DATA"));
_ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once);
}
[Test]
public void ReadProgramAsync_BareIdentifier_ResolvesWithDirectoryNoExtension()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
// Bare "1234" -> ProgramDirectory + "1234" verbatim, NO .eia / extension.
_ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("DATA");
_machine.ConnectAsync().GetAwaiter().GetResult();
_machine.ReadProgramAsync("1234").GetAwaiter().GetResult();
_ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once);
// Must NOT append any extension (e.g. .eia / .EIA).
_ftpMock.Verify(f => f.DownloadFile(It.Is<string>(s => s.Contains("."))), Times.Never);
}
// WriteProgramAsync
[Test]
public void WriteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithRead()
{
// ISS-020 symmetry regression: write("1234") must upload to the SAME
// resolved path that read("1234") downloads from ("/PRG/1234"), so the
// roundtrip targets one location. Path comes from the identifier verbatim,
// NOT from a directory combined with program.Name.
const string readPath = "/PRG/1234"; // path used by ReadProgramAsync("1234")
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile(readPath, "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
// program.Name deliberately differs from the identifier to prove the path
// is built from the identifier ("1234"), not from program.Name.
var prog = new CncProgram { Name = "O0001", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("1234", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
// write-path == read-path for identical input -> symmetry guard.
_ftpMock.Verify(f => f.UploadFile(readPath, "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_FullPath_CallsUploadFileWithExactPath()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile("/PRG/1234", "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "1234", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.UploadFile("/PRG/1234", "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_LetterName_VerbatimPassthroughNoRename()
{
// Passthrough proof: a letter name "MYPART.NC" is uploaded to
// ProgramDirectory + "MYPART.NC" UNCHANGED - no rename, no O#### enforcement,
// no extension mangling. Validator is always-ok (Mazak passthrough).
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "MYPART.NC", Content = "CONTENT" };
var result = _machine.WriteProgramAsync("MYPART.NC", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT"), Times.Once);
}
[Test]
public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_machine.ConnectAsync().GetAwaiter().GetResult();
var prog = new CncProgram { Name = "1234", Content = "" };
var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
[Test]
public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
{
_ftpMock.Setup(f => f.Ping()).Returns(true);
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.WriteProgramAsync("/PRG/1234", null).GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
_ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
// DeleteProgramAsync
[Test]
public void DeleteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithReadWrite()
{
// ISS-020 symmetry regression: delete("1234") must target the SAME
// resolved path read/write use for "1234" ("/PRG/1234"). Path comes from
// ResolvePath(identifier), NOT the raw identifier verbatim.
_ftpMock.Setup(f => f.Ping()).Returns(true);
_ftpMock.Setup(f => f.DeleteFile("/PRG/1234"));
_machine.ConnectAsync().GetAwaiter().GetResult();
var result = _machine.DeleteProgramAsync("1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.True);
_ftpMock.Verify(f => f.DeleteFile("/PRG/1234"), Times.Once);
}
// ReadActiveProgramAsync - not supported via FTP
[Test]
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
// SelectMainProgramAsync - not supported via FTP
[Test]
public void SelectMainProgramAsync_ReturnsFailNotSupported()
{
var result = _machine.SelectMainProgramAsync("/PRG/1234").GetAwaiter().GetResult();
Assert.That(result.Success, Is.False);
}
}
}