Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/Csv/CsvRoundTripTests.cs
T
Joseph Doherty c3277b52c9 review(track0): async ContextMenu OnClick + keyboard-⋯ anchoring + focus-return; discovery-driven DriverTypeNames guard; CSV round-trip comment
Wave-0 reviewer findings (no Critical/High):
- T0-1 M: ContextMenuItem.OnClick Action→Func<Task> (kills async-void at menu call sites Wave A/B/C need); keyboard-activated ⋯ now anchors below the button instead of viewport (0,0); focus returns to trigger on close; backdrop closes on native right-click too.
- T0-3 M: guard test now discovers *DriverFactoryExtensions.Register by convention from the deployed driver assemblies instead of a hand-copied list, so a future driver lacking a constant is actually caught.
- T0-2 L: corrected the round-trip 'sole non-round-trippable shape' comment (any table whose final row is a lone empty field).

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 02:17:37 -04:00

96 lines
3.9 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Csv;
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Csv;
/// <summary>
/// The required round-trip property: for arbitrary field content, <c>Parse(Write(rows)) == rows</c>.
/// This is what makes the writer a true inverse of the parser and pins the quote-on-demand rules to
/// the parser's grammar. The corpus deliberately includes every tricky character.
/// </summary>
public sealed class CsvRoundTripTests
{
public static IEnumerable<object[]> Tables()
{
// Each case is a jagged table of fields.
yield return new object[] { new[] { new[] { "a", "b", "c" } } };
yield return new object[] { new[] { new[] { "a", "b" }, new[] { "c", "d" } } };
// NOTE: a table whose FINAL row is a single empty field ([[""]], or e.g. [["a"],[""]]) is deliberately
// excluded — that trailing lone-empty-field row serialises to nothing after the preceding row's
// terminator, so the parser's no-phantom-trailing-row rule drops it on the way back. This is the only
// non-round-trippable shape; a single-empty-field row anywhere but last (see the interior [""] case
// below), ",,", and blank interior lines all round-trip exactly.
yield return new object[] { new[] { new[] { "", "", "" } } };
yield return new object[] { new[] { new[] { "a" }, new[] { "" }, new[] { "b" } } };
yield return new object[] { new[] { new[] { "a,b", "c" } } }; // embedded delimiter
yield return new object[] { new[] { new[] { "he said \"hi\"" } } }; // embedded quote
yield return new object[] { new[] { new[] { "line1\nline2" } } }; // embedded LF
yield return new object[] { new[] { new[] { "line1\r\nline2" } } }; // embedded CRLF
yield return new object[] { new[] { new[] { "line1\rline2" } } }; // embedded CR
yield return new object[] { new[] { new[] { " leading", "trailing " } } }; // spaces
yield return new object[] { new[] { new[] { " ", "\t" } } }; // whitespace-only
yield return new object[] { new[] { new[] { "\"", "\"\"", ",", "\n" } } }; // every special alone
yield return new object[] { new[] { new[] { "mix,\"quote\"\nand\r\nnewlines" } } };
yield return new object[]
{
new[]
{
new[] { "id", "name", "note" },
new[] { "1", "alice", "says \"hi, there\"" },
new[] { "2", "bob", "multi\nline\nnote" },
new[] { "3", "", "" },
},
};
}
[Theory]
[MemberData(nameof(Tables))]
public void RoundTrips_with_default_crlf(string[][] rows)
{
var csv = CsvWriter.WriteToString(rows);
var parsed = CsvParser.Parse(csv);
ShouldMatch(parsed, rows);
}
[Theory]
[MemberData(nameof(Tables))]
public void RoundTrips_with_lf_newline(string[][] rows)
{
var csv = CsvWriter.WriteToString(rows, newline: "\n");
var parsed = CsvParser.Parse(csv);
ShouldMatch(parsed, rows);
}
[Theory]
[MemberData(nameof(Tables))]
public void RoundTrips_with_quote_all_fields(string[][] rows)
{
var csv = CsvWriter.WriteToString(rows, quoteAllFields: true);
var parsed = CsvParser.Parse(csv);
ShouldMatch(parsed, rows);
}
[Theory]
[MemberData(nameof(Tables))]
public void RoundTrips_with_semicolon_delimiter(string[][] rows)
{
var csv = CsvWriter.WriteToString(rows, delimiter: ';');
var parsed = CsvParser.Parse(csv, ';');
ShouldMatch(parsed, rows);
}
private static void ShouldMatch(IReadOnlyList<string[]> parsed, string[][] expected)
{
parsed.Count.ShouldBe(expected.Length);
for (var i = 0; i < expected.Length; i++)
{
parsed[i].ShouldBe(expected[i], $"row {i} mismatch");
}
}
}