Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 89 additions & 36 deletions LibLouis.NET.Test/BrailleSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ private static string Describe(string value) =>
: string.Concat(value.Select(c => c is >= ' ' and <= '~' ? c.ToString() : $"\\u{(int)c:X4}"));
}

/// <summary>
/// A parsed spec: the cases it yields, and how many spec entries were dropped for each construct
/// that was recognised but not driven, so what is being skipped stays visible rather than becoming
/// invisible coverage loss.
/// </summary>
public sealed record BrailleSpec(
IReadOnlyList<BrailleSpecCase> Cases,
IReadOnlyDictionary<string, int> SkippedConstructs);

public enum TestDirection
{
Forward,
Expand All @@ -51,6 +60,9 @@ public enum TestMode
Forward,
Backward,
BothDirections,

/// <summary>A liblouis feature this harness recognises but does not drive yet.</summary>
Unsupported,
}

/// <summary>
Expand All @@ -66,16 +78,19 @@ public enum TestMode
/// Consecutive <c>table</c> keys accumulate rather than replace: the following <c>tests</c> block
/// runs once per accumulated table. <c>flags</c> persists until the next <c>flags</c>.
///
/// Only the constructs the Danish specs actually use are supported. Anything else throws rather
/// than being skipped, so a spec using a feature this reader does not model fails loudly instead
/// of silently testing less than it appears to.
/// Constructs fall into three groups. Those the harness drives are turned into cases. Those
/// liblouis supports but the harness does not drive yet are counted in
/// <see cref="BrailleSpec.SkippedConstructs"/> and their cases dropped. Anything else throws, so a
/// spec using something nobody has looked at fails loudly rather than quietly testing less than it
/// appears to.
/// </remarks>
public static class BrailleSpecReader
{
public static IReadOnlyList<BrailleSpecCase> Read(string path)
public static BrailleSpec Read(string path)
{
string specFile = Path.GetFileName(path);
var cases = new List<BrailleSpecCase>();
var skipped = new SortedDictionary<string, int>(StringComparer.Ordinal);

using var reader = new StreamReader(path);
var parser = new Parser(reader);
Expand All @@ -87,6 +102,7 @@ public static IReadOnlyList<BrailleSpecCase> Read(string path)
string displayTable = string.Empty;
var tables = new List<(string Query, string? AssertMatch)>();
TestMode mode = TestMode.Forward;
string? unsupportedMode = null;

// Consecutive table keys accumulate, but the first one after a tests block starts a fresh
// set rather than adding to the one just used.
Expand All @@ -113,23 +129,22 @@ public static IReadOnlyList<BrailleSpecCase> Read(string path)
break;

case "flags":
mode = ReadFlags(parser);
(mode, unsupportedMode) = ReadFlags(parser);
break;

case "tests":
ReadTests(parser, specFile, tables, displayTable, mode, cases);
ReadTests(parser, specFile, tables, displayTable, mode, unsupportedMode, cases, skipped);
tablesUsed = true;
break;

default:
throw new NotSupportedException(
$"{specFile}: unsupported top level key '{key}'. This reader models only the " +
"constructs the Danish specs use; see lou_checkyaml.c for the full format.");
$"{specFile}: unsupported top level key '{key}'. See lou_checkyaml.c for the " +
"full format.");
}

}

return cases;
return new BrailleSpec(cases, skipped);
}

/// <summary>
Expand Down Expand Up @@ -169,11 +184,12 @@ private static (string Query, string? AssertMatch) ReadTableValue(IParser parser
return (string.Join(' ', terms), assertMatch);
}

private static TestMode ReadFlags(IParser parser)
private static (TestMode Mode, string? Unsupported) ReadFlags(IParser parser)
{
parser.Consume<MappingStart>();

TestMode mode = TestMode.Forward;
string? unsupported = null;

while (parser.Current is not MappingEnd)
{
Expand All @@ -185,19 +201,26 @@ private static TestMode ReadFlags(IParser parser)
throw new NotSupportedException($"unsupported flag '{key}'");
}

mode = ParseTestMode(value);
(mode, unsupported) = ParseTestMode(value);
}

parser.Consume<MappingEnd>();

return mode;
return (mode, unsupported);
}

private static TestMode ParseTestMode(string value) => value switch
/// <summary>
/// hyphenate and display are liblouis features this harness does not drive yet — they map to
/// <c>Hyphenate</c> and <c>DotsToCharacters</c>/<c>CharactersToDots</c> — so they come back as
/// <see cref="TestMode.Unsupported"/>, named so the entries they drop can be counted. An
/// unrecognised mode still throws.
/// </summary>
private static (TestMode Mode, string? Unsupported) ParseTestMode(string value) => value switch
{
"forward" => TestMode.Forward,
"backward" => TestMode.Backward,
"bothDirections" => TestMode.BothDirections,
"forward" => (TestMode.Forward, null),
"backward" => (TestMode.Backward, null),
"bothDirections" => (TestMode.BothDirections, null),
"hyphenate" or "hyphenateBraille" or "display" => (TestMode.Unsupported, $"testmode: {value}"),
_ => throw new NotSupportedException($"unsupported testmode '{value}'"),
};

Expand All @@ -207,7 +230,9 @@ private static void ReadTests(
List<(string Query, string? AssertMatch)> tables,
string displayTable,
TestMode mode,
List<BrailleSpecCase> cases)
string? unsupportedMode,
List<BrailleSpecCase> cases,
IDictionary<string, int> skipped)
{
parser.Consume<SequenceStart>();

Expand All @@ -216,22 +241,40 @@ private static void ReadTests(
SequenceStart entryStart = parser.Consume<SequenceStart>();
int line = (int)entryStart.Start.Line;

string input = Unescape(parser.Consume<Scalar>().Value);
string expected = Unescape(parser.Consume<Scalar>().Value);
// An entry is [input, expected] or [description, input, expected], the second form
// labelling a group of cases. They are told apart by what follows the first two
// scalars: another scalar means the first was a label.
string first = Unescape(parser.Consume<Scalar>().Value);
string second = Unescape(parser.Consume<Scalar>().Value);
string input, expected;

if (parser.Current is Scalar)
{
input = second;
expected = Unescape(parser.Consume<Scalar>().Value);
}
else
{
input = first;
expected = second;
}

var xfail = XFail.None;
TestMode entryMode = mode;
bool skip = false;
string? unsupported = unsupportedMode;

if (parser.Current is MappingStart)
{
(xfail, entryMode, skip) = ReadTestOptions(parser, mode);
(xfail, entryMode, unsupported) = ReadTestOptions(parser, mode, unsupportedMode);
}

parser.Consume<SequenceEnd>();

if (skip)
// Counted once per dropped entry, under the construct that dropped it, so the tally says
// how much of the spec went unchecked.
if (unsupported is not null)
{
skipped[unsupported] = skipped.TryGetValue(unsupported, out int n) ? n + 1 : 1;
continue;
}

Expand Down Expand Up @@ -275,13 +318,16 @@ private enum XFail
Both = Forward | Backward,
}

private static (XFail XFail, TestMode Mode, bool Skip) ReadTestOptions(
IParser parser, TestMode mode)
private static (XFail XFail, TestMode Mode, string? Unsupported) ReadTestOptions(
IParser parser, TestMode mode, string? unsupportedMode)
{
parser.Consume<MappingStart>();

var xfail = XFail.None;
bool skip = false;

// Kept apart from the mode: a testmode later in the same options may replace the mode, but
// must not bring back an entry an unsupported option has already dropped.
string? unsupportedOption = null;

while (parser.Current is not MappingEnd)
{
Expand All @@ -294,15 +340,23 @@ private static (XFail XFail, TestMode Mode, bool Skip) ReadTestOptions(
break;

case "testmode":
mode = ParseTestMode(parser.Consume<Scalar>().Value);
(mode, unsupportedMode) = ParseTestMode(parser.Consume<Scalar>().Value);
break;

// Emphasis is applied through typeform, which this prototype does not drive yet.
// Skip the value so the rest of the file still parses, and drop the case: silently
// running it without the typeform would compare against the wrong expectation.
// Options liblouis supports that this harness does not drive. Each changes what the
// expected output means, so running the case without honouring it would compare
// against the wrong thing. The value is consumed and the case dropped.
case "typeform":
case "mode":
case "inputPos":
case "outputPos":
case "cursorPos":
case "cursorOutPos":
case "maxOutputLength":
case "realInputLength":
case "expected_typeform":
parser.SkipThisAndNestedEvents();
skip = true;
unsupportedOption ??= $"test option: {key}";
break;

default:
Expand All @@ -312,7 +366,7 @@ private static (XFail XFail, TestMode Mode, bool Skip) ReadTestOptions(

parser.Consume<MappingEnd>();

return (xfail, mode, skip);
return (xfail, mode, unsupportedOption ?? unsupportedMode);
}

/// <summary>
Expand Down Expand Up @@ -355,10 +409,9 @@ private static XFail ReadXFail(IParser parser)

/// <summary>
/// The specs use single quoted scalars, where YAML performs no escape processing at all, and
/// rely on liblouis to interpret the escapes itself. Only the forms the Danish specs actually
/// use are handled: \xNNNN and \uNNNN code points, and \\ for a literal backslash.
/// Without the backslash case, 'at\\bliver' parses as two backslashes and translates to two
/// cells where upstream expects one.
/// rely on liblouis to interpret the escapes itself. Only the forms the specs use are handled:
/// \xNNNN and \uNNNN code points, and \\ for a literal backslash. Without the backslash case,
/// 'at\\bliver' parses as two backslashes and translates to two cells where one is expected.
/// </summary>
private static string Unescape(string value)
{
Expand Down
Loading
Loading