191 lines
6.3 KiB
C#
191 lines
6.3 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using CliFx.Exceptions;
|
|
using CliFx.Infrastructure;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Commands;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests;
|
|
|
|
/// <summary>
|
|
/// Coverage for the <c>import-rslogix</c> CLI command. The command is intentionally
|
|
/// thin (open file, hand to <c>RsLogixSymbolImport</c>, serialise) — these tests focus
|
|
/// on the I/O + flag-handling shape rather than re-running the parser.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ImportRslogixCommandTests
|
|
{
|
|
private const string CanonicalCsv = """
|
|
Symbol,Address,Description,DataType,Scope
|
|
MotorSpeed,N7:0,Motor speed,INT,Global
|
|
TankLevel,F8:0,Tank level,REAL,Global
|
|
RunFlag,B3:0/0,Run flag,BOOL,Global
|
|
""";
|
|
|
|
[Fact]
|
|
public async Task Execute_with_valid_csv_emits_json_fragment_with_three_tags()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"rslogix-cli-{Guid.NewGuid():N}.csv");
|
|
File.WriteAllText(path, CanonicalCsv);
|
|
try
|
|
{
|
|
using var console = new FakeInMemoryConsole();
|
|
var cmd = new ImportRslogixCommand
|
|
{
|
|
File = path,
|
|
Device = "ab://10.0.0.5/1,0",
|
|
Emit = "appsettings-fragment",
|
|
};
|
|
|
|
await cmd.ExecuteAsync(console);
|
|
|
|
var output = console.ReadOutputString();
|
|
output.ShouldContain("\"Tags\"");
|
|
|
|
// Parse the emitted JSON and assert the structural properties — flake-resistant
|
|
// vs. comparing whitespace-sensitive text.
|
|
using var doc = JsonDocument.Parse(output);
|
|
var tags = doc.RootElement.GetProperty("Tags");
|
|
tags.GetArrayLength().ShouldBe(3);
|
|
tags[0].GetProperty("Name").GetString().ShouldBe("MotorSpeed");
|
|
tags[0].GetProperty("DataType").GetString().ShouldBe("Int");
|
|
tags[2].GetProperty("DataType").GetString().ShouldBe("Bit");
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(path); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_with_summary_emit_prints_counters()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"rslogix-cli-{Guid.NewGuid():N}.csv");
|
|
File.WriteAllText(path, CanonicalCsv);
|
|
try
|
|
{
|
|
using var console = new FakeInMemoryConsole();
|
|
var cmd = new ImportRslogixCommand
|
|
{
|
|
File = path,
|
|
Device = "ab://10.0.0.5/1,0",
|
|
Emit = "summary",
|
|
};
|
|
|
|
await cmd.ExecuteAsync(console);
|
|
|
|
var output = console.ReadOutputString();
|
|
output.ShouldContain("Imported 3");
|
|
output.ShouldContain("skipped 0");
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(path); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_with_output_path_writes_file_and_prints_summary()
|
|
{
|
|
var inputPath = Path.Combine(Path.GetTempPath(), $"rslogix-cli-{Guid.NewGuid():N}.csv");
|
|
var outputPath = Path.Combine(Path.GetTempPath(), $"rslogix-cli-{Guid.NewGuid():N}.json");
|
|
File.WriteAllText(inputPath, CanonicalCsv);
|
|
try
|
|
{
|
|
using var console = new FakeInMemoryConsole();
|
|
var cmd = new ImportRslogixCommand
|
|
{
|
|
File = inputPath,
|
|
Device = "ab://10.0.0.5/1,0",
|
|
Emit = "appsettings-fragment",
|
|
Output = outputPath,
|
|
};
|
|
|
|
await cmd.ExecuteAsync(console);
|
|
|
|
File.Exists(outputPath).ShouldBeTrue();
|
|
var fileBody = File.ReadAllText(outputPath);
|
|
using var doc = JsonDocument.Parse(fileBody);
|
|
doc.RootElement.GetProperty("Tags").GetArrayLength().ShouldBe(3);
|
|
|
|
// Stdout still gets the human-readable summary line.
|
|
console.ReadOutputString().ShouldContain("Wrote 3");
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(inputPath); } catch { /* best-effort */ }
|
|
try { File.Delete(outputPath); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_with_missing_file_throws_command_exception()
|
|
{
|
|
var missing = Path.Combine(Path.GetTempPath(), $"does-not-exist-{Guid.NewGuid():N}.csv");
|
|
using var console = new FakeInMemoryConsole();
|
|
var cmd = new ImportRslogixCommand
|
|
{
|
|
File = missing,
|
|
Device = "ab://10.0.0.5/1,0",
|
|
};
|
|
|
|
var ex = await Should.ThrowAsync<CommandException>(async () => await cmd.ExecuteAsync(console));
|
|
ex.ExitCode.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_with_unknown_emit_throws_command_exception()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"rslogix-cli-{Guid.NewGuid():N}.csv");
|
|
File.WriteAllText(path, CanonicalCsv);
|
|
try
|
|
{
|
|
using var console = new FakeInMemoryConsole();
|
|
var cmd = new ImportRslogixCommand
|
|
{
|
|
File = path,
|
|
Device = "ab://10.0.0.5/1,0",
|
|
Emit = "yaml",
|
|
};
|
|
|
|
var ex = await Should.ThrowAsync<CommandException>(async () => await cmd.ExecuteAsync(console));
|
|
ex.ExitCode.ShouldBe(2);
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(path); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Execute_scope_filter_only_imports_matching_rows()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"rslogix-cli-{Guid.NewGuid():N}.csv");
|
|
File.WriteAllText(path, """
|
|
Symbol,Address,Description,DataType,Scope
|
|
G1,N7:0,desc,INT,Global
|
|
L1,N7:1,desc,INT,Local:1
|
|
L2,N7:2,desc,INT,Local:2
|
|
""");
|
|
try
|
|
{
|
|
using var console = new FakeInMemoryConsole();
|
|
var cmd = new ImportRslogixCommand
|
|
{
|
|
File = path,
|
|
Device = "ab://10.0.0.5/1,0",
|
|
Emit = "summary",
|
|
Scope = "Local:1",
|
|
};
|
|
|
|
await cmd.ExecuteAsync(console);
|
|
console.ReadOutputString().ShouldContain("Imported 1");
|
|
}
|
|
finally
|
|
{
|
|
try { File.Delete(path); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
}
|