WriteProgramAsync resolved -path as a directory and combined it with
program.Name ("1" + "O0001" -> "1/O0001"), while ReadProgramAsync
resolved it as a program identifier under ProgramDirectory ("1" ->
"/PRG/1"). The write target therefore never matched the read target:
STOR went to a nonexistent relative dir and failed with "unable to
connect to remote server" (-204). Surfaced once ISS-018/019 stopped
masking it.
Per Mitsubishi NC Explorer manual IB-1500904, programs live under
PRG/USER and are addressed by their program name/number with no
extension. Resolve write the same way as read: ResolvePath is now
single-arg (path = identifier or full path, joined with
ProgramDirectory). program.Name is used for validation only.
Update write unit test to the symmetric contract and add a roundtrip
regression test (-path "1" -> /PRG/1).
Siemens/Fagor share the pattern but tie extension handling to name
(entangled with ISS-013); left open in ISS-020.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
181 lines
6.8 KiB
C#
181 lines
6.8 KiB
C#
using System;
|
|
using NUnit.Framework;
|
|
using Moq;
|
|
using NcProgramManager.Cnc;
|
|
using NcProgramManager.Cnc.Models;
|
|
using NcProgramManager.Cnc.Mitsubishi;
|
|
|
|
namespace NcProgramManager.Tests.Unit
|
|
{
|
|
[TestFixture]
|
|
public class MitsubishiMachineTests
|
|
{
|
|
private MitsubishiConnectionConfig _config;
|
|
private Mock<IMitsubishiFtpClient> _ftpMock;
|
|
private Mock<INcProgramValidator> _validatorMock;
|
|
private MitsubishiMachine _machine;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_config = new MitsubishiConnectionConfig
|
|
{
|
|
Name = "TestMitsubishi",
|
|
IpAddress = "192.168.1.3",
|
|
Port = 21,
|
|
ProgramDirectory = "/PRG/"
|
|
};
|
|
_ftpMock = new Mock<IMitsubishiFtpClient>(MockBehavior.Strict);
|
|
// Always-ok validator so existing write tests are not affected by validation rules
|
|
_validatorMock = new Mock<INcProgramValidator>();
|
|
_validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
|
|
.Returns(ValidationResult.Ok());
|
|
_machine = new MitsubishiMachine(_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(_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 — no .MPF extension for Mitsubishi
|
|
|
|
[Test]
|
|
public void ReadProgramAsync_FullPath_CallsDownloadFileNoExtension()
|
|
{
|
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
|
_ftpMock.Setup(f => f.DownloadFile("/PRG/O1234")).Returns("NC_DATA");
|
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
|
|
|
var result = _machine.ReadProgramAsync("/PRG/O1234").GetAwaiter().GetResult();
|
|
|
|
Assert.That(result.Success, Is.True);
|
|
Assert.That(result.Value, Is.EqualTo("NC_DATA"));
|
|
_ftpMock.Verify(f => f.DownloadFile("/PRG/O1234"), Times.Once);
|
|
}
|
|
|
|
[Test]
|
|
public void ReadProgramAsync_ShortName_ResolvesWithDirectoryNoExtension()
|
|
{
|
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
|
_ftpMock.Setup(f => f.DownloadFile("/PRG/O1234")).Returns("DATA");
|
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
|
|
|
_machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
|
|
|
|
// Must NOT append .MPF
|
|
_ftpMock.Verify(f => f.DownloadFile("/PRG/O1234"), Times.Once);
|
|
_ftpMock.Verify(f => f.DownloadFile(It.Is<string>(s => s.EndsWith(".MPF"))), Times.Never);
|
|
}
|
|
|
|
// WriteProgramAsync
|
|
|
|
[Test]
|
|
public void WriteProgramAsync_FullPath_CallsUploadFileNoExtension()
|
|
{
|
|
// ISS-020: write resolves the path identically to read (path = program
|
|
// identifier / full path, not a directory combined with program.Name).
|
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
|
_ftpMock.Setup(f => f.UploadFile("/PRG/O1234", "CONTENT"));
|
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
|
|
|
var prog = new CncProgram { Name = "O1234", Content = "CONTENT" };
|
|
var result = _machine.WriteProgramAsync("/PRG/O1234", prog).GetAwaiter().GetResult();
|
|
|
|
Assert.That(result.Success, Is.True);
|
|
_ftpMock.Verify(f => f.UploadFile("/PRG/O1234", "CONTENT"), Times.Once);
|
|
_ftpMock.Verify(f => f.UploadFile(It.Is<string>(s => s.EndsWith(".MPF")), It.IsAny<string>()), Times.Never);
|
|
}
|
|
|
|
[Test]
|
|
public void WriteProgramAsync_ShortName_ResolvesWithDirectory_SymmetricWithRead()
|
|
{
|
|
// ISS-020 regression: -path "1" must write to ProgramDirectory + "1"
|
|
// (the same location ReadProgramAsync("1") reads), so roundtrip works.
|
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
|
_ftpMock.Setup(f => f.UploadFile("/PRG/1", "CONTENT"));
|
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
|
|
|
var prog = new CncProgram { Name = "O0001", Content = "CONTENT" };
|
|
var result = _machine.WriteProgramAsync("1", prog).GetAwaiter().GetResult();
|
|
|
|
Assert.That(result.Success, Is.True);
|
|
_ftpMock.Verify(f => f.UploadFile("/PRG/1", "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 = "O1234", Content = "" };
|
|
var result = _machine.WriteProgramAsync("/PRG/", 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/", 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_ValidPath_CallsDeleteFile()
|
|
{
|
|
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
|
_ftpMock.Setup(f => f.DeleteFile("/PRG/O1234"));
|
|
_machine.ConnectAsync().GetAwaiter().GetResult();
|
|
|
|
var result = _machine.DeleteProgramAsync("/PRG/O1234").GetAwaiter().GetResult();
|
|
|
|
Assert.That(result.Success, Is.True);
|
|
_ftpMock.Verify(f => f.DeleteFile("/PRG/O1234"), Times.Once);
|
|
}
|
|
|
|
// ReadActiveProgramAsync
|
|
|
|
[Test]
|
|
public void ReadActiveProgramAsync_ReturnsFailNotSupported()
|
|
{
|
|
var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();
|
|
|
|
Assert.That(result.Success, Is.False);
|
|
}
|
|
}
|
|
}
|