The update removed the files of sources no longer wanted first, Korean's words from Wiktionary among them, before KRDict's replacement was written, which on the first run after v0.16.0 came after Wiktionary's 3 GB conversion: the server unloaded Korean meanwhile, and for good if KRDict failed. Removal now comes last, and a replaced source stays until its replacement is there. A bare curate update, without --languages, no longer removes every other language. A KRDict export of another shape (the page's buttons reordered) is refused rather than written as a dictionary of no words over Korean's. The server never unloads from an empty dictionaries folder (a volume made anew), and a failed unload no longer stops the files from loading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
144 lines
4.8 KiB
Dart
144 lines
4.8 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:archive/archive.dart';
|
|
import 'package:halkyon_learn_curator/src/sources/krdict.dart';
|
|
import 'package:halkyon_learn_curator/src/sources/wiktionary.dart';
|
|
import 'package:halkyon_learn_curator/src/wordfreq.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
/// The fixture packed as KRDict packs its export: its first file.
|
|
List<int> export() => ZipEncoder().encodeBytes(
|
|
Archive()..add(
|
|
ArchiveFile.bytes(
|
|
'1_5000_20260919.json',
|
|
File('test/fixtures/krdict-sample.json').readAsBytesSync(),
|
|
),
|
|
),
|
|
);
|
|
|
|
Future<List<Map<String, Object?>>> converted({
|
|
WordFrequencies? frequencies,
|
|
WiktionaryKorean? korean,
|
|
}) async {
|
|
final (:version, :entries) = krdictExport(export());
|
|
return [
|
|
for (final line in await krdictDictionary(
|
|
entries,
|
|
sourceVersion: version,
|
|
frequencies: frequencies,
|
|
korean: korean,
|
|
).toList())
|
|
jsonDecode(line) as Map<String, Object?>,
|
|
];
|
|
}
|
|
|
|
test('an export of another shape is refused rather than written as a '
|
|
'dictionary of no words', () async {
|
|
for (final files in [
|
|
<ArchiveFile>[],
|
|
[ArchiveFile.bytes('1_5000_20260919.xml', utf8.encode('<xml/>'))],
|
|
[
|
|
ArchiveFile.bytes(
|
|
'1_5000_20260919.json',
|
|
utf8.encode(jsonEncode({'LexicalResource': <String, Object?>{}})),
|
|
),
|
|
],
|
|
]) {
|
|
final archive = Archive();
|
|
files.forEach(archive.add);
|
|
final zip = ZipEncoder().encodeBytes(archive);
|
|
final (version: _, :entries) = krdictExport(zip);
|
|
|
|
await expectLater(entries.toList(), throwsFormatException);
|
|
}
|
|
});
|
|
|
|
test("KRDict's words with their level, pronunciation where it differs from "
|
|
'the spelling, Hanja, forms and English senses', () async {
|
|
final [header, ...entries] = await converted();
|
|
|
|
expect(header['source'], 'krdict');
|
|
expect(header['language'], 'ko');
|
|
expect(header['sourceVersion'], '2026-09-19');
|
|
expect(
|
|
[
|
|
for (final credit in header['credits']! as List)
|
|
(credit as Map)['name'],
|
|
],
|
|
['한국어기초사전 (Basic Korean Dictionary)'],
|
|
);
|
|
final school = entries.singleWhere((entry) => entry['headword'] == '학교');
|
|
expect(school['id'], '73276');
|
|
expect(school['reading'], '학꾜');
|
|
expect(school['labels'], ['level:beginner']);
|
|
expect(school['keys'], containsAll(['학교', '學校', 'school']));
|
|
const definition =
|
|
'An institution that educates students according to a specific '
|
|
'purpose, curriculum, system, etc.';
|
|
expect(school['entry'], {
|
|
'pos': 'noun',
|
|
'hanja': ['學校'],
|
|
'senses': [
|
|
{
|
|
'glosses': ['school'],
|
|
'info': [definition],
|
|
},
|
|
],
|
|
});
|
|
final read = entries.singleWhere((entry) => entry['headword'] == '읽다');
|
|
expect(read['reading'], '익따');
|
|
expect(read['textKeys'], containsAll(['읽다', '읽어', '읽고']));
|
|
expect(
|
|
(read['entry']! as Map)['inflections'],
|
|
contains(equals({'text': '읽어'})),
|
|
);
|
|
final together = entries.where((entry) => entry['headword'] == '같이');
|
|
expect(
|
|
[for (final entry in together) (entry['entry']! as Map)['pos']],
|
|
['particle', 'adv'],
|
|
);
|
|
});
|
|
|
|
test('an idiom or a proverb is an entry of its own, never found in running '
|
|
'text, with an id told apart from its word', () async {
|
|
final [_, ...entries] = await converted();
|
|
|
|
final proverb = entries.singleWhere(
|
|
(entry) => entry['headword'] == '가는 날이 장날',
|
|
);
|
|
expect(proverb['id'], '27500:가는 날이 장날');
|
|
expect((proverb['entry']! as Map)['pos'], 'proverb');
|
|
expect(proverb.containsKey('textKeys'), isFalse);
|
|
expect(
|
|
entries.singleWhere((entry) => entry['headword'] == '가다')['id'],
|
|
'27500',
|
|
);
|
|
});
|
|
|
|
test("Wiktionary's forms find a word in text; a verb rarely written as "
|
|
'the dictionary writes it ranks by its most used form', () async {
|
|
final korean = WiktionaryKorean()..forms['가다'] = {'가요', '갔어요', '갑니다'};
|
|
final [_, ...entries] = await converted(
|
|
korean: korean,
|
|
frequencies: WordFrequencies(const {'갔어요': 50}),
|
|
);
|
|
|
|
final go = entries.singleWhere((entry) => entry['headword'] == '가다');
|
|
expect(go['textKeys'], containsAll(['가다', '가요', '갔어요']));
|
|
expect(go['labels'], contains('common'));
|
|
expect(go['rank'], 100000 + 100);
|
|
});
|
|
|
|
test("the export's JSON download is the third its page offers", () {
|
|
expect(
|
|
krdictJsonDownloadOf('''
|
|
<button onclick="batchDownload('215')">엑셀</button>
|
|
<button onclick="batchDownload('216')">XML</button>
|
|
<button onclick="batchDownload('217')">Json</button>'''),
|
|
'217',
|
|
);
|
|
expect(krdictJsonDownloadOf("batchDownload('0')"), isNull);
|
|
});
|
|
}
|