From 2a0e17cab70ca85eaf534f19a6bc54174a110052 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 31 Oct 2024 09:29:46 +0700 Subject: [PATCH] fix(developer): handle missing files in kmc-model If a file is not found, loadfile returns null, which kmc-model now handles with a clear error message rather than a generic exception. Checks added for missing .model.ts and missing wordlist.tsv files. Added corresponding unit tests. Fixes: #12553 Fixes: KEYMAN-DEVELOPER-294 --- developer/src/kmc-model/src/build-trie.ts | 3 ++ .../kmc-model/src/lexical-model-compiler.ts | 6 ++- .../kmc-model/src/model-compiler-messages.ts | 22 +++++++- .../example.qaa.missing-wordlist.model.ts | 5 ++ developer/src/kmc-model/test/test-messages.ts | 54 ++++++++++++++++++- 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 developer/src/kmc-model/test/fixtures/invalid-models/example.qaa.missing-wordlist/example.qaa.missing-wordlist.model.ts diff --git a/developer/src/kmc-model/src/build-trie.ts b/developer/src/kmc-model/src/build-trie.ts index 8bbc6aaa53a..366c4f8871c 100644 --- a/developer/src/kmc-model/src/build-trie.ts +++ b/developer/src/kmc-model/src/build-trie.ts @@ -169,6 +169,9 @@ class WordListFromFilename { *lines() { const data = callbacks.loadFile(this.name); + if(!data) { + throw new ModelCompilerError(ModelCompilerMessages.Error_WordlistFileNotFound({filename:this.name})); + } const contents = new TextDecoder(detectEncoding(data)).decode(data); yield *enumerateLines(contents.split(NEWLINE_SEPARATOR)); } diff --git a/developer/src/kmc-model/src/lexical-model-compiler.ts b/developer/src/kmc-model/src/lexical-model-compiler.ts index e1071512a9d..922646a3dbb 100644 --- a/developer/src/kmc-model/src/lexical-model-compiler.ts +++ b/developer/src/kmc-model/src/lexical-model-compiler.ts @@ -125,7 +125,11 @@ export class LexicalModelCompiler implements KeymanCompiler { */ public loadFromFilename(filename: string): LexicalModelSource { - let sourceCode = new TextDecoder().decode(callbacks.loadFile(filename)); + const data = callbacks.loadFile(filename); + if(!data) { + throw new ModelCompilerError(ModelCompilerMessages.Error_ModelFileNotFound({filename})); + } + let sourceCode = new TextDecoder().decode(data); // Compile the module to JavaScript code. // NOTE: transpile module does a very simple TS to JS compilation. // It DOES NOT check for types! diff --git a/developer/src/kmc-model/src/model-compiler-messages.ts b/developer/src/kmc-model/src/model-compiler-messages.ts index 51e4042ea8b..dad81470be1 100644 --- a/developer/src/kmc-model/src/model-compiler-messages.ts +++ b/developer/src/kmc-model/src/model-compiler-messages.ts @@ -7,8 +7,8 @@ const SevHint = CompilerErrorSeverity.Hint | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; const SevFatal = CompilerErrorSeverity.Fatal | Namespace; -const m = (code: number, message: string) : CompilerEvent => ({ - ...CompilerMessageSpec(code, message), +const m = (code: number, message: string, detail?: string) : CompilerEvent => ({ + ...CompilerMessageSpec(code, message, detail), line: ModelCompilerMessageContext.line, filename: ModelCompilerMessageContext.filename, }); @@ -70,6 +70,24 @@ export class ModelCompilerMessages { static ERROR_UnsupportedScriptOverride = SevError | 0x000A; static Error_UnsupportedScriptOverride = (o:{option:string}) => m(this.ERROR_UnsupportedScriptOverride, `Unsupported script override: ${def(o.option)}`); + + static ERROR_ModelFileNotFound = SevError | 0x000B; + static Error_ModelFileNotFound = (o:{filename:string}) => m( + this.ERROR_ModelFileNotFound, + `Lexical model source file ${def(o.filename)} was not found`, + `The model source file was not found on the disk. Verify that you have + the correct path to the file.` + ); + + static ERROR_WordlistFileNotFound = SevError | 0x000C; + static Error_WordlistFileNotFound = (o:{filename:string}) => m( + this.ERROR_WordlistFileNotFound, + `Wordlist file ${def(o.filename)} was not found`, + `The wordlist file was not found on the disk. Verify that you have + the correct path to the file.` + ); + + }; /** diff --git a/developer/src/kmc-model/test/fixtures/invalid-models/example.qaa.missing-wordlist/example.qaa.missing-wordlist.model.ts b/developer/src/kmc-model/test/fixtures/invalid-models/example.qaa.missing-wordlist/example.qaa.missing-wordlist.model.ts new file mode 100644 index 00000000000..2e6c20b2c65 --- /dev/null +++ b/developer/src/kmc-model/test/fixtures/invalid-models/example.qaa.missing-wordlist/example.qaa.missing-wordlist.model.ts @@ -0,0 +1,5 @@ +const source: LexicalModelSource = { + format: 'trie-1.0', + sources: ['wordlist.tsv'], +}; +export default source; diff --git a/developer/src/kmc-model/test/test-messages.ts b/developer/src/kmc-model/test/test-messages.ts index 2f87dfd7181..f9d3eb07b8a 100644 --- a/developer/src/kmc-model/test/test-messages.ts +++ b/developer/src/kmc-model/test/test-messages.ts @@ -1,10 +1,62 @@ import 'mocha'; +import { assert } from 'chai'; import { ModelCompilerMessages } from '../src/model-compiler-messages.js'; -import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; +import { LexicalModelCompiler } from '../src/lexical-model-compiler.js'; +import { makePathToFixture } from './helpers/index.js'; describe('ModelCompilerMessages', function () { + + const callbacks = new TestCompilerCallbacks(); + + this.beforeEach(function() { + callbacks.clear(); + }); + + this.afterEach(function() { + if(this.currentTest?.isFailed()) { + callbacks.printMessages(); + } + }); + it('should have a valid ModelCompilerMessages object', function() { return verifyCompilerMessagesObject(ModelCompilerMessages, CompilerErrorNamespace.ModelCompiler); }); + + async function testForMessage(fixture: string[], messageId?: number) { + const compiler = new LexicalModelCompiler(); + assert.isTrue(await compiler.init(callbacks, null)); + + const modelPath = makePathToFixture(...fixture); + + // Note: throwing away compile results (just to memory) + await compiler.run(modelPath, null); + + if(messageId) { + assert.isTrue(callbacks.hasMessage(messageId), `messageId ${messageId.toString(16)} not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + assert.lengthOf(callbacks.messages, 1, `messages should have 1 entry, instead has: `+JSON.stringify(callbacks.messages,null,2)); + } else { + assert.lengthOf(callbacks.messages, 0, `messages should be empty, but instead got: `+JSON.stringify(callbacks.messages,null,2)); + } + } + + // ERROR_ModelFileNotFound + + it('should generate ERROR_ModelFileNotFound if a .model.ts file is missing', async function() { + await testForMessage( + ['invalid-models', 'missing-file.model.ts'], + ModelCompilerMessages.ERROR_ModelFileNotFound + ); + }); + + // ERROR_WordlistFileNotFound + + it('should generate ERROR_WordlistFileNotFound if a .tsv file is missing', async function() { + await testForMessage( + ['invalid-models', 'example.qaa.missing-wordlist', 'example.qaa.missing-wordlist.model.ts'], + ModelCompilerMessages.ERROR_WordlistFileNotFound + ); + }); + });