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

// 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.