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>
217 lines
8.6 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|