using AvParser.Core.Parsing; using AvParser.Core.Parsing.Samples; namespace AvParser.Core.Tests; public class DelimitedTextParserTests { private readonly DelimitedTextParser _parser = new(); [Fact] public async Task Parses_header_and_rows() { var (records, errors) = await _parser.CollectAsync("id,name\n1,Ada\n2,Grace"); errors.ShouldBeEmpty(); records.Count.ShouldBe(2); records[0].Field("id").ShouldBe("1"); records[0].Field("name").ShouldBe("Ada"); records[1].LineNumber.ShouldBe(3); } [Theory] [InlineData("a;b\n1;2")] [InlineData("a\tb\n1\t2")] [InlineData("a|b\n1|2")] public async Task Detects_the_delimiter_from_the_header(string input) { var (records, errors) = await _parser.CollectAsync(input); errors.ShouldBeEmpty(); records.ShouldHaveSingleItem().Fields.Count.ShouldBe(2); } [Fact] public async Task Reports_a_field_count_mismatch_without_aborting() { var (records, errors) = await _parser.CollectAsync("id,name\n1\n2,Grace"); // The bad line becomes an error; the good line after it still parses. errors.ShouldHaveSingleItem().LineNumber.ShouldBe(2); records.ShouldHaveSingleItem().Field("name").ShouldBe("Grace"); } [Fact] public async Task Skips_blank_lines_and_comments() { var (records, errors) = await _parser.CollectAsync("# a comment\nid,name\n\n1,Ada\n"); errors.ShouldBeEmpty(); records.ShouldHaveSingleItem().Field("name").ShouldBe("Ada"); } [Fact] public async Task Trims_surrounding_whitespace() { var (records, _) = await _parser.CollectAsync("id , name\n 1 , Ada "); records.ShouldHaveSingleItem().Field("name").ShouldBe("Ada"); } [Fact] public async Task Reports_an_error_when_there_is_no_header() { var (records, errors) = await _parser.CollectAsync("\n\n"); records.ShouldBeEmpty(); errors.ShouldHaveSingleItem().Message.ShouldContain("header"); } [Fact] public async Task Reports_progress_reaching_completion() { var reports = new List(); // Not Progress: it posts to the captured synchronization context, so the reports would // arrive after the assertions. A direct IProgress keeps the test deterministic. await foreach ( var _ in _parser.ParseAsync( ParserTestExtensions.DelimitedDocument(900), new SynchronousProgress(reports.Add), TestContext.Current.CancellationToken ) ) { } reports.ShouldNotBeEmpty(); reports[^1].Fraction.ShouldBe(1d); } [Fact] public async Task Honours_cancellation() { using var cancellation = new CancellationTokenSource(); var act = async () => { await foreach ( var _ in _parser.ParseAsync(ParserTestExtensions.DelimitedDocument(20_000), null, cancellation.Token) ) { await cancellation.CancelAsync(); } }; await act.ShouldThrowAsync(); } [Theory] [InlineData("", false)] [InlineData(" ", false)] [InlineData("no delimiters here", false)] [InlineData("a,b", true)] public void CanParse_checks_for_a_delimiter(string input, bool expected) => _parser.CanParse(input).ShouldBe(expected); }