Remove the demo text-parsing domain
The scaffolding domain existed to prove the shell end to end before there was
anything real to put in it. There is now, so it goes - as CLAUDE.md promised it
would.
Gone: the two sample parsers, ITextParser, ParsedRecord, the parser catalog,
ParseViewModel and ParseView, their tests, and the settings key that remembered
which parser was last used. ParseError.LineNumber becomes Index, since for a
listing "line 42" was simply untrue, and the error keys move from Parse.Error.*
to Collect.Error.* now that parsing is not a concept here.
Kept: IParser<,>, ParseOutcome, ParseProgress and ParseError. The streaming
contract was always the general part - it was only ever the text-shaped closure
of it that was scaffolding.
Rendering the dashboard caught two keys that were referenced but never added
during the rename: the XAML was repointed and the resources were not. The parity
test could not see it, because it compares the two files against each other and
a key absent from both is consistent. That gap now has its own test, which reads
every {l:Loc} in the XAML and checks it resolves - a screenshot is too late and
too manual a way to find a missing string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fe62bcf53f
commit
f8744c930a
@@ -1,114 +0,0 @@
|
||||
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<ParseProgress>();
|
||||
|
||||
// Not Progress<T>: it posts to the captured synchronization context, so the reports would
|
||||
// arrive after the assertions. A direct IProgress<T> keeps the test deterministic.
|
||||
await foreach (
|
||||
var _ in _parser.ParseAsync(
|
||||
ParserTestExtensions.DelimitedDocument(900),
|
||||
new SynchronousProgress<ParseProgress>(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<OperationCanceledException>();
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using AvParser.Core.Parsing.Samples;
|
||||
|
||||
namespace AvParser.Core.Tests;
|
||||
|
||||
public class KeyValueTextParserTests
|
||||
{
|
||||
private readonly KeyValueTextParser _parser = new();
|
||||
|
||||
[Theory]
|
||||
[InlineData("host = localhost")]
|
||||
[InlineData("host: localhost")]
|
||||
public async Task Accepts_both_separators(string input)
|
||||
{
|
||||
var (records, errors) = await _parser.CollectAsync(input);
|
||||
|
||||
errors.ShouldBeEmpty();
|
||||
var record = records.ShouldHaveSingleItem();
|
||||
record.Field("Key").ShouldBe("host");
|
||||
record.Field("Value").ShouldBe("localhost");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Splits_on_the_first_separator_only()
|
||||
{
|
||||
var (records, _) = await _parser.CollectAsync("url = https://example.com:8080/path");
|
||||
|
||||
records.ShouldHaveSingleItem().Field("Value").ShouldBe("https://example.com:8080/path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reports_lines_without_a_separator()
|
||||
{
|
||||
var (records, errors) = await _parser.CollectAsync("host = localhost\ngarbage\nport = 80");
|
||||
|
||||
records.Count.ShouldBe(2);
|
||||
errors.ShouldHaveSingleItem().LineNumber.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reports_an_empty_key()
|
||||
{
|
||||
var (_, errors) = await _parser.CollectAsync("= orphan");
|
||||
|
||||
errors.ShouldHaveSingleItem().Message.ShouldContain("separator");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Skips_comments_and_blank_lines()
|
||||
{
|
||||
var (records, errors) = await _parser.CollectAsync("# comment\n\nhost = localhost\n");
|
||||
|
||||
errors.ShouldBeEmpty();
|
||||
records.ShouldHaveSingleItem().Field("Key").ShouldBe("host");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Allows_an_empty_value()
|
||||
{
|
||||
var (records, errors) = await _parser.CollectAsync("host =");
|
||||
|
||||
errors.ShouldBeEmpty();
|
||||
records.ShouldHaveSingleItem().Field("Value").ShouldBe(string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using AvParser.Core.Parsing;
|
||||
using AvParser.Core.Parsing.Samples;
|
||||
|
||||
namespace AvParser.Core.Tests;
|
||||
|
||||
public class ParserCatalogTests
|
||||
{
|
||||
private static IParserCatalog Catalog() => new ParserCatalog([new KeyValueTextParser(), new DelimitedTextParser()]);
|
||||
|
||||
[Fact]
|
||||
public void Orders_parsers_by_display_name_regardless_of_registration_order() =>
|
||||
Catalog().Parsers.Select(p => p.Id).ShouldBe(["delimited", "key-value"]);
|
||||
|
||||
[Fact]
|
||||
public void Finds_a_parser_by_id_ignoring_case() => Catalog().Find("KEY-VALUE")!.Id.ShouldBe("key-value");
|
||||
|
||||
[Fact]
|
||||
public void Returns_null_for_an_unknown_id() => Catalog().Find("nope").ShouldBeNull();
|
||||
|
||||
[Fact]
|
||||
public void Falls_back_to_the_default_for_an_unknown_id()
|
||||
{
|
||||
var catalog = Catalog();
|
||||
|
||||
catalog.FindOrDefault("nope").ShouldBeSameAs(catalog.DefaultParser);
|
||||
catalog.FindOrDefault(null).ShouldBeSameAs(catalog.DefaultParser);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rejects_an_empty_registration() => Should.Throw<ArgumentException>(() => new ParserCatalog([]));
|
||||
|
||||
[Fact]
|
||||
public void Rejects_duplicate_ids() =>
|
||||
Should
|
||||
.Throw<ArgumentException>(() => new ParserCatalog([new DelimitedTextParser(), new DelimitedTextParser()]))
|
||||
.Message.ShouldContain("Duplicate");
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using AvParser.Core.Parsing;
|
||||
|
||||
namespace AvParser.Core.Tests;
|
||||
|
||||
/// <summary>Collection helpers so the tests read as assertions rather than as loops.</summary>
|
||||
internal static class ParserTestExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Drains a parse into memory, using the ambient test cancellation token.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately takes no <see cref="CancellationToken"/>: every call site would otherwise have
|
||||
/// to pass <c>TestContext.Current.CancellationToken</c> to satisfy xUnit1051. Cancellation
|
||||
/// behaviour is covered by driving <see cref="IParser{TInput,TOutput}.ParseAsync"/> directly.
|
||||
/// </remarks>
|
||||
internal static async Task<(List<ParsedRecord> Records, List<ParseError> Errors)> CollectAsync(
|
||||
this ITextParser parser,
|
||||
string input,
|
||||
IProgress<ParseProgress>? progress = null
|
||||
)
|
||||
{
|
||||
var records = new List<ParsedRecord>();
|
||||
var errors = new List<ParseError>();
|
||||
|
||||
await foreach (var outcome in parser.ParseAsync(input, progress, TestContext.Current.CancellationToken))
|
||||
{
|
||||
if (outcome.IsSuccess)
|
||||
{
|
||||
records.Add(outcome.Value!);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(outcome.Error);
|
||||
}
|
||||
}
|
||||
|
||||
return (records, errors);
|
||||
}
|
||||
|
||||
/// <summary>Reads a field by name, failing the test if it is absent.</summary>
|
||||
internal static string Field(this ParsedRecord record, string name) =>
|
||||
record[name] ?? throw new InvalidOperationException($"Field '{name}' is missing.");
|
||||
|
||||
/// <summary>Builds a delimited document with a header plus <paramref name="rows"/> data rows.</summary>
|
||||
internal static string DelimitedDocument(int rows) =>
|
||||
string.Join('\n', Enumerable.Range(0, rows + 1).Select(i => i == 0 ? "id,name" : $"{i},row{i}"));
|
||||
}
|
||||
Reference in New Issue
Block a user