237 lines
8.3 KiB
C#
237 lines
8.3 KiB
C#
using System.CommandLine;
|
|
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands;
|
|
|
|
/// <summary>
|
|
/// CLI-019 regression tests for <see cref="BundleCommands.StreamBase64ToFile"/>.
|
|
/// The pre-fix code did <c>Convert.FromBase64String(...) → File.WriteAllBytes(...)</c>,
|
|
/// doubling the bundle's bytes onto the LOH and writing synchronously. The new
|
|
/// streaming helper decodes the base64 string in fixed-size chunks straight into
|
|
/// a <see cref="FileStream"/>, so peak working set is bounded by the chunk size
|
|
/// regardless of how large the bundle is.
|
|
/// </summary>
|
|
public class BundleCommandsStreamingTests : IDisposable
|
|
{
|
|
private readonly string _tempPath;
|
|
|
|
public BundleCommandsStreamingTests()
|
|
{
|
|
_tempPath = Path.Combine(Path.GetTempPath(), $"bundle-stream-test-{Guid.NewGuid():N}.bin");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (File.Exists(_tempPath))
|
|
{
|
|
File.Delete(_tempPath);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamBase64ToFile_SmallPayload_RoundTrips()
|
|
{
|
|
var bytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
|
var base64 = Convert.ToBase64String(bytes);
|
|
|
|
var written = BundleCommands.StreamBase64ToFile(base64, _tempPath);
|
|
|
|
Assert.Equal(bytes.Length, written);
|
|
var roundTripped = File.ReadAllBytes(_tempPath);
|
|
Assert.Equal(bytes, roundTripped);
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamBase64ToFile_PayloadCrossesChunkBoundary_RoundTrips()
|
|
{
|
|
// Build a payload several chunks wide so the slicing loop runs more than
|
|
// once, with enough trailing bytes that the final slice is short and
|
|
// exercises the padding/short-final-chunk path.
|
|
var size = (BundleCommands.Base64StreamChunkChars / 4 * 3) * 3 + 17;
|
|
var bytes = new byte[size];
|
|
for (var i = 0; i < size; i++) bytes[i] = (byte)(i & 0xFF);
|
|
|
|
var base64 = Convert.ToBase64String(bytes);
|
|
|
|
var written = BundleCommands.StreamBase64ToFile(base64, _tempPath);
|
|
|
|
Assert.Equal(size, written);
|
|
var roundTripped = File.ReadAllBytes(_tempPath);
|
|
Assert.Equal(bytes, roundTripped);
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamBase64ToFile_EmptyString_WritesEmptyFile()
|
|
{
|
|
var written = BundleCommands.StreamBase64ToFile(string.Empty, _tempPath);
|
|
|
|
Assert.Equal(0, written);
|
|
Assert.True(File.Exists(_tempPath));
|
|
Assert.Empty(File.ReadAllBytes(_tempPath));
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamBase64ToFile_InvalidBase64_ThrowsFormatException()
|
|
{
|
|
// '*' is not a valid base64 character, so TryFromBase64Chars returns
|
|
// false and the helper throws — the pre-fix code threw FormatException
|
|
// from Convert.FromBase64String, so the contract is preserved.
|
|
var invalid = "this is not valid base64 !!!*";
|
|
|
|
Assert.Throws<FormatException>(() => BundleCommands.StreamBase64ToFile(invalid, _tempPath));
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamBase64ToFile_NullBase64_Throws()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => BundleCommands.StreamBase64ToFile(null!, _tempPath));
|
|
}
|
|
|
|
[Fact]
|
|
public void StreamBase64ToFile_EmptyOutputPath_Throws()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => BundleCommands.StreamBase64ToFile("AAAA", string.Empty));
|
|
}
|
|
|
|
// ---- M8 (B4): --sites / --instances comma-split + flag parsing ----------
|
|
|
|
[Fact]
|
|
public void ParseNameList_CommaSeparated_SplitsAndTrims()
|
|
{
|
|
var result = BundleCommands.ParseNameList(" North-01 , East-02 ,West-03 ");
|
|
Assert.NotNull(result);
|
|
Assert.Equal(new[] { "North-01", "East-02", "West-03" }, result);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void ParseNameList_NullOrBlank_ReturnsNull(string? token)
|
|
{
|
|
Assert.Null(BundleCommands.ParseNameList(token));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseNameList_EmptyEntries_AreDropped()
|
|
{
|
|
var result = BundleCommands.ParseNameList("a,,b, ,c");
|
|
Assert.Equal(new[] { "a", "b", "c" }, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void BundleExport_SitesAndInstancesFlags_ParseWithoutError()
|
|
{
|
|
var url = new Option<string>("--url") { Recursive = true };
|
|
var format = new Option<string>("--format") { Recursive = true };
|
|
var username = new Option<string>("--username") { Recursive = true };
|
|
var password = new Option<string>("--password") { Recursive = true };
|
|
var bundle = BundleCommands.Build(url, format, username, password);
|
|
|
|
var parse = bundle.Parse(new[]
|
|
{
|
|
"export", "--output", "/tmp/out.scadabundle",
|
|
"--sites", "NORTH-01,East Plant",
|
|
"--instances", "NORTH-01.Pump1,NORTH-01.Pump2",
|
|
});
|
|
|
|
Assert.Empty(parse.Errors);
|
|
}
|
|
|
|
// ---- M8 (D3): --map-site / --map-connection spec parsing ----------------
|
|
|
|
[Fact]
|
|
public void ParseSiteMappings_Null_ReturnsEmpty()
|
|
{
|
|
Assert.Empty(BundleCommands.ParseSiteMappings(null));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseSiteMappings_SrcEqualsDst_MapsToExisting()
|
|
{
|
|
var specs = BundleCommands.ParseSiteMappings(new[] { "NORTH-01=PLANT-A" });
|
|
var spec = Assert.Single(specs);
|
|
Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
|
|
Assert.Equal("PLANT-A", spec.TargetSiteIdentifier);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("NORTH-01")] // no '=' part -> create-new
|
|
[InlineData("NORTH-01=")] // empty rhs -> create-new
|
|
[InlineData("NORTH-01= ")] // whitespace rhs -> create-new
|
|
public void ParseSiteMappings_NoTarget_MeansCreateNew(string token)
|
|
{
|
|
var specs = BundleCommands.ParseSiteMappings(new[] { token });
|
|
var spec = Assert.Single(specs);
|
|
Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
|
|
Assert.Null(spec.TargetSiteIdentifier);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseSiteMappings_Repeated_AccumulatesAll()
|
|
{
|
|
var specs = BundleCommands.ParseSiteMappings(new[] { "A=X", "B" });
|
|
Assert.Equal(2, specs.Count);
|
|
Assert.Equal("X", specs[0].TargetSiteIdentifier);
|
|
Assert.Null(specs[1].TargetSiteIdentifier);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseSiteMappings_EmptySource_Throws()
|
|
{
|
|
Assert.Throws<FormatException>(() => BundleCommands.ParseSiteMappings(new[] { "=PLANT-A" }));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseConnectionMappings_SrcEqualsDst_MapsToExisting()
|
|
{
|
|
var specs = BundleCommands.ParseConnectionMappings(new[] { "NORTH-01/OpcA=OpcLive" });
|
|
var spec = Assert.Single(specs);
|
|
Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
|
|
Assert.Equal("OpcA", spec.SourceConnectionName);
|
|
Assert.Equal("OpcLive", spec.TargetConnectionName);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("NORTH-01/OpcA")] // no '=' part -> create-new
|
|
[InlineData("NORTH-01/OpcA=")] // empty rhs -> create-new
|
|
public void ParseConnectionMappings_NoTarget_MeansCreateNew(string token)
|
|
{
|
|
var specs = BundleCommands.ParseConnectionMappings(new[] { token });
|
|
var spec = Assert.Single(specs);
|
|
Assert.Equal("NORTH-01", spec.SourceSiteIdentifier);
|
|
Assert.Equal("OpcA", spec.SourceConnectionName);
|
|
Assert.Null(spec.TargetConnectionName);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("OpcA")] // missing site/name shape
|
|
[InlineData("NORTH-01/")] // empty connection name
|
|
[InlineData("/OpcA")] // empty site
|
|
public void ParseConnectionMappings_MalformedSource_Throws(string token)
|
|
{
|
|
Assert.Throws<FormatException>(() => BundleCommands.ParseConnectionMappings(new[] { token }));
|
|
}
|
|
|
|
[Fact]
|
|
public void BundleImport_MapAndCreateMissingFlags_ParseWithoutError()
|
|
{
|
|
var url = new Option<string>("--url") { Recursive = true };
|
|
var format = new Option<string>("--format") { Recursive = true };
|
|
var username = new Option<string>("--username") { Recursive = true };
|
|
var password = new Option<string>("--password") { Recursive = true };
|
|
var bundle = BundleCommands.Build(url, format, username, password);
|
|
|
|
var parse = bundle.Parse(new[]
|
|
{
|
|
"import", "--input", "/tmp/in.scadabundle",
|
|
"--map-site", "NORTH-01=PLANT-A",
|
|
"--map-connection", "NORTH-01/OpcA=OpcLive",
|
|
"--create-missing-sites",
|
|
"--create-missing-connections",
|
|
});
|
|
|
|
Assert.Empty(parse.Errors);
|
|
}
|
|
}
|