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.
294 lines
10 KiB
294 lines
10 KiB
|
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']}';
|