// 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 vectors = jsonDecode(pathruleVectorsJson) as Map; List> cases(String name) => (vectors[name]! as List).cast>(); String cp(List 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 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>(); 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 = []; 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 = []; 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 []) 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(); 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 c) => c['name'] as String? ?? 'paths ${c['in']}';