fix(mazak): harden FTP list parsing + surface root-list failure
- FtpListParser: tolerant DOS/IIS regex (optional AM/PM, 2-or-4-digit year, 1-or-2-digit hour) for 24h/locale-dependent Mazak servers; NLST fallback now only accepts single-token lines (multi-token garbage/header -> skip) to avoid whole-line-as-filename. - MazakMachine.BuildTreeAsync: root-level LIST failure now propagates -> Fail (no more silent exit 0 on a diagnostic probe). Children keep best-effort inline "errore lista" swallow. Depth semantics preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19da0e598e
commit
89487256d7
13 changed files with 641 additions and 2 deletions
|
|
@ -75,6 +75,10 @@ namespace NcProgramManager
|
|||
if (!string.IsNullOrEmpty(pathprogramma))
|
||||
inputArgs.pathLocaleProgramma = pathprogramma.Substring(15);
|
||||
|
||||
var ftpDir = arguments.Find(it => new Regex("-ftpdir=.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||
if (!string.IsNullOrEmpty(ftpDir))
|
||||
inputArgs.ftpDir = ftpDir.Substring(8);
|
||||
|
||||
var hasToolOffsetData = arguments.Find(it => new Regex("-tooloffset.*", RegexOptions.IgnoreCase).IsMatch(it));
|
||||
if (!string.IsNullOrEmpty(hasToolOffsetData))
|
||||
inputArgs.hasToolOffsetData = true;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ namespace NcProgramManager.Cnc
|
|||
IpAddress = args.ip,
|
||||
Port = string.IsNullOrEmpty(args.porta) ? 21 : int.Parse(args.porta),
|
||||
Username = args.username,
|
||||
Password = args.password
|
||||
Password = args.password,
|
||||
ProgramDirectory = string.IsNullOrEmpty(args.ftpDir) ? "/" : args.ftpDir
|
||||
});
|
||||
|
||||
default: // Fanuc
|
||||
|
|
|
|||
24
Cnc/Mazak/FtpEntry.cs
Normal file
24
Cnc/Mazak/FtpEntry.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace NcProgramManager.Cnc.Mazak
|
||||
{
|
||||
/// <summary>Single entry of an FTP directory listing (file or directory).</summary>
|
||||
public sealed class FtpEntry
|
||||
{
|
||||
/// <summary>Entry name (no path).</summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>Full FTP path of the entry (parent dir + name).</summary>
|
||||
public string FullPath { get; set; }
|
||||
/// <summary>True if the entry is a directory.</summary>
|
||||
public bool IsDirectory { get; set; }
|
||||
/// <summary>File size in bytes; -1 when unknown (directories).</summary>
|
||||
public long Size { get; set; } = -1;
|
||||
/// <summary>Raw LIST line as returned by the server (diagnostics).</summary>
|
||||
public string Raw { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsDirectory
|
||||
? string.Format("[DIR] {0}", Name)
|
||||
: string.Format(" {0} ({1} B)", Name, Size >= 0 ? Size.ToString() : "?");
|
||||
}
|
||||
}
|
||||
}
|
||||
91
Cnc/Mazak/FtpListParser.cs
Normal file
91
Cnc/Mazak/FtpListParser.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NcProgramManager.Cnc.Mazak
|
||||
{
|
||||
/// <summary>
|
||||
/// Parsing puro (senza I/O) delle righe LIST/ListDirectoryDetails FTP dei
|
||||
/// controlli Mazak Smooth/Matrix. Supporta formato Unix (drwx...) e DOS/IIS
|
||||
/// (MM-DD-YY ... <DIR>), piu' fallback NLST (solo nome).
|
||||
/// </summary>
|
||||
internal static class FtpListParser
|
||||
{
|
||||
/// <summary>Assicura path con slash iniziale e finale per uso come directory.</summary>
|
||||
public static string NormalizeDir(string path)
|
||||
{
|
||||
string dir = string.IsNullOrEmpty(path) ? "/" : path;
|
||||
if (!dir.StartsWith("/")) dir = "/" + dir;
|
||||
if (!dir.EndsWith("/")) dir += "/";
|
||||
return dir;
|
||||
}
|
||||
|
||||
// DOS/IIS: "07-18-26 10:00AM <DIR> NAME"
|
||||
// "07-18-26 10:00AM 1234 FILE.EIA"
|
||||
private static readonly Regex _dosLine = new Regex(
|
||||
@"^\d{2}-\d{2}-\d{2,4}\s+\d{1,2}:\d{2}\s*(AM|PM)?\s+(?<dir><DIR>|\d+)\s+(?<name>.+)$",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
// Whitespace chars used to detect multi-token (non-NLST) lines in fallback.
|
||||
private static readonly char[] _whitespace = { ' ', '\t', '\v', '\f' };
|
||||
|
||||
// Unix: "drwxr-xr-x 1 owner group 4096 Jul 18 10:00 NAME"
|
||||
private static readonly Regex _unixLine = new Regex(
|
||||
@"^(?<perm>[dl\-][rwxsStT\-]{9})\s+\d+\s+\S+\s+\S+\s+(?<size>\d+)\s+\S+\s+\S+\s+\S+\s+(?<name>.+)$");
|
||||
|
||||
/// <summary>Parsa una riga LIST. Ritorna null se non riconosciuta o '.'/'..'.</summary>
|
||||
public static FtpEntry Parse(string line, string dir)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return null;
|
||||
|
||||
string name = null;
|
||||
bool isDir = false;
|
||||
long size = -1;
|
||||
|
||||
var dos = _dosLine.Match(line);
|
||||
if (dos.Success)
|
||||
{
|
||||
name = dos.Groups["name"].Value.Trim();
|
||||
if (string.Equals(dos.Groups["dir"].Value, "<DIR>", StringComparison.OrdinalIgnoreCase))
|
||||
isDir = true;
|
||||
else
|
||||
long.TryParse(dos.Groups["dir"].Value, out size);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unix = _unixLine.Match(line);
|
||||
if (unix.Success)
|
||||
{
|
||||
name = unix.Groups["name"].Value.Trim();
|
||||
isDir = unix.Groups["perm"].Value[0] == 'd';
|
||||
if (!isDir)
|
||||
long.TryParse(unix.Groups["size"].Value, NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture, out size);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: NLST-style (bare name only). Only a single token
|
||||
// (no internal whitespace) is a real name-only listing; a line
|
||||
// with internal whitespace that matched neither regex is an
|
||||
// unrecognized header/garbage -> skip (avoid whole-line-as-name).
|
||||
string trimmed = line.Trim();
|
||||
if (trimmed.IndexOfAny(_whitespace) >= 0)
|
||||
return null;
|
||||
name = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(name) || name == "." || name == "..")
|
||||
return null;
|
||||
|
||||
return new FtpEntry
|
||||
{
|
||||
Name = name,
|
||||
FullPath = dir + name,
|
||||
IsDirectory = isDir,
|
||||
Size = size,
|
||||
Raw = line
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NcProgramManager.Cnc.Mazak
|
||||
{
|
||||
internal interface IMazakFtpClient
|
||||
{
|
||||
bool Ping();
|
||||
IReadOnlyList<FtpEntry> ListDirectory(string path);
|
||||
string DownloadFile(string ncPath);
|
||||
void UploadFile(string ncPath, string content);
|
||||
void DeleteFile(string ncPath);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
|
@ -29,6 +30,31 @@ namespace NcProgramManager.Cnc.Mazak
|
|||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lista una directory FTP (LIST/ListDirectoryDetails). Ritorna file e
|
||||
/// sotto-cartelle con nome, path completo, flag directory e dimensione.
|
||||
/// Parsa sia il formato Unix (drwx...) sia quello DOS/IIS (MM-DD-YY ... <DIR>),
|
||||
/// cioe' i due layout esposti dai controlli Mazak Smooth/Matrix.
|
||||
/// </summary>
|
||||
public IReadOnlyList<FtpEntry> ListDirectory(string path)
|
||||
{
|
||||
string dir = FtpListParser.NormalizeDir(path);
|
||||
var req = CreateRequest(dir, WebRequestMethods.Ftp.ListDirectoryDetails);
|
||||
var entries = new List<FtpEntry>();
|
||||
using (var resp = (FtpWebResponse)req.GetResponse())
|
||||
using (var reader = new StreamReader(resp.GetResponseStream(), Encoding.ASCII))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var entry = FtpListParser.Parse(line, dir);
|
||||
if (entry != null)
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>Scarica file dal Mazak in stringa.</summary>
|
||||
public string DownloadFile(string ncPath)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -135,6 +135,85 @@ namespace NcProgramManager.Cnc.Mazak
|
|||
}, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lista una directory FTP del Mazak (un solo livello). Utile per sondare
|
||||
/// la connessione e verificare che <see cref="MazakConnectionConfig.ProgramDirectory"/>
|
||||
/// sia vista correttamente e quali file EIA/ISO siano disponibili.
|
||||
/// Path vuoto = ProgramDirectory configurata.
|
||||
/// </summary>
|
||||
public Task<CncResult<IReadOnlyList<FtpEntry>>> ListDirectoryAsync(string path = null, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
return RunGuardedAsync(() =>
|
||||
{
|
||||
EnsureConnected();
|
||||
string dir = string.IsNullOrEmpty(path) ? _config.ProgramDirectory : path;
|
||||
try { return _ftp.ListDirectory(dir); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errors.Add(new CncError(0, "ListDirectory", ex.Message));
|
||||
return null;
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce un albero testuale (tree) della directory FTP a partire da
|
||||
/// <paramref name="path"/>, scendendo ricorsivamente fino a
|
||||
/// <paramref name="maxDepth"/> livelli. Diagnostica: mostra come il server
|
||||
/// FTP del Mazak espone la cartella programmi e quali file contiene.
|
||||
/// Errori per singola sottocartella sono annotati inline e non interrompono
|
||||
/// l'intero albero.
|
||||
/// </summary>
|
||||
public Task<CncResult<string>> BuildTreeAsync(string path = null, int maxDepth = 2, CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
return RunGuardedAsync(() =>
|
||||
{
|
||||
EnsureConnected();
|
||||
string root = string.IsNullOrEmpty(path) ? _config.ProgramDirectory : path;
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine(string.IsNullOrEmpty(root) ? "/" : root);
|
||||
// Root-level list failure MUST propagate -> RunGuardedAsync -> Fail.
|
||||
// A diagnostic probe that can't even list its root must not exit 0.
|
||||
var rootEntries = _ftp.ListDirectory(root);
|
||||
AppendEntries(sb, rootEntries, "", maxDepth - 1, ct);
|
||||
return sb.ToString();
|
||||
}, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rende gli entry di una directory e ricorre nelle sottocartelle finche'
|
||||
/// <paramref name="depthLeft"/> > 0. Best-effort per i figli: gli errori
|
||||
/// di lista delle sottocartelle sono annotati inline e non interrompono
|
||||
/// l'intero albero (la root e' gestita a monte e propaga).
|
||||
/// </summary>
|
||||
private void AppendEntries(System.Text.StringBuilder sb, IReadOnlyList<FtpEntry> entries, string prefix, int depthLeft, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var e = entries[i];
|
||||
bool last = i == entries.Count - 1;
|
||||
string connector = last ? "└─ " : "├─ "; // "└─ " / "├─ "
|
||||
sb.AppendLine(string.Format("{0}{1}{2}{3}",
|
||||
prefix, connector, e.Name,
|
||||
e.IsDirectory ? "/" : (e.Size >= 0 ? string.Format(" ({0} B)", e.Size) : "")));
|
||||
|
||||
if (e.IsDirectory && depthLeft > 0)
|
||||
{
|
||||
string childPrefix = prefix + (last ? " " : "│ "); // "│ "
|
||||
IReadOnlyList<FtpEntry> childEntries;
|
||||
try { childEntries = _ftp.ListDirectory(e.FullPath); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
sb.AppendLine(string.Format("{0}!! <errore lista: {1}>", childPrefix, ex.Message));
|
||||
continue;
|
||||
}
|
||||
AppendEntries(sb, childEntries, childPrefix, depthLeft - 1, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<CncResult<MachineSnapshot>> ReadAsync(CancellationToken ct = default(CancellationToken))
|
||||
{
|
||||
return RunGuardedAsync(() =>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace NcProgramManager
|
|||
public string password = "";
|
||||
public string commentoProgramma = "";
|
||||
public string pathLocaleProgramma = "";
|
||||
public string ftpDir = ""; // Mazak: remote FTP program directory (LISTA/probe)
|
||||
public bool compatibilityMode = false;
|
||||
public bool hasToolOffsetData = false;
|
||||
public bool hasWorkZeroOffsetData = false;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
<Compile Include="Unit\HeidenhainMachineTests.cs" />
|
||||
<Compile Include="Unit\FagorMachineTests.cs" />
|
||||
<Compile Include="Unit\MazakMachineTests.cs" />
|
||||
<Compile Include="Unit\FtpListParserTests.cs" />
|
||||
<Compile Include="Integration\Stubs\FtpServerStub.cs" />
|
||||
<Compile Include="Integration\Stubs\Lsv2ServerStub.cs" />
|
||||
<Compile Include="Integration\HardwareTestHelper.cs" />
|
||||
|
|
|
|||
189
NcProgramManager.Tests/Unit/FtpListParserTests.cs
Normal file
189
NcProgramManager.Tests/Unit/FtpListParserTests.cs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
using NUnit.Framework;
|
||||
using NcProgramManager.Cnc.Mazak;
|
||||
|
||||
namespace NcProgramManager.Tests.Unit
|
||||
{
|
||||
[TestFixture]
|
||||
public class FtpListParserTests
|
||||
{
|
||||
// NormalizeDir
|
||||
|
||||
[Test]
|
||||
public void NormalizeDir_Empty_ReturnsRoot()
|
||||
{
|
||||
Assert.That(FtpListParser.NormalizeDir(""), Is.EqualTo("/"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeDir_Null_ReturnsRoot()
|
||||
{
|
||||
Assert.That(FtpListParser.NormalizeDir(null), Is.EqualTo("/"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeDir_NoSlashes_AddsBoth()
|
||||
{
|
||||
Assert.That(FtpListParser.NormalizeDir("PRG"), Is.EqualTo("/PRG/"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeDir_NoTrailingSlash_AddsTrailing()
|
||||
{
|
||||
Assert.That(FtpListParser.NormalizeDir("/a/b"), Is.EqualTo("/a/b/"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeDir_AlreadyNormalized_Unchanged()
|
||||
{
|
||||
Assert.That(FtpListParser.NormalizeDir("/a/b/"), Is.EqualTo("/a/b/"));
|
||||
}
|
||||
|
||||
// Parse - DOS/IIS format
|
||||
|
||||
[Test]
|
||||
public void Parse_DosDir_DirectoryEntry()
|
||||
{
|
||||
var e = FtpListParser.Parse("07-18-26 10:00AM <DIR> MYDIR", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("MYDIR"));
|
||||
Assert.That(e.IsDirectory, Is.True);
|
||||
Assert.That(e.Size, Is.EqualTo(-1));
|
||||
Assert.That(e.FullPath, Is.EqualTo("/PRG/MYDIR"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DosFile_FileEntryWithSize()
|
||||
{
|
||||
var e = FtpListParser.Parse("07-18-26 10:00AM 1234 O1000.EIA", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("O1000.EIA"));
|
||||
Assert.That(e.IsDirectory, Is.False);
|
||||
Assert.That(e.Size, Is.EqualTo(1234));
|
||||
}
|
||||
|
||||
// Parse - DOS/IIS format, T4 tolerances (24h time, 4-digit year, spaces)
|
||||
|
||||
[Test]
|
||||
public void Parse_Dos24hTime_DirectoryEntry()
|
||||
{
|
||||
// 24h time, no AM/PM: _dosLine \s*(AM|PM)? optional absorbs this.
|
||||
var e = FtpListParser.Parse("07-18-26 22:00 <DIR> NIGHTDIR", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("NIGHTDIR"));
|
||||
Assert.That(e.IsDirectory, Is.True);
|
||||
Assert.That(e.Size, Is.EqualTo(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_Dos4DigitYear_FileEntryWithSize()
|
||||
{
|
||||
// 4-digit year: date group is \d{2}-\d{2}-\d{2,4}.
|
||||
var e = FtpListParser.Parse("07-18-2026 10:00AM 2048 O4000.EIA", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("O4000.EIA"));
|
||||
Assert.That(e.IsDirectory, Is.False);
|
||||
Assert.That(e.Size, Is.EqualTo(2048));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DosFileNameWithSpaces_KeepsSpaces()
|
||||
{
|
||||
// name group is .+ (greedy to $) -> internal spaces preserved.
|
||||
var e = FtpListParser.Parse("07-18-26 10:00AM 128 MY PROG.EIA", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("MY PROG.EIA"));
|
||||
Assert.That(e.IsDirectory, Is.False);
|
||||
Assert.That(e.Size, Is.EqualTo(128));
|
||||
}
|
||||
|
||||
// Parse - Unix format
|
||||
|
||||
[Test]
|
||||
public void Parse_UnixFileNameWithSpaces_KeepsSpaces()
|
||||
{
|
||||
// Unix name group is .+ -> internal spaces preserved.
|
||||
var e = FtpListParser.Parse("-rw-r--r-- 1 own grp 64 Jul 18 10:00 MY PROG", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("MY PROG"));
|
||||
Assert.That(e.IsDirectory, Is.False);
|
||||
Assert.That(e.Size, Is.EqualTo(64));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_UnixDir_DirectoryEntry()
|
||||
{
|
||||
var e = FtpListParser.Parse("drwxr-xr-x 1 own grp 4096 Jul 18 10:00 sub", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.IsDirectory, Is.True);
|
||||
Assert.That(e.Name, Is.EqualTo("sub"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_UnixFile_FileEntryWithSize()
|
||||
{
|
||||
var e = FtpListParser.Parse("-rw-r--r-- 1 own grp 512 Jul 18 10:00 O2000", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.IsDirectory, Is.False);
|
||||
Assert.That(e.Size, Is.EqualTo(512));
|
||||
Assert.That(e.Name, Is.EqualTo("O2000"));
|
||||
}
|
||||
|
||||
// Parse - skip '.' and '..'
|
||||
|
||||
[Test]
|
||||
public void Parse_DotDir_ReturnsNull()
|
||||
{
|
||||
var e = FtpListParser.Parse("07-18-26 10:00AM <DIR> .", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Parse_DotDotDir_ReturnsNull()
|
||||
{
|
||||
var e = FtpListParser.Parse("07-18-26 10:00AM <DIR> ..", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Null);
|
||||
}
|
||||
|
||||
// Parse - fallback (NLST bare name)
|
||||
|
||||
[Test]
|
||||
public void Parse_BareName_FallbackFileEntry()
|
||||
{
|
||||
var e = FtpListParser.Parse("O3000", "/PRG/");
|
||||
|
||||
Assert.That(e, Is.Not.Null);
|
||||
Assert.That(e.Name, Is.EqualTo("O3000"));
|
||||
Assert.That(e.IsDirectory, Is.False);
|
||||
Assert.That(e.Size, Is.EqualTo(-1));
|
||||
}
|
||||
|
||||
// Parse - garbage/header multi-token line matches neither regex -> skip
|
||||
|
||||
[Test]
|
||||
public void Parse_GarbageMultiToken_ReturnsNull()
|
||||
{
|
||||
// "total 8": DOS regex needs leading MM-DD-YY date (no); Unix perm group is
|
||||
// [dl-][rwxsStT-]{9} and 't' is not in [dl-] (no). Trimmed line has internal
|
||||
// whitespace -> T4 fallback returns null (not treated as bare NLST name).
|
||||
Assert.That(FtpListParser.Parse("total 8", "/PRG/"), Is.Null);
|
||||
}
|
||||
|
||||
// Parse - whitespace/empty
|
||||
|
||||
[Test]
|
||||
public void Parse_Whitespace_ReturnsNull()
|
||||
{
|
||||
Assert.That(FtpListParser.Parse(" ", "/PRG/"), Is.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Moq;
|
||||
using NcProgramManager.Cnc;
|
||||
|
|
@ -213,5 +214,166 @@ namespace NcProgramManager.Tests.Unit
|
|||
|
||||
Assert.That(result.Success, Is.False);
|
||||
}
|
||||
|
||||
// ListDirectoryAsync
|
||||
|
||||
private void Connect()
|
||||
{
|
||||
_ftpMock.Setup(f => f.Ping()).Returns(true);
|
||||
_machine.ConnectAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ListDirectoryAsync_NullPath_UsesProgramDirectory()
|
||||
{
|
||||
Connect();
|
||||
var entries = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "O1000", FullPath = "/PRG/O1000", IsDirectory = false, Size = 10 },
|
||||
new FtpEntry { Name = "O2000", FullPath = "/PRG/O2000", IsDirectory = false, Size = 20 }
|
||||
};
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(entries);
|
||||
|
||||
var result = _machine.ListDirectoryAsync(null).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value.Count, Is.EqualTo(2));
|
||||
Assert.That(result.Value[0].Name, Is.EqualTo("O1000"));
|
||||
_ftpMock.Verify(f => f.ListDirectory("/PRG/"), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ListDirectoryAsync_ExplicitPath_PassesThrough()
|
||||
{
|
||||
Connect();
|
||||
var entries = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "O3000", FullPath = "/OTHER/O3000", IsDirectory = false, Size = 30 }
|
||||
};
|
||||
_ftpMock.Setup(f => f.ListDirectory("/OTHER/")).Returns(entries);
|
||||
|
||||
var result = _machine.ListDirectoryAsync("/OTHER/").GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value.Count, Is.EqualTo(1));
|
||||
_ftpMock.Verify(f => f.ListDirectory("/OTHER/"), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ListDirectoryAsync_FtpThrows_ResultFail()
|
||||
{
|
||||
Connect();
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Throws(new Exception("boom"));
|
||||
|
||||
var result = _machine.ListDirectoryAsync(null).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ListDirectoryAsync_NotConnected_Fail()
|
||||
{
|
||||
// No ConnectAsync -> EnsureConnected throws, wrapped as Fail.
|
||||
var result = _machine.ListDirectoryAsync(null).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.False);
|
||||
}
|
||||
|
||||
// BuildTreeAsync
|
||||
|
||||
[Test]
|
||||
public void BuildTreeAsync_FlatDir_RendersEntries()
|
||||
{
|
||||
Connect();
|
||||
var entries = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "O1000", FullPath = "/PRG/O1000", IsDirectory = false, Size = 10 },
|
||||
new FtpEntry { Name = "O2000", FullPath = "/PRG/O2000", IsDirectory = false, Size = 20 }
|
||||
};
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(entries);
|
||||
|
||||
var result = _machine.BuildTreeAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value, Does.Contain("O1000"));
|
||||
Assert.That(result.Value, Does.Contain("O2000"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildTreeAsync_NestedDir_RecursesWithinDepth()
|
||||
{
|
||||
Connect();
|
||||
var top = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "O1", FullPath = "/PRG/O1", IsDirectory = false, Size = 10 },
|
||||
// FullPath has NO trailing slash: real FtpListParser.Parse produces
|
||||
// "/PRG/SUB" (dir + name), and recursion keys ListDirectory on it verbatim.
|
||||
new FtpEntry { Name = "SUB", FullPath = "/PRG/SUB", IsDirectory = true, Size = -1 }
|
||||
};
|
||||
var sub = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "O2", FullPath = "/PRG/SUB/O2", IsDirectory = false, Size = 20 }
|
||||
};
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(top);
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/SUB")).Returns(sub);
|
||||
|
||||
var result = _machine.BuildTreeAsync(null, 2).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value, Does.Contain("O1"));
|
||||
Assert.That(result.Value, Does.Contain("SUB"));
|
||||
Assert.That(result.Value, Does.Contain("O2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildTreeAsync_MaxDepthCutoff()
|
||||
{
|
||||
Connect();
|
||||
var top = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "SUB", FullPath = "/PRG/SUB", IsDirectory = true, Size = -1 }
|
||||
};
|
||||
// maxDepth 1 -> only top level; deeper ListDirectory NOT set up.
|
||||
// Strict mock: if "/PRG/SUB" were listed the test would fail (proves no descent).
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(top);
|
||||
|
||||
var result = _machine.BuildTreeAsync(null, 1).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value, Does.Contain("SUB"));
|
||||
Assert.That(result.Value, Does.Not.Contain("O2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildTreeAsync_SubDirListThrows_InlineErrorNotFatal()
|
||||
{
|
||||
Connect();
|
||||
var top = new List<FtpEntry>
|
||||
{
|
||||
new FtpEntry { Name = "SUB", FullPath = "/PRG/SUB", IsDirectory = true, Size = -1 }
|
||||
};
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Returns(top);
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/SUB")).Throws(new Exception("denied"));
|
||||
|
||||
var result = _machine.BuildTreeAsync(null, 2).GetAwaiter().GetResult();
|
||||
|
||||
// Child (depth>0) list error is swallowed inline -> Success stays true.
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value, Does.Contain("errore lista"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildTreeAsync_RootListThrows_ResultFail()
|
||||
{
|
||||
// Root-level list failure now fetched DIRECTLY and MUST propagate ->
|
||||
// RunGuardedAsync catch -> Fail. Contrast with SubDirListThrows (child,
|
||||
// swallowed inline, Success stays true).
|
||||
Connect();
|
||||
_ftpMock.Setup(f => f.ListDirectory("/PRG/")).Throws(new Exception("530"));
|
||||
|
||||
var result = _machine.BuildTreeAsync(null).GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(result.Success, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@
|
|||
<Compile Include="Cnc\Mitsubishi\MitsubishiFtpClient.cs" />
|
||||
<Compile Include="Cnc\Mitsubishi\MitsubishiMachine.cs" />
|
||||
<Compile Include="Cnc\Mazak\MazakConnectionConfig.cs" />
|
||||
<Compile Include="Cnc\Mazak\FtpEntry.cs" />
|
||||
<Compile Include="Cnc\Mazak\FtpListParser.cs" />
|
||||
<Compile Include="Cnc\Mazak\IMazakFtpClient.cs" />
|
||||
<Compile Include="Cnc\Mazak\MazakFtpClient.cs" />
|
||||
<Compile Include="Cnc\Mazak\MazakMachine.cs" />
|
||||
|
|
|
|||
58
Program.cs
58
Program.cs
|
|
@ -51,7 +51,9 @@ namespace NcProgramManager
|
|||
#endif
|
||||
|
||||
bool needsIp = inputArgs.manufacturer != CncManufacturer.Fanuc || inputArgs.nodoHssb == -1;
|
||||
if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || inputArgs.pathLocaleProgramma == "")
|
||||
// LISTA (probe FTP) non richiede un programma locale.
|
||||
bool needsLocalProgram = !string.Equals(inputArgs.azione, "LISTA", StringComparison.OrdinalIgnoreCase);
|
||||
if (inputArgs.azione == "" || (needsIp && string.IsNullOrEmpty(inputArgs.ip)) || (needsLocalProgram && inputArgs.pathLocaleProgramma == ""))
|
||||
{
|
||||
VediErrore(-102, inputArgs.debugMode);
|
||||
return -102;
|
||||
|
|
@ -74,6 +76,7 @@ namespace NcProgramManager
|
|||
{
|
||||
case "SCARICA": return Scarica(machine, inputArgs);
|
||||
case "INVIA": return Invia(machine, inputArgs);
|
||||
case "LISTA": return Lista(machine, inputArgs);
|
||||
default:
|
||||
VediErrore(-103, inputArgs.debugMode);
|
||||
return -103;
|
||||
|
|
@ -270,6 +273,59 @@ namespace NcProgramManager
|
|||
}
|
||||
}
|
||||
|
||||
static int Lista(ICncMachine macchina, InputArgs inputArgs)
|
||||
{
|
||||
_log.Info("LISTA FTP (probe)-->");
|
||||
var mazak = macchina as NcProgramManager.Cnc.Mazak.MazakMachine;
|
||||
if (mazak == null)
|
||||
{
|
||||
_log.Info("Azione LISTA supportata solo per manufacturer=mazak");
|
||||
VediErrore(-103, inputArgs.debugMode);
|
||||
return -103;
|
||||
}
|
||||
|
||||
bool connected = false;
|
||||
try
|
||||
{
|
||||
var connResult = macchina.ConnectAsync().GetAwaiter().GetResult();
|
||||
if (!connResult.Success)
|
||||
{
|
||||
_log.Info("Macchina non connessa");
|
||||
PrintErrors(connResult.Errors);
|
||||
VediErrore(-201, inputArgs.debugMode);
|
||||
return -201;
|
||||
}
|
||||
connected = true;
|
||||
|
||||
_log.Info("Directory FTP: {0}", string.IsNullOrEmpty(inputArgs.ftpDir) ? "/" : inputArgs.ftpDir);
|
||||
var treeResult = mazak.BuildTreeAsync(inputArgs.ftpDir, 2).GetAwaiter().GetResult();
|
||||
if (!treeResult.Success)
|
||||
{
|
||||
PrintErrors(treeResult.Errors);
|
||||
VediErrore(-204, inputArgs.debugMode);
|
||||
return -204;
|
||||
}
|
||||
|
||||
foreach (var riga in treeResult.Value.Split('\n'))
|
||||
_log.Info("{0}", riga.TrimEnd('\r'));
|
||||
|
||||
VediErrore(0, inputArgs.debugMode);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(ex, "Lista failed");
|
||||
_log.Info("{0}", ex.Message);
|
||||
VediErrore(-202, inputArgs.debugMode);
|
||||
return -202;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connected)
|
||||
macchina.DisconnectAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintErrors(IReadOnlyList<CncError> errors)
|
||||
{
|
||||
if (errors == null) return;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue