fix(ui): harden settings focus and semantics
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../scripts/generate_ducet_ranks.dart';
|
||||
|
||||
void main() {
|
||||
late Directory temporaryDirectory;
|
||||
|
||||
setUp(() async {
|
||||
temporaryDirectory = await Directory.systemTemp.createTemp('plezy_ducet_test_');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (await temporaryDirectory.exists()) {
|
||||
await temporaryDirectory.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
test('pins immutable Unicode and CLDR source descriptors', () {
|
||||
expect(allKeysSource.url, 'https://www.unicode.org/Public/UCA/13.0.0/allkeys.txt');
|
||||
expect(allKeysSource.sha256Digest, 'a3255d45b7af97f4dc14fb8364d7573b434425e5c58cacf00d16901ce081c78d');
|
||||
expect(allKeysSource.bundledFileName, 'allkeys-13.0.0.txt.gz');
|
||||
expect(allKeysSource.licenseUrl, 'https://www.unicode.org/license.txt');
|
||||
expect(
|
||||
fractionalUcaSource.url,
|
||||
'https://raw.githubusercontent.com/unicode-org/cldr/'
|
||||
'651afecf9ccf1541a49306993e8210fa2209aa0b/common/uca/FractionalUCA.txt',
|
||||
);
|
||||
expect(fractionalUcaSource.sha256Digest, 'a6144d0c8c19cc899a5d2f48fbc14e3e31e819049a73aa86b67111f1f3f81637');
|
||||
expect(fractionalUcaSource.bundledFileName, 'FractionalUCA-651afecf9ccf1541a49306993e8210fa2209aa0b.txt.gz');
|
||||
expect(fractionalUcaSource.licenseUrl, contains('/651afecf9ccf1541a49306993e8210fa2209aa0b/LICENSE'));
|
||||
});
|
||||
|
||||
test('bundled gzip inputs have deterministic normalized headers and bytes', () {
|
||||
final allKeys = File('scripts/data/${allKeysSource.bundledFileName}').readAsBytesSync();
|
||||
final fractional = File('scripts/data/${fractionalUcaSource.bundledFileName}').readAsBytesSync();
|
||||
|
||||
expect(allKeys.take(8), [0x1F, 0x8B, 0x08, 0, 0, 0, 0, 0]);
|
||||
expect(fractional.take(8), [0x1F, 0x8B, 0x08, 0, 0, 0, 0, 0]);
|
||||
expect(sha256.convert(allKeys).toString(), '744040b99e266f9ec599744e267091c159369a36f6ca7558c12a9eb3e039575f');
|
||||
expect(sha256.convert(fractional).toString(), 'edc486bae663abf83d18c9006d08c1e8bb19986810cf8ab663fa33bee6d22275');
|
||||
});
|
||||
|
||||
test('dense lookup stores rank plus one with a zero sentinel', () {
|
||||
final ranks = buildDenseRankLookup([0x41, 0x24, 0x5C]);
|
||||
|
||||
expect(ranks.length, 0x10000);
|
||||
expect(ranks.codeUnitAt(0x41), 1);
|
||||
expect(ranks.codeUnitAt(0x24), 2);
|
||||
expect(ranks.codeUnitAt(0x5C), 3);
|
||||
expect(ranks.codeUnitAt(0x42), 0);
|
||||
});
|
||||
|
||||
test('dense lookup rejects duplicate, out-of-BMP, and overflowing orders', () {
|
||||
expect(() => buildDenseRankLookup([0x41, 0x41]), throwsStateError);
|
||||
expect(() => buildDenseRankLookup([-1]), throwsRangeError);
|
||||
expect(() => buildDenseRankLookup([0x10000]), throwsRangeError);
|
||||
expect(() => buildDenseRankLookup(List<int>.generate(0x10000, (index) => index)), throwsStateError);
|
||||
});
|
||||
|
||||
test('parse and render seams are deterministic and escape Dart interpolation characters', () {
|
||||
const allKeysText = '''
|
||||
0041 ; [.0100.0020.0002]
|
||||
30A2 ; [.0200.0020.000F]
|
||||
3042 ; [.0200.0020.000D]
|
||||
''';
|
||||
const fractionalText = '[radical 1=⼀一:一丁]';
|
||||
|
||||
final firstAllKeys = parseAllKeys(allKeysText);
|
||||
final secondAllKeys = parseAllKeys(allKeysText);
|
||||
final (firstCjk, firstKangxi) = parseRadicals(fractionalText);
|
||||
final (secondCjk, secondKangxi) = parseRadicals(fractionalText);
|
||||
final firstOrder = buildOrder(firstAllKeys, firstCjk);
|
||||
final secondOrder = buildOrder(secondAllKeys, secondCjk);
|
||||
|
||||
expect(firstOrder, secondOrder);
|
||||
expect(renderDucetOrder(firstOrder, firstKangxi), renderDucetOrder(secondOrder, secondKangxi));
|
||||
|
||||
final ranksWithSpecialCodeUnits = List<int>.generate(0x5C, (index) => 0x1000 + index);
|
||||
final rendered = renderDucetOrder(ranksWithSpecialCodeUnits, const {});
|
||||
expect(rendered, contains(r"\'"));
|
||||
expect(rendered, contains(r'\\'));
|
||||
expect(rendered, contains(r'\$'));
|
||||
});
|
||||
|
||||
test('explicit inputs are both verified before parsing and writing', () async {
|
||||
const allKeysText = '0041 ; [.0100.0020.0002]\n';
|
||||
const fractionalText = '[radical 1=⼀一:一]\n';
|
||||
final allKeysFile = File('${temporaryDirectory.path}/allkeys.txt')..writeAsStringSync(allKeysText);
|
||||
final fractionalFile = File('${temporaryDirectory.path}/FractionalUCA.txt')..writeAsStringSync(fractionalText);
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
var downloads = 0;
|
||||
|
||||
await generateDucetRanks(
|
||||
useBundledSources: false,
|
||||
explicitAllKeys: allKeysFile,
|
||||
explicitFractionalUca: fractionalFile,
|
||||
output: output,
|
||||
allKeysDescriptor: descriptorFor('allkeys', allKeysText),
|
||||
fractionalUcaDescriptor: descriptorFor('fractional', fractionalText),
|
||||
downloader: (_) async {
|
||||
downloads++;
|
||||
throw StateError('network must not be used');
|
||||
},
|
||||
);
|
||||
|
||||
expect(downloads, 0);
|
||||
expect(output.readAsStringSync(), contains('const String _ducetRanks ='));
|
||||
expect(output.readAsStringSync(), isNot(contains('_buildRanks')));
|
||||
});
|
||||
|
||||
test('rejects an explicit digest mismatch without replacing output', () async {
|
||||
final allKeysFile = File('${temporaryDirectory.path}/allkeys.txt')..writeAsStringSync('tampered');
|
||||
final fractionalFile = File('${temporaryDirectory.path}/FractionalUCA.txt')..writeAsStringSync('fractional');
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
var writerCalled = false;
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
useBundledSources: false,
|
||||
explicitAllKeys: allKeysFile,
|
||||
explicitFractionalUca: fractionalFile,
|
||||
output: output,
|
||||
allKeysDescriptor: descriptorFor('allkeys', 'expected'),
|
||||
fractionalUcaDescriptor: descriptorFor('fractional', 'fractional'),
|
||||
writer: (_, _) => writerCalled = true,
|
||||
),
|
||||
throwsA(isA<SourceIntegrityException>()),
|
||||
);
|
||||
|
||||
expect(writerCalled, isFalse);
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
expect(allKeysFile.readAsStringSync(), 'tampered');
|
||||
});
|
||||
|
||||
test('rejects an invalid cached source without downloading or replacing files', () async {
|
||||
final cache = Directory('${temporaryDirectory.path}/cache')..createSync();
|
||||
final allKeysDescriptor = descriptorFor('allkeys', 'expected allkeys');
|
||||
final fractionalDescriptor = descriptorFor('fractional', 'fractional');
|
||||
final cachedAllKeys = File('${cache.path}/${allKeysDescriptor.cacheFileName}')..writeAsStringSync('tampered cache');
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
var downloads = 0;
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
useBundledSources: false,
|
||||
cacheDirectory: cache,
|
||||
output: output,
|
||||
allKeysDescriptor: allKeysDescriptor,
|
||||
fractionalUcaDescriptor: fractionalDescriptor,
|
||||
downloader: (_) async {
|
||||
downloads++;
|
||||
return response(HttpStatus.ok, utf8.encode('unused'));
|
||||
},
|
||||
),
|
||||
throwsA(isA<SourceIntegrityException>()),
|
||||
);
|
||||
|
||||
expect(downloads, 0);
|
||||
expect(cachedAllKeys.readAsStringSync(), 'tampered cache');
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
});
|
||||
|
||||
test('rejects HTTP status before parsing, caching, or replacing output', () async {
|
||||
final cache = Directory('${temporaryDirectory.path}/cache')..createSync();
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
useBundledSources: false,
|
||||
cacheDirectory: cache,
|
||||
output: output,
|
||||
allKeysDescriptor: descriptorFor('allkeys', 'allkeys'),
|
||||
fractionalUcaDescriptor: descriptorFor('fractional', 'fractional'),
|
||||
downloader: (_) async => response(HttpStatus.notFound, utf8.encode('not found')),
|
||||
),
|
||||
throwsA(isA<SourceIntegrityException>().having((error) => error.message, 'message', contains('HTTP 404'))),
|
||||
);
|
||||
|
||||
expect(cache.listSync(), isEmpty);
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
});
|
||||
|
||||
test('a rejected second download preserves the first cache and output', () async {
|
||||
const allKeysText = '0041 ; [.0100.0020.0002]\n';
|
||||
const fractionalText = '[radical 1=⼀一:一]\n';
|
||||
final cache = Directory('${temporaryDirectory.path}/cache')..createSync();
|
||||
final marker = File('${cache.path}/marker')..writeAsStringSync('keep');
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
var request = 0;
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
useBundledSources: false,
|
||||
cacheDirectory: cache,
|
||||
output: output,
|
||||
allKeysDescriptor: descriptorFor('allkeys', allKeysText),
|
||||
fractionalUcaDescriptor: descriptorFor('fractional', fractionalText),
|
||||
downloader: (_) async {
|
||||
request++;
|
||||
return response(HttpStatus.ok, utf8.encode(request == 1 ? allKeysText : 'tampered fractional'));
|
||||
},
|
||||
),
|
||||
throwsA(isA<SourceIntegrityException>()),
|
||||
);
|
||||
|
||||
expect(request, 2);
|
||||
expect(cache.listSync().map((entity) => entity.path).toList(), [marker.path]);
|
||||
expect(marker.readAsStringSync(), 'keep');
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
});
|
||||
|
||||
test('parse rejection leaves verified inputs, caches, and output untouched', () async {
|
||||
const duplicateAllKeys = '''
|
||||
0041 ; [.0100.0020.0002]
|
||||
0041 ; [.0101.0020.0002]
|
||||
''';
|
||||
const fractionalText = '[radical 1=⼀一:一]\n';
|
||||
final allKeysFile = File('${temporaryDirectory.path}/allkeys.txt')..writeAsStringSync(duplicateAllKeys);
|
||||
final fractionalFile = File('${temporaryDirectory.path}/FractionalUCA.txt')..writeAsStringSync(fractionalText);
|
||||
final cache = Directory('${temporaryDirectory.path}/cache')..createSync();
|
||||
final marker = File('${cache.path}/marker')..writeAsStringSync('keep');
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
useBundledSources: false,
|
||||
explicitAllKeys: allKeysFile,
|
||||
explicitFractionalUca: fractionalFile,
|
||||
cacheDirectory: cache,
|
||||
output: output,
|
||||
allKeysDescriptor: descriptorFor('allkeys', duplicateAllKeys),
|
||||
fractionalUcaDescriptor: descriptorFor('fractional', fractionalText),
|
||||
),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(allKeysFile.readAsStringSync(), duplicateAllKeys);
|
||||
expect(fractionalFile.readAsStringSync(), fractionalText);
|
||||
expect(cache.listSync().map((entity) => entity.path).toList(), [marker.path]);
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
});
|
||||
|
||||
test('default bundled generation is deterministic and never downloads', () async {
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart');
|
||||
var downloads = 0;
|
||||
String? first;
|
||||
String? second;
|
||||
Future<SourceDownloadResponse> failDownloader(Uri _) async {
|
||||
downloads++;
|
||||
throw StateError('bundled generation must not use the network');
|
||||
}
|
||||
|
||||
await generateDucetRanks(output: output, downloader: failDownloader, writer: (_, contents) => first = contents);
|
||||
await generateDucetRanks(output: output, downloader: failDownloader, writer: (_, contents) => second = contents);
|
||||
|
||||
expect(downloads, 0);
|
||||
expect(first, isNotNull);
|
||||
expect(second, first);
|
||||
expect(first, File('lib/data/ducet_order.dart').readAsStringSync());
|
||||
});
|
||||
|
||||
test('tampered bundled gzip fails before replacing output', () async {
|
||||
const allKeysText = '0041 ; [.0100.0020.0002]\n';
|
||||
const fractionalText = '[radical 1=⼀一:一]\n';
|
||||
final bundled = Directory('${temporaryDirectory.path}/bundled')..createSync();
|
||||
final allKeysDescriptor = descriptorFor('allkeys', allKeysText);
|
||||
final fractionalDescriptor = descriptorFor('fractional', fractionalText);
|
||||
File('${bundled.path}/${allKeysDescriptor.bundledFileName}').writeAsBytesSync(const [0x1F, 0x8B, 0x08, 0x00, 0x00]);
|
||||
writeBundledSource(bundled, fractionalDescriptor, fractionalText);
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
bundledSourceDirectory: bundled,
|
||||
output: output,
|
||||
allKeysDescriptor: allKeysDescriptor,
|
||||
fractionalUcaDescriptor: fractionalDescriptor,
|
||||
),
|
||||
throwsA(isA<SourceIntegrityException>()),
|
||||
);
|
||||
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
});
|
||||
|
||||
test('tampered decompressed bundled bytes fail digest before replacing output', () async {
|
||||
const allKeysText = '0041 ; [.0100.0020.0002]\n';
|
||||
const fractionalText = '[radical 1=⼀一:一]\n';
|
||||
final bundled = Directory('${temporaryDirectory.path}/bundled')..createSync();
|
||||
final allKeysDescriptor = descriptorFor('allkeys', allKeysText);
|
||||
final fractionalDescriptor = descriptorFor('fractional', fractionalText);
|
||||
writeBundledSource(bundled, allKeysDescriptor, 'tampered raw bytes');
|
||||
writeBundledSource(bundled, fractionalDescriptor, fractionalText);
|
||||
final output = File('${temporaryDirectory.path}/ducet_order.dart')..writeAsStringSync('old output');
|
||||
|
||||
await expectLater(
|
||||
generateDucetRanks(
|
||||
bundledSourceDirectory: bundled,
|
||||
output: output,
|
||||
allKeysDescriptor: allKeysDescriptor,
|
||||
fractionalUcaDescriptor: fractionalDescriptor,
|
||||
),
|
||||
throwsA(
|
||||
isA<SourceIntegrityException>().having((error) => error.message, 'message', contains('SHA-256 mismatch')),
|
||||
),
|
||||
);
|
||||
|
||||
expect(output.readAsStringSync(), 'old output');
|
||||
});
|
||||
}
|
||||
|
||||
SourceDescriptor descriptorFor(String name, String contents) => SourceDescriptor(
|
||||
name: name,
|
||||
url: 'https://example.test/$name',
|
||||
bundledFileName: '$name.txt.gz',
|
||||
cacheFileName: '$name.txt',
|
||||
sha256Digest: sha256.convert(utf8.encode(contents)).toString(),
|
||||
licenseUrl: 'https://example.test/license',
|
||||
);
|
||||
|
||||
void writeBundledSource(Directory directory, SourceDescriptor descriptor, String contents) {
|
||||
File('${directory.path}/${descriptor.bundledFileName}').writeAsBytesSync(gzip.encode(utf8.encode(contents)));
|
||||
}
|
||||
|
||||
SourceDownloadResponse response(int statusCode, List<int> bytes) =>
|
||||
SourceDownloadResponse(statusCode: statusCode, bytes: Stream<List<int>>.value(bytes));
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../scripts/generate_hid_key_labels.dart';
|
||||
|
||||
void main() {
|
||||
late String validSource;
|
||||
|
||||
setUpAll(() async {
|
||||
validSource = await File(defaultHidKeyLabelsInput).readAsString();
|
||||
});
|
||||
|
||||
test('parse and render are deterministic and reproduce the checked-in artifact', () async {
|
||||
final first = renderHidKeyLabels(parseHidKeyLabelsCatalog(validSource));
|
||||
final second = renderHidKeyLabels(parseHidKeyLabelsCatalog(validSource));
|
||||
|
||||
expect(second, first);
|
||||
expect(first, await File(defaultHidKeyLabelsOutput).readAsString());
|
||||
});
|
||||
|
||||
test('rejects invalid schema, empty groups, group names, keys, labels, and IDs', () {
|
||||
_expectInvalid(validSource, (catalog) => catalog['schemaVersion'] = 2);
|
||||
_expectInvalid(validSource, (catalog) => catalog['groups'] = <Object?>[]);
|
||||
_expectInvalid(validSource, (catalog) => _group(catalog, 0)['name'] = ' ');
|
||||
_expectInvalid(validSource, (catalog) => _group(catalog, 0)['keys'] = <Object?>[]);
|
||||
_expectInvalid(validSource, (catalog) => _key(catalog, 0, 0)['label'] = '');
|
||||
_expectInvalid(validSource, (catalog) => _key(catalog, 0, 0)['id'] = '0000001');
|
||||
_expectInvalid(validSource, (catalog) => _key(catalog, 0, 0)['id'] = '0000001A');
|
||||
});
|
||||
|
||||
test('rejects globally duplicate or noncanonical numeric IDs', () {
|
||||
_expectInvalid(validSource, (catalog) => _key(catalog, 0, 1)['id'] = _key(catalog, 0, 0)['id']);
|
||||
_expectInvalid(validSource, (catalog) => _key(catalog, 0, 1)['id'] = '0000000f');
|
||||
_expectInvalid(validSource, (catalog) => _key(catalog, 1, 0)['id'] = '0000000f');
|
||||
});
|
||||
|
||||
test('escapes controls in labels that require double-quoted Dart literals', () {
|
||||
const catalog = HidKeyCatalog(
|
||||
groups: [
|
||||
HidKeyGroup(
|
||||
name: 'Test',
|
||||
keys: [HidKeyLabel(id: '00000001', label: "Player's\nKey")],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(renderHidKeyLabels(catalog), contains("\"Player's\\nKey\""));
|
||||
});
|
||||
|
||||
test('invalid input fails before the existing output is replaced', () async {
|
||||
final directory = await Directory.systemTemp.createTemp('plezy_hid_generator_test.');
|
||||
addTearDown(() => directory.delete(recursive: true));
|
||||
final input = File('${directory.path}/invalid.json');
|
||||
final output = File('${directory.path}/output.dart');
|
||||
await input.writeAsString('{"schemaVersion":1,"groups":[]}');
|
||||
await output.writeAsString('sentinel');
|
||||
|
||||
await expectLater(generateHidKeyLabels(input.path, output.path), throwsA(isA<FormatException>()));
|
||||
expect(await output.readAsString(), 'sentinel');
|
||||
});
|
||||
|
||||
test('atomic writer replaces the complete destination', () async {
|
||||
final directory = await Directory.systemTemp.createTemp('plezy_hid_atomic_test.');
|
||||
addTearDown(() => directory.delete(recursive: true));
|
||||
final output = File('${directory.path}/output.dart');
|
||||
await output.writeAsString('old');
|
||||
|
||||
await writeFileAtomically(output.path, 'new bytes\n');
|
||||
|
||||
expect(await output.readAsString(), 'new bytes\n');
|
||||
expect(directory.listSync().where((entry) => entry.path.contains('.tmp.')), isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
void _expectInvalid(String source, void Function(Map<String, Object?> catalog) mutate) {
|
||||
final catalog = (jsonDecode(source) as Map).cast<String, Object?>();
|
||||
mutate(catalog);
|
||||
expect(() => parseHidKeyLabelsCatalog(jsonEncode(catalog)), throwsA(isA<FormatException>()));
|
||||
}
|
||||
|
||||
Map<String, Object?> _group(Map<String, Object?> catalog, int index) {
|
||||
final groups = catalog['groups']! as List<Object?>;
|
||||
return (groups[index]! as Map).cast<String, Object?>();
|
||||
}
|
||||
|
||||
Map<String, Object?> _key(Map<String, Object?> catalog, int groupIndex, int keyIndex) {
|
||||
final keys = _group(catalog, groupIndex)['keys']! as List<Object?>;
|
||||
return (keys[keyIndex]! as Map).cast<String, Object?>();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../scripts/generate_iso_639_data.dart';
|
||||
|
||||
void main() {
|
||||
late String validSource;
|
||||
|
||||
setUpAll(() async {
|
||||
validSource = await File(defaultIso639Input).readAsString();
|
||||
});
|
||||
|
||||
test('parse and render are deterministic and reproduce the checked-in artifact', () async {
|
||||
final first = renderIso639Data(parseIso639Catalog(validSource));
|
||||
final second = renderIso639Data(parseIso639Catalog(validSource));
|
||||
|
||||
expect(second, first);
|
||||
expect(first, await File(defaultIso639Output).readAsString());
|
||||
});
|
||||
|
||||
test('rejects invalid schema, keys, primary consistency, names, and field shapes', () {
|
||||
_expectInvalid(validSource, (catalog) => catalog['schemaVersion'] = 2);
|
||||
_expectInvalid(validSource, (catalog) => catalog['languages'] = <String, Object?>{});
|
||||
_expectInvalid(validSource, (catalog) => _entry(catalog, 'aa')['primary'] = 'ab');
|
||||
_expectInvalid(validSource, (catalog) => _entry(catalog, 'aa')['terminology'] = 'AAR');
|
||||
_expectInvalid(validSource, (catalog) => _entry(catalog, 'bo')['bibliographic'] = 'TIB');
|
||||
_expectInvalid(validSource, (catalog) => _entry(catalog, 'aa')['name'] = ' ');
|
||||
|
||||
_expectInvalid(validSource, (catalog) {
|
||||
final languages = _languages(catalog);
|
||||
languages['a'] = languages.remove('aa');
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects duplicate aliases and noncanonical primary ordering', () {
|
||||
_expectInvalid(
|
||||
validSource,
|
||||
(catalog) => _entry(catalog, 'ab')['terminology'] = _entry(catalog, 'aa')['terminology'],
|
||||
);
|
||||
_expectInvalid(
|
||||
validSource,
|
||||
(catalog) => _entry(catalog, 'bo')['bibliographic'] = _entry(catalog, 'aa')['terminology'],
|
||||
);
|
||||
_expectInvalid(validSource, (catalog) {
|
||||
final languages = _languages(catalog);
|
||||
final first = languages.remove('aa');
|
||||
languages['aa'] = first;
|
||||
});
|
||||
});
|
||||
|
||||
test('escapes controls in names that require double-quoted Dart literals', () {
|
||||
const catalog = Iso639Catalog(
|
||||
entries: [Iso639CatalogEntry(primary: 'aa', terminology: 'aaa', bibliographic: null, name: "People's\nLanguage")],
|
||||
);
|
||||
|
||||
expect(renderIso639Data(catalog), contains("\"People's\\nLanguage\""));
|
||||
});
|
||||
|
||||
test('invalid input fails before the existing output is replaced', () async {
|
||||
final directory = await Directory.systemTemp.createTemp('plezy_iso_generator_test.');
|
||||
addTearDown(() => directory.delete(recursive: true));
|
||||
final input = File('${directory.path}/invalid.json');
|
||||
final output = File('${directory.path}/output.dart');
|
||||
await input.writeAsString('{"schemaVersion":1,"languages":{}}');
|
||||
await output.writeAsString('sentinel');
|
||||
|
||||
await expectLater(generateIso639Data(input.path, output.path), throwsA(isA<FormatException>()));
|
||||
expect(await output.readAsString(), 'sentinel');
|
||||
});
|
||||
|
||||
test('atomic writer replaces the complete destination', () async {
|
||||
final directory = await Directory.systemTemp.createTemp('plezy_iso_atomic_test.');
|
||||
addTearDown(() => directory.delete(recursive: true));
|
||||
final output = File('${directory.path}/output.dart');
|
||||
await output.writeAsString('old');
|
||||
|
||||
await writeFileAtomically(output.path, 'new bytes\n');
|
||||
|
||||
expect(await output.readAsString(), 'new bytes\n');
|
||||
expect(directory.listSync().where((entry) => entry.path.contains('.tmp.')), isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
void _expectInvalid(String source, void Function(Map<String, Object?> catalog) mutate) {
|
||||
final catalog = (jsonDecode(source) as Map).cast<String, Object?>();
|
||||
mutate(catalog);
|
||||
expect(() => parseIso639Catalog(jsonEncode(catalog)), throwsA(isA<FormatException>()));
|
||||
}
|
||||
|
||||
Map<String, Object?> _languages(Map<String, Object?> catalog) {
|
||||
return (catalog['languages']! as Map).cast<String, Object?>();
|
||||
}
|
||||
|
||||
Map<String, Object?> _entry(Map<String, Object?> catalog, String code) {
|
||||
return (_languages(catalog)[code]! as Map).cast<String, Object?>();
|
||||
}
|
||||
Reference in New Issue
Block a user