Skip to content

Commit

Permalink
fix(developer): handle missing files in kmc-model
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mcdurdin committed Oct 31, 2024
1 parent 55e3f73 commit 2a0e17c
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 4 deletions.
3 changes: 3 additions & 0 deletions developer/src/kmc-model/src/build-trie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
6 changes: 5 additions & 1 deletion developer/src/kmc-model/src/lexical-model-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
22 changes: 20 additions & 2 deletions developer/src/kmc-model/src/model-compiler-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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.`
);


};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const source: LexicalModelSource = {
format: 'trie-1.0',
sources: ['wordlist.tsv'],
};
export default source;
54 changes: 53 additions & 1 deletion developer/src/kmc-model/test/test-messages.ts
Original file line number Diff line number Diff line change
@@ -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
);
});

});

0 comments on commit 2a0e17c

Please sign in to comment.