You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/test/pathrule_test.dart

294 lines
10 KiB

Stage 4a: the rules of paths and texts on the Unicode tables lib/src/pathrule.dart ports internal/pathrule of datekeys-go at c531e93 (spec §29.5, §29.5.1, §29.6): NFD with the canonical ordering and Hangul, the case folding, the simple lowercase, Default_Ignorable with the whitelist of R4, the best-fit projections of R6c, every rule of a path (R2 to R6c and R10), the tree (R7 with its key and the two paths it names, and R9), and the texts of the comment and the declared author, with the texts of Go. canonicalTables is pathrule.Canonical, and a test recomputes tablesDigest from the lists. Go reads a string as bytes, and so does this port: the functions whose name ends in Utf8 take the bytes of a Go string, where a byte that is not valid UTF-8 is the rune U+FFFD, and the limits count bytes; the others take a String as utf8Bytes writes it. Each rule returns its violation, as in Go, and only the public functions throw. tool/pathrule_go_vectors.go runs in an export of datekeys-go, since internal/pathrule cannot be imported from outside its tree, and writes test/vectors/pathrule_vectors.json and its Dart copy: the cases of the tests of Go and of datekeys-ts, 1300 strings and 350 trees drawn from a fixed seed (marks, Hangul, ignorables, emoji, best-fit look-alikes, device names, 8.3 aliases, limits, texts and invalid UTF-8), the cases of R9, code points, and for each plane the SHA-256 of one line per code point of each function. Every code point of every plane gives the results of Go: planes 0, 1 and 14 also on Node.js. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
// The rules of the paths and texts of a format 3 head (spec §29.5, §29.6)
// against test/vectors/pathrule_vectors.json, whose expected values the Go
// reference computed (tool/pathrule_go_vectors.go): the cases of the tests
// of internal/pathrule and of datekeys-ts, and strings and trees drawn from a
// fixed seed, bytes that are not valid UTF-8 among them. The vectors come
// from a Dart constant, so that these tests also run compiled to
// JavaScript, where a String counts UTF-16 code units and the integers are
// doubles. Every code point of three planes is checked here, those of the
// others and the shared vectors of testdata/ in pathrule_vm_test.dart.
import 'dart:convert';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/pathrule.dart';
import 'package:datekeys/src/pathrule_tables.dart';
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'pathrule_planes.dart';
import 'vectors/pathrule_vectors.g.dart';
final Map<String, Object?> vectors =
jsonDecode(pathruleVectorsJson) as Map<String, Object?>;
List<Map<String, Object?>> cases(String name) =>
(vectors[name]! as List).cast<Map<String, Object?>>();
String cp(List<int> runes) => String.fromCharCodes(runes);
/// "ok", or the message of the [PathRuleException] that [f] throws, as the
/// vectors write the result of Go.
String outcome(void Function() f) {
try {
f();
return 'ok';
} on PathRuleException catch (e) {
return e.message;
}
}
/// The [PathRuleException] that [f] throws.
PathRuleException violation(void Function() f) {
try {
f();
} on PathRuleException catch (e) {
return e;
}
throw StateError('no violation');
}
/// A case of the vectors for a failure message: its name, or its bytes.
String label(Map<String, Object?> c) => c['name'] as String? ?? 'in ${c['in']}';
void main() {
group('the tables', () {
test('match their digest, recomputed from the lists as Go does', () {
final digest = toHex(sha256(utf8Bytes(canonicalTables())));
expect(digest, tablesDigest);
expect(vectors['tables_digest'], tablesDigest);
expect(unicodeVersion, vectors['unicode_version']);
expect(sources, hasLength(vectors['sources']));
expect(bestFitTables, hasLength(15));
});
test('the limits are those of Go', () {
expect(vectors['limits'], {
'max_author_len': maxAuthorLen,
'max_comment_len': maxCommentLen,
'max_implicit_dirs': maxImplicitDirs,
'max_path_len': maxPathLen,
'max_segment_len': maxSegmentLen,
'max_segment_utf16': maxSegmentUtf16,
'max_segments': maxSegments,
});
});
test('Assigned, DefaultIgnorable and Lower of code points', () {
final points = (vectors['code_points']! as List).cast<List<Object?>>();
expect(points, hasLength(greaterThan(1000)));
for (final p in points) {
final r = p[0]! as int;
final name = codePointName(r);
expect(isAssigned(r), p[1] == 1, reason: '$name assigned');
expect(isDefaultIgnorable(r), p[2] == 1, reason: '$name ignorable');
expect(lower(r), p[3], reason: '$name lower');
}
});
});
group('strings', () {
test('CheckPath, CheckComment, CheckAuthor, NFD and Key of Go', () {
final failures = <String>[];
void check(String what, Object? got, Object? want, String c) {
if (got != want) failures.add('$c: $what $got, want $want');
}
final all = cases('strings');
expect(all, hasLength(greaterThan(1400)));
for (final c in all) {
final input = fromHex(c['in']! as String);
final name = label(c);
final comment = c['comment'];
check('path', outcome(() => checkPathUtf8(input)), c['path'], name);
check('comment', outcome(() => checkCommentUtf8(input)), comment, name);
check(
'author',
outcome(() => checkAuthorUtf8(input)),
c['author'] ?? comment,
name,
);
check('nfd', toHex(nfdUtf8(input)), c['nfd'], name);
check('key', toHex(pathKeyUtf8(input)), c['key'], name);
}
expect(failures, isEmpty, reason: failures.take(20).join('\n'));
});
test('a String is its UTF-8, a lone surrogate the bytes of Go', () {
// Every valid UTF-8 input, as a String, gives the same results; and a
// lone surrogate is the three bytes of its code point, which Go reads
// as three U+FFFD.
var strings = 0;
for (final c in cases('strings')) {
final s = decodeUtf8(fromHex(c['in']! as String));
if (s == null) continue;
strings++;
final name = label(c);
expect(outcome(() => checkPath(s)), c['path'], reason: name);
expect(outcome(() => checkComment(s)), c['comment'], reason: name);
expect(
outcome(() => checkAuthor(s)),
c['author'] ?? c['comment'],
reason: name,
);
expect(utf8Bytes(nfd(s)), fromHex(c['nfd']! as String), reason: name);
expect(
utf8Bytes(pathKey(s)),
fromHex(c['key']! as String),
reason: name,
);
}
expect(strings, greaterThan(1000));
final surrogate = cases('strings')
.singleWhere((c) => c['name'] == 'a surrogate in UTF-8 bytes');
expect(surrogate['in'], 'eda080');
final lone = cp([0xd800]);
expect(outcome(() => checkPath(lone)), surrogate['path']);
expect(outcome(() => checkComment(lone)), surrogate['comment']);
expect(utf8Bytes(nfd(lone)), fromHex(surrogate['nfd']! as String));
expect(utf8Bytes(pathKey(lone)), fromHex(surrogate['key']! as String));
});
test('Fold maps each code point alone', () {
// The planes of pathrule_vm_test.dart check Fold code point by code
// point against Go; on a string it is their concatenation.
for (final c in cases('strings')) {
final input = fromHex(c['in']! as String);
final each = <int>[];
for (final r in decodeUtf8(nfdUtf8(input))!.runes) {
each.addAll(foldUtf8(utf8Bytes(cp([r]))));
}
expect(foldUtf8(nfdUtf8(input)), each, reason: label(c));
}
});
test('the named cases of Go, with their exact texts', () {
final named = {
for (final c in cases('strings'))
if (c['name'] != null) c['name']! as String: c,
};
expect(named, hasLength(greaterThan(140)));
String path(String name) => named[name]!['path']! as String;
// A few of them as the reader of a head sees them.
expect(outcome(() => checkPath('a${cp([9])}b')), path('TAB'));
expect(path('TAB'), 'R4: segment 1: control U+0009');
expect(
outcome(() => checkPath('CON.txt')),
'R6: segment 1: CON is a reserved device name',
);
expect(path('CON.txt'), 'R6: segment 1: CON is a reserved device name');
expect(
path('.datekeys-x'),
'R10: the first segment starts with ".datekeys-"',
);
expect(outcome(() => checkPath('.datekeys-x')), path('.datekeys-x'));
expect(
outcome(() => checkPath(cp([0x3000, 0x61]))),
path('U+3000 first'),
);
expect(
outcome(() => checkPath(cp(List.filled(127, 0x390)))),
path('127 times U+0390: 381 UTF-16 units after NFD'),
);
expect(
outcome(() => checkAuthor(' Ana')),
named['a leading space in an author']!['author'],
);
});
test('a violation carries its rule and detail apart', () {
final e = violation(() => checkPath('a/b${cp([0x202e])}'));
expect(e.rule, 'R4');
expect(e.detail, 'segment 2: invisible U+202E');
expect(e.message, 'R4: segment 2: invisible U+202E');
expect(e.toString(), e.message);
expect(e.paths, (0, 0));
final t = violation(() => checkComment('ok\na${cp([0xfe0f])}'));
expect(t.rule, 'text');
expect(
t.detail,
'line 2: U+FE0F is not part of an emoji variation sequence',
);
});
test('limits count UTF-8 bytes, not UTF-16 code units', () {
// 85 emoji are 170 UTF-16 code units and 340 bytes: R3 counts bytes.
final emoji = cp(List.filled(85, 0x1f600));
expect(emoji.length, 170);
expect(
outcome(() => checkPath(emoji)),
'R3: segment 1: 340 bytes, more than 255',
);
// R6b counts code points: an astral letter is one, not two.
final astral = '${cp([0x10400])}BCDEF~1';
expect(astral.length, 9);
expect(
outcome(() => checkPath(astral)),
'R6b: segment 1: the segment has the form of an 8.3 alias',
);
});
});
test('every code point of planes 0, 1 and 14 gives the results of Go', () {
final planes = cases('planes');
for (final plane in sharedPlanes) {
checkPlane(plane, planes[plane]);
}
});
group('trees', () {
test('CheckTree of Go, with the two paths of R7', () {
final all = cases('trees');
expect(all, hasLength(greaterThan(350)));
var r7 = 0;
for (final c in all) {
final paths = [
for (final p in (c['in'] as List?) ?? const <Object?>[])
fromHex(p! as String),
];
final name = label2(c);
final want = c['result']! as String;
expect(outcome(() => checkTreeUtf8(paths)), want, reason: name);
final pair = (c['paths']! as List).cast<int>();
if (want != 'ok') {
final e = violation(() => checkTreeUtf8(paths));
expect(e.paths, (pair[0], pair[1]), reason: name);
if (e.rule == 'R7') r7++;
} else {
expect(pair, [0, 0], reason: name);
}
final strings = [for (final p in paths) decodeUtf8(p)];
if (strings.every((s) => s != null)) {
expect(
outcome(() => checkTree([for (final s in strings) s!])),
want,
reason: name,
);
}
}
expect(r7, greaterThan(50));
});
test('R9 counts the distinct folders of every path', () {
for (final c in cases('folders')) {
final prefix = c['prefix']! as String;
final suffix = c['suffix']! as String;
final digits = c['digits']! as int;
final paths = [
for (var i = 0; i < (c['count']! as int); i++)
'$prefix${i.toString().padLeft(digits, '0')}$suffix',
];
expect(
outcome(() => checkTree(paths)),
c['result'],
reason: c['name']! as String,
);
}
});
});
}
/// A tree of the vectors for a failure message: its name, or its paths.
String label2(Map<String, Object?> c) =>
c['name'] as String? ?? 'paths ${c['in']}';

Powered by TurnKey Linux.