using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Csv; namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.Csv; /// /// The required round-trip property: for arbitrary field content, Parse(Write(rows)) == rows. /// 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. /// public sealed class CsvRoundTripTests { public static IEnumerable 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 parsed, string[][] expected) { parsed.Count.ShouldBe(expected.Length); for (var i = 0; i < expected.Length; i++) { parsed[i].ShouldBe(expected[i], $"row {i} mismatch"); } } }