From 764f4294c3e95faf63999e60916c71c2e74ff9aa Mon Sep 17 00:00:00 2001 From: jahorton Date: Wed, 7 Oct 2020 12:04:45 +0700 Subject: [PATCH 01/13] feat(common/core/web): input processor unit test setup --- .../web/input-processor/package-lock.json | 6 + common/core/web/input-processor/package.json | 4 +- .../src/text/inputProcessor.ts | 13 +- common/core/web/input-processor/test.sh | 67 ++++++++++ .../tests/cases/languageProcessor.js | 22 ++++ .../src/text/keyboardProcessor.ts | 2 +- common/core/web/keyboard-processor/test.sh | 1 + .../json/models/angle-punct-dummy.json | 5 + .../json/models/quote-punct-dummy.json | 5 + .../resources/models/angle-punct-dummy.js | 119 ++++++++++++++++++ .../resources/models/quote-punct-dummy.js | 119 ++++++++++++++++++ web/unit_tests/test.sh | 5 +- 12 files changed, 362 insertions(+), 6 deletions(-) create mode 100755 common/core/web/input-processor/test.sh create mode 100644 common/core/web/input-processor/tests/cases/languageProcessor.js create mode 100644 common/core/web/tests/resources/json/models/angle-punct-dummy.json create mode 100644 common/core/web/tests/resources/json/models/quote-punct-dummy.json create mode 100644 common/core/web/tests/resources/models/angle-punct-dummy.js create mode 100644 common/core/web/tests/resources/models/quote-punct-dummy.js diff --git a/common/core/web/input-processor/package-lock.json b/common/core/web/input-processor/package-lock.json index c65405e442..ee97548531 100644 --- a/common/core/web/input-processor/package-lock.json +++ b/common/core/web/input-processor/package-lock.json @@ -229,6 +229,12 @@ } } }, + "mocha-teamcity-reporter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mocha-teamcity-reporter/-/mocha-teamcity-reporter-3.0.0.tgz", + "integrity": "sha512-FyGgmtFfW2nDwEZU3mrjQShAAK/zhGivwY4HCsqoDoyeS8vV8HGdq1Dn2P+SFaIoCeXTQ0Z+5xVRyikYaKrW5w==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/common/core/web/input-processor/package.json b/common/core/web/input-processor/package.json index accb9cd943..6ce8f5f269 100644 --- a/common/core/web/input-processor/package.json +++ b/common/core/web/input-processor/package.json @@ -21,12 +21,14 @@ "@keymanapp/resources-gosh": "^14.0.155", "chai": "^4.2.0", "mocha": "^5.2.0", + "mocha-teamcity-reporter": "^3.0.0", "typescript": "^3.8.3" }, "scripts": { "lerna": "cd ../ && npm run lerna --", "tsc": "tsc", - "test": "gosh ./unit_tests/test.sh" + "test": "gosh ./test.sh", + "mocha": "mocha" }, "dependencies": { "@keymanapp/keyboard-processor": "^14.0.155", diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index dd48308060..3e0aa26cad 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -179,4 +179,15 @@ namespace com.keyman.text { this.languageProcessor.invalidateContext(); } } -} \ No newline at end of file +} + +(function () { + let ns = com.keyman.text; + + // Let the InputProcessor be available both in the browser and in Node. + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = ns.InputProcessor; + //@ts-ignore + ns.InputProcessor.com = com; // Export the root namespace so that all InputProcessor classes are accessible by unit tests. + } +}()); \ No newline at end of file diff --git a/common/core/web/input-processor/test.sh b/common/core/web/input-processor/test.sh new file mode 100755 index 0000000000..f961ee5cfe --- /dev/null +++ b/common/core/web/input-processor/test.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# We should work within the script's directory, not the one we were called in. +cd $(dirname "$BASH_SOURCE") + +# Include useful testing resource functions +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")" +. "$(dirname "$THIS_SCRIPT")/../../../../resources/build/build-utils.sh" +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# A simple utility script to facilitate unit-testing for the LM Layer. +# It's rigged to be callable by NPM to facilitate testing during development when in other folders. + +display_usage ( ) { + echo "test.sh [-skip-package-install] [-CI] [ -? | -h | -help]" + echo " -CI to perform continuous-integration friendly tests and reporting formatted for TeamCity" + echo " -? | -h | -help to display this help information" + echo " -skip-package-install to bypass refreshing dependencies. Useful when called by scripts that pre-fetch" + echo "" + exit 0 +} + +# Defaults +FLAGS= +CI_REPORTING=0 +FETCH_DEPS=true + +# Parse args +while [[ $# -gt 0 ]] ; do + key="$1" + case $key in + -h|-help|-?) + display_usage + exit + ;; + -CI) + CI_REPORTING=1 + ;; + -skip-package-install) + FETCH_DEPS=false + esac + shift # past argument +done + +if [ $FETCH_DEPS = true ]; then + verify_npm_setup +fi + +test-headless ( ) { + if (( CI_REPORTING )); then + FLAGS="$FLAGS --reporter mocha-teamcity-reporter" + fi + + npm run mocha -- --recursive $FLAGS ./tests/cases/ +} + +# First, run tests on the keyboard processor. +pushd $WORKING_DIRECTORY/node_modules/@keymanapp/keyboard-processor +./test.sh -skip-package-install || fail "Tests failed by dependencies; aborting integration tests." +popd + +# Now we run our local tests. +echo "${TERM_HEADING}Running Input Processor test suite${NORMAL}" +test-headless || fail "Input Processor tests failed!" diff --git a/common/core/web/input-processor/tests/cases/languageProcessor.js b/common/core/web/input-processor/tests/cases/languageProcessor.js new file mode 100644 index 0000000000..daacff1037 --- /dev/null +++ b/common/core/web/input-processor/tests/cases/languageProcessor.js @@ -0,0 +1,22 @@ +var assert = require('chai').assert; +var fs = require("fs"); +var vm = require("vm"); + +let InputProcessor = require('../../dist'); + +// Required initialization setup. +global.com = InputProcessor.com; // exports all keyboard-processor namespacing. +global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. + // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. + +// Initialize supplementary plane string extensions +String.kmwEnableSupplementaryPlane(false); + +// Test the KeyboardProcessor interface. +describe('LanguageProcessor', function() { + it('attempts to run unit tests', function() { + let languageProcessor = new com.keyman.text.prediction.LanguageProcessor(); + + assert.isOk(languageProcessor); + }); +}); \ No newline at end of file diff --git a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts index 9b65f38bde..66bd13dccd 100644 --- a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -680,7 +680,7 @@ namespace com.keyman.text { (function () { let ns = com.keyman.text; - // Let LMLayer be available both in the browser and in Node. + // Let the Keyboard Processor be available both in the browser and in Node. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = ns.KeyboardProcessor; //@ts-ignore diff --git a/common/core/web/keyboard-processor/test.sh b/common/core/web/keyboard-processor/test.sh index 13b651fd03..e6ce278430 100755 --- a/common/core/web/keyboard-processor/test.sh +++ b/common/core/web/keyboard-processor/test.sh @@ -63,5 +63,6 @@ pushd "$KEYMAN_ROOT/common/core/web/tools/recorder/src" popd # Run headless (browserless) tests. +echo "${TERM_HEADING}Running Keyboard Processor test suite${NORMAL}" test-headless || fail "Keyboard Processor tests failed!" diff --git a/common/core/web/tests/resources/json/models/angle-punct-dummy.json b/common/core/web/tests/resources/json/models/angle-punct-dummy.json new file mode 100644 index 0000000000..34b3beeb58 --- /dev/null +++ b/common/core/web/tests/resources/json/models/angle-punct-dummy.json @@ -0,0 +1,5 @@ +{ + "id": "angle-punct-dummy", + "languages": ["en"], + "filename":"resources/models/angle-punct-dummy.js" +} \ No newline at end of file diff --git a/common/core/web/tests/resources/json/models/quote-punct-dummy.json b/common/core/web/tests/resources/json/models/quote-punct-dummy.json new file mode 100644 index 0000000000..c6aa45ff09 --- /dev/null +++ b/common/core/web/tests/resources/json/models/quote-punct-dummy.json @@ -0,0 +1,5 @@ +{ + "id": "quote-punct-dummy", + "languages": ["en"], + "filename":"resources/models/quote-punct-dummy.js" +} \ No newline at end of file diff --git a/common/core/web/tests/resources/models/angle-punct-dummy.js b/common/core/web/tests/resources/models/angle-punct-dummy.js new file mode 100644 index 0000000000..f2e1f6c65f --- /dev/null +++ b/common/core/web/tests/resources/models/angle-punct-dummy.js @@ -0,0 +1,119 @@ +/** + * While handwritten, this class is designed to mirror the potential results of TypeScript compilation of + * model TS source files. + */ +(function(){ + var Model = /** @class */ (function() { + function Model() { // implements Model + } + + Model.punctuation = { + // The key part that this model is intended to test. + quotesForKeepSuggestion: { open: '«', close: '»'}, + // Important! Set this, or else the model compositor will + // insert something for us! + insertAfterWord: "", + }; + + // A direct import/copy from i_got_distracted_by_hazel.json. + Model.futureSuggestions = [ + [ + { + "transform": { + "insert": "I ", + "deleteLeft": 0 + }, + "displayAs": "I" + }, + { + "transform": { + "insert": "I'm ", + "deleteLeft": 0 + }, + "displayAs": "I'm" + }, + { + "transform": { + "insert": "Oh ", + "deleteLeft": 0 + }, + "displayAs": "Oh" + } + ], + [ + { + "transform": { + "insert": "love ", + "deleteLeft": 0 + }, + "displayAs": "love" + }, + { + "transform": { + "insert": "am ", + "deleteLeft": 0 + }, + "displayAs": "am" + }, + { + "transform": { + "insert": "got ", + "deleteLeft": 0 + }, + "displayAs": "got" + } + ], + [ + { + "transform": { + "insert": "distracted ", + "deleteLeft": 0 + }, + "displayAs": "distracted by" + }, + { + "transform": { + "insert": "distracted ", + "deleteLeft": 0 + }, + "displayAs": "distracted" + }, + { + "transform": { + "insert": "a ", + "deleteLeft": 0 + }, + "displayAs": "a" + } + ], + [ + { + "transform": { + "insert": "Hazel ", + "deleteLeft": 0 + }, + "displayAs": "Hazel" + }, + { + "transform": { + "insert": "the ", + "deleteLeft": 0 + }, + "displayAs": "the" + }, + { + "transform": { + "insert": "a ", + "deleteLeft": 0 + }, + "displayAs": "a" + } + ] + ]; + + return Model; + }()); + + // It's a 'dummy' model, so there's no need for extra methods and such within the Model's class definition. + LMLayerWorker.loadModel(new models.DummyModel({futureSuggestions: Model.futureSuggestions, punctuation: Model.punctuation})); +})(); \ No newline at end of file diff --git a/common/core/web/tests/resources/models/quote-punct-dummy.js b/common/core/web/tests/resources/models/quote-punct-dummy.js new file mode 100644 index 0000000000..ec0d83e439 --- /dev/null +++ b/common/core/web/tests/resources/models/quote-punct-dummy.js @@ -0,0 +1,119 @@ +/** + * While handwritten, this class is designed to mirror the potential results of TypeScript compilation of + * model TS source files. + */ +(function(){ + var Model = /** @class */ (function() { + function Model() { // implements Model + } + + Model.punctuation = { + // The key part that this model is intended to test. + quotesForKeepSuggestion: { open: '“', close: '”'}, + // Important! Set this, or else the model compositor will + // insert something for us! + insertAfterWord: "", + }; + + // A direct import/copy from i_got_distracted_by_hazel.json. + Model.futureSuggestions = [ + [ + { + "transform": { + "insert": "I ", + "deleteLeft": 0 + }, + "displayAs": "I" + }, + { + "transform": { + "insert": "I'm ", + "deleteLeft": 0 + }, + "displayAs": "I'm" + }, + { + "transform": { + "insert": "Oh ", + "deleteLeft": 0 + }, + "displayAs": "Oh" + } + ], + [ + { + "transform": { + "insert": "love ", + "deleteLeft": 0 + }, + "displayAs": "love" + }, + { + "transform": { + "insert": "am ", + "deleteLeft": 0 + }, + "displayAs": "am" + }, + { + "transform": { + "insert": "got ", + "deleteLeft": 0 + }, + "displayAs": "got" + } + ], + [ + { + "transform": { + "insert": "distracted ", + "deleteLeft": 0 + }, + "displayAs": "distracted by" + }, + { + "transform": { + "insert": "distracted ", + "deleteLeft": 0 + }, + "displayAs": "distracted" + }, + { + "transform": { + "insert": "a ", + "deleteLeft": 0 + }, + "displayAs": "a" + } + ], + [ + { + "transform": { + "insert": "Hazel ", + "deleteLeft": 0 + }, + "displayAs": "Hazel" + }, + { + "transform": { + "insert": "the ", + "deleteLeft": 0 + }, + "displayAs": "the" + }, + { + "transform": { + "insert": "a ", + "deleteLeft": 0 + }, + "displayAs": "a" + } + ] + ]; + + return Model; + }()); + + // It's a 'dummy' model, so there's no need for extra methods and such within the Model's class definition. + LMLayerWorker.loadModel(new models.DummyModel({futureSuggestions: Model.futureSuggestions, punctuation: Model.punctuation})); +})(); \ No newline at end of file diff --git a/web/unit_tests/test.sh b/web/unit_tests/test.sh index bb9934b93a..8e692269a6 100755 --- a/web/unit_tests/test.sh +++ b/web/unit_tests/test.sh @@ -125,9 +125,8 @@ cd ../tools/recorder # Run our headless tests first. # Since we're using `lerna`, this actually puts us within the projects when run in-repo! -# First: Keyboard Processor tests. -echo "${TERM_HEADING}Running Keyboard Processor test suite${NORMAL}" -pushd $WORKING_DIRECTORY/node_modules/@keymanapp/keyboard-processor +# First: Web-core tests. +pushd $WORKING_DIRECTORY/node_modules/@keymanapp/input-processor ./test.sh $HEADLESS_FLAGS || fail "Tests failed by dependencies; aborting integration tests." # Once done, now we run the integrated (KeymanWeb) tests. popd From 2e9cfabd5247268344974a079cb66038009990e4 Mon Sep 17 00:00:00 2001 From: jahorton Date: Thu, 5 Nov 2020 15:27:16 +0700 Subject: [PATCH 02/13] feat(common/core/web): better skip-deps flag handling --- common/core/web/input-processor/test.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/common/core/web/input-processor/test.sh b/common/core/web/input-processor/test.sh index f961ee5cfe..9595b8d8fa 100755 --- a/common/core/web/input-processor/test.sh +++ b/common/core/web/input-processor/test.sh @@ -15,10 +15,11 @@ THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BA # It's rigged to be callable by NPM to facilitate testing during development when in other folders. display_usage ( ) { - echo "test.sh [-skip-package-install] [-CI] [ -? | -h | -help]" + echo "test.sh [-skip-package-install|-S] [-CI] [ -? | -h | -help]" echo " -CI to perform continuous-integration friendly tests and reporting formatted for TeamCity" echo " -? | -h | -help to display this help information" echo " -skip-package-install to bypass refreshing dependencies. Useful when called by scripts that pre-fetch" + echo " (or -S)" echo "" exit 0 } @@ -32,15 +33,16 @@ FETCH_DEPS=true while [[ $# -gt 0 ]] ; do key="$1" case $key in - -h|-help|-?) + -h|-help|-\?) display_usage exit ;; -CI) CI_REPORTING=1 ;; - -skip-package-install) + -skip-package-install|-S) FETCH_DEPS=false + ;; esac shift # past argument done @@ -57,10 +59,12 @@ test-headless ( ) { npm run mocha -- --recursive $FLAGS ./tests/cases/ } -# First, run tests on the keyboard processor. -pushd $WORKING_DIRECTORY/node_modules/@keymanapp/keyboard-processor -./test.sh -skip-package-install || fail "Tests failed by dependencies; aborting integration tests." -popd +if [ $FETCH_DEPS = true ]; then + # First, run tests on the keyboard processor. + pushd $WORKING_DIRECTORY/node_modules/@keymanapp/keyboard-processor + ./test.sh -skip-package-install || fail "Tests failed by dependencies; aborting integration tests." + popd +fi # Now we run our local tests. echo "${TERM_HEADING}Running Input Processor test suite${NORMAL}" From 016b0587f26a6fd9b2d8d7115e0a1608caf276a0 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Nov 2020 09:57:46 +0700 Subject: [PATCH 03/13] feat(common/core/web): inputProcessor init tests --- .../src/text/prediction/languageProcessor.ts | 2 +- .../tests/cases/inputProcessor.js | 45 +++++++++++++++++++ .../tests/cases/languageProcessor.js | 17 +++++-- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 common/core/web/input-processor/tests/cases/inputProcessor.js diff --git a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts index e3f781f903..20d0fc557e 100644 --- a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts @@ -313,7 +313,7 @@ namespace com.keyman.text.prediction { this._mayPredict = false; return false; } - return this.activeModel && this._mayPredict; + return (this.activeModel || false) && this._mayPredict; } public canEnable(): boolean { diff --git a/common/core/web/input-processor/tests/cases/inputProcessor.js b/common/core/web/input-processor/tests/cases/inputProcessor.js new file mode 100644 index 0000000000..d054d4a620 --- /dev/null +++ b/common/core/web/input-processor/tests/cases/inputProcessor.js @@ -0,0 +1,45 @@ +var assert = require('chai').assert; +var fs = require("fs"); +var vm = require("vm"); + +let InputProcessor = require('../../dist'); + +// Required initialization setup. +global.com = InputProcessor.com; // exports all keyboard-processor namespacing. +global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. + // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. + +// Initialize supplementary plane string extensions +String.kmwEnableSupplementaryPlane(false); + +// Test the KeyboardProcessor interface. +describe('InputProcessor', function() { + describe('[[constructor]]', function () { + it('should initialize without errors', function () { + let core = new InputProcessor(); + assert.isNotNull(core); + }); + + it('has expected default values after initialization', function () { + let core = new InputProcessor(); + + assert.isOk(core.keyboardProcessor); + assert.isOk(core.languageProcessor); + assert.isOk(core.keyboardInterface); + assert.isUndefined(core.activeKeyboard); // No keyboard should be loaded yet. + assert.isUndefined(core.activeModel); // Same for the model. + + // These checks are lifted from the keyboard-processor init checks found in + // common/core/web/keyboard-processor/tests/cases/basic-init.js. + assert.equal('us', core.keyboardProcessor.baseLayout, 'KeyboardProcessor has unexpected base layout') + assert.isNotNull(global.KeymanWeb, 'KeymanWeb global was not automatically installed'); + assert.equal('default', core.keyboardProcessor.layerId, 'Default layer is not set to "default"'); + assert.isUndefined(core.keyboardProcessor.activeKeyboard, 'Initialized with already-active keyboard'); + + // Lifted from languageProcessor.js - the core should not be changing these with its init. + assert.isUndefined(core.languageProcessor.activeModel); + assert.isFalse(core.languageProcessor.isActive); + assert.isTrue(core.languageProcessor.mayPredict); + }); + }); +}); \ No newline at end of file diff --git a/common/core/web/input-processor/tests/cases/languageProcessor.js b/common/core/web/input-processor/tests/cases/languageProcessor.js index daacff1037..60bcbe7fc4 100644 --- a/common/core/web/input-processor/tests/cases/languageProcessor.js +++ b/common/core/web/input-processor/tests/cases/languageProcessor.js @@ -14,9 +14,20 @@ String.kmwEnableSupplementaryPlane(false); // Test the KeyboardProcessor interface. describe('LanguageProcessor', function() { - it('attempts to run unit tests', function() { - let languageProcessor = new com.keyman.text.prediction.LanguageProcessor(); + describe('[[constructor]]', function () { + it('should initialize without errors', function () { + let lp = new com.keyman.text.prediction.LanguageProcessor(); + assert.isNotNull(lp); + }); - assert.isOk(languageProcessor); + it('has expected default values after initialization', function () { + let languageProcessor = new com.keyman.text.prediction.LanguageProcessor(); + + // These checks are lifted from the keyboard-processor init checks found in + // common/core/web/keyboard-processor/tests/cases/basic-init.js. + assert.isUndefined(languageProcessor.activeModel); + assert.isFalse(languageProcessor.isActive); + assert.isTrue(languageProcessor.mayPredict); + }); }); }); \ No newline at end of file From 70d569a5896b96fdc913b15b06eae2fa8769b2d1 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Nov 2020 14:23:05 +0700 Subject: [PATCH 04/13] feat(common/models): LoadMessage refactor, model load from raw string (for tests) --- common/predictive-text/index.ts | 14 +++++- .../unit_tests/headless/top-level-lmlayer.js | 6 ++- .../headless/worker-dummy-integration.js | 34 ++++++++++++- .../headless/worker-initialization.js | 48 +++++++++++++++++-- .../unit_tests/headless/worker-predict.js | 5 +- .../unit_tests/in_browser/cases/worker.js | 5 +- common/predictive-text/worker/index.ts | 36 ++++++++++++-- .../worker/worker-interfaces.ts | 23 ++++++++- 8 files changed, 155 insertions(+), 16 deletions(-) diff --git a/common/predictive-text/index.ts b/common/predictive-text/index.ts index f4ec5e1567..6377da492f 100644 --- a/common/predictive-text/index.ts +++ b/common/predictive-text/index.ts @@ -90,15 +90,25 @@ namespace com.keyman.text.prediction { /** * Initializes the LMLayer worker with a path to the desired model file. */ - loadModel(modelFilePath: string): Promise { + loadModel(modelSource: string, loadType: 'file' | 'raw' = 'file'): Promise { return new Promise((resolve, _reject) => { // Sets up so the promise is resolved in the onMessage() callback, when it receives // the 'ready' message. this._declareLMLayerReady = resolve; + let modelSourceSpec: any = { + type: loadType + }; + + if(loadType == 'file') { + modelSourceSpec.file = modelSource; + } else { + modelSourceSpec.code = modelSource; + } + this._worker.postMessage({ message: 'load', - model: modelFilePath + source: modelSourceSpec }); }); } diff --git a/common/predictive-text/unit_tests/headless/top-level-lmlayer.js b/common/predictive-text/unit_tests/headless/top-level-lmlayer.js index f340044a19..a4645e7e27 100644 --- a/common/predictive-text/unit_tests/headless/top-level-lmlayer.js +++ b/common/predictive-text/unit_tests/headless/top-level-lmlayer.js @@ -49,7 +49,11 @@ describe('LMLayer', function() { } assert.propertyVal(data, 'message', 'load'); - assert.isString(data.model); + assert.property(data, 'source'); + assert.propertyVal(data.source, 'type', 'file'); + assert.notProperty(data.source, 'code'); + assert.property(data.source, 'file'); + assert.isString(data.source.file); callAsynchronously(() => fakeWorker.onmessage({ data: { diff --git a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js index bea5941d0e..9e4b2e495c 100644 --- a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js +++ b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js @@ -1,4 +1,5 @@ var assert = require('chai').assert; +var fs = require('fs'); let LMLayer = require('../../build/headless'); /* @@ -11,7 +12,7 @@ let LMLayer = require('../../build/headless'); */ describe('LMLayer using dummy model', function () { describe('Prediction', function () { - it('will predict future suggestions', function () { + it('will predict future suggestions (loaded from file)', function () { var lmLayer = new LMLayer(capabilities()); // We're testing many as asynchronous messages in a row. @@ -39,6 +40,37 @@ describe('LMLayer using dummy model', function () { return Promise.resolve(); }); }); + + it('will predict future suggestions (loaded from raw source)', function () { + var lmLayer = new LMLayer(capabilities()); + + // We're running headlessly, so the path can be relative to the npm root directory. + let modelCode = fs.readFileSync("./unit_tests/in_browser/resources/models/simple-dummy.js").toString(); + + // We're testing many as asynchronous messages in a row. + // this would be cleaner using async/await syntax. + // Not done yet, as this test case is a slightly-edited copy of the in-browser version. + return lmLayer.loadModel( + modelCode, 'raw' + ).then(function (actualConfiguration) { + return Promise.resolve(); + }).then(function () { + return lmLayer.predict(zeroTransform(), emptyContext()); + }).then(function (suggestions) { + assert.deepEqual(suggestions, iGotDistractedByHazel()[0]); + return lmLayer.predict(zeroTransform(), emptyContext()); + }).then(function (suggestions) { + assert.deepEqual(suggestions, iGotDistractedByHazel()[1]); + return lmLayer.predict(zeroTransform(), emptyContext()); + }).then(function (suggestions) { + assert.deepEqual(suggestions, iGotDistractedByHazel()[2]); + return lmLayer.predict(zeroTransform(), emptyContext()); + }).then(function (suggestions) { + assert.deepEqual(suggestions, iGotDistractedByHazel()[3]); + lmLayer.shutdown(); + return Promise.resolve(); + }); + }); }); describe('Wordbreaking', function () { diff --git a/common/predictive-text/unit_tests/headless/worker-initialization.js b/common/predictive-text/unit_tests/headless/worker-initialization.js index d5d9b55b1d..bb436ffccc 100644 --- a/common/predictive-text/unit_tests/headless/worker-initialization.js +++ b/common/predictive-text/unit_tests/headless/worker-initialization.js @@ -1,5 +1,6 @@ var assert = require('chai').assert; var sinon = require('sinon'); +var fs = require('fs'); let LMLayerWorker = require('../../build/intermediate'); @@ -25,7 +26,10 @@ describe('LMLayerWorker', function() { // Sending it the `load` message should notify us that it's loaded! worker.onMessage(createMessageEventWithData({ message: 'load', - model: "./unit_tests/in_browser/resources/models/simple-dummy.js" + source: { + type: 'file', + file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + } })); assert(fakePostMessage.calledOnce); }); @@ -69,7 +73,10 @@ describe('LMLayerWorker', function() { // Send a message; we should get something back. worker.onMessage(createMessageEventWithData({ message: 'load', - model: "./unit_tests/in_browser/resources/models/simple-dummy.js" + source: { + type: 'file', + file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + } })); // It called the postMessage() in its global scope. @@ -128,7 +135,7 @@ describe('LMLayerWorker', function() { }, /invalid message/i); }); - it('should send back a "ready" message', function () { + it('should send back a "ready" message when given a file', function () { var fakePostMessage = sinon.fake(); var context = { postMessage: fakePostMessage @@ -140,7 +147,10 @@ describe('LMLayerWorker', function() { worker.onMessage(createMessageEventWithData({ message: 'load', - model: "./unit_tests/in_browser/resources/models/simple-dummy.js" + source: { + type: 'file', + file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + } })); assert(fakePostMessage.calledOnceWith(sinon.match({ @@ -148,6 +158,31 @@ describe('LMLayerWorker', function() { }))); }); + it('should send back a "ready" message when given raw code', function () { + var fakePostMessage = sinon.fake(); + var context = { + postMessage: fakePostMessage + }; + context.importScripts = importScriptsWith(context); + + var worker = LMLayerWorker.install(context); + configWorker(worker); + + let modelCode = fs.readFileSync("./unit_tests/in_browser/resources/models/simple-dummy.js").toString(); + + worker.onMessage(createMessageEventWithData({ + message: 'load', + source: { + type: 'raw', + code: modelCode + } + })); + + assert(fakePostMessage.calledOnceWith(sinon.match({ + message: 'ready' + })), 'mocked callback was not called'); + }); + it('should send back configuration', function () { var fakePostMessage = sinon.fake(); var context = { @@ -162,7 +197,10 @@ describe('LMLayerWorker', function() { var maxCodeUnits = 64; worker.onMessage(createMessageEventWithData({ message: 'load', - model: "./unit_tests/in_browser/resources/models/simple-dummy.js" + source: { + type: 'file', + file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + } })); sinon.assert.calledWithMatch(fakePostMessage, { diff --git a/common/predictive-text/unit_tests/headless/worker-predict.js b/common/predictive-text/unit_tests/headless/worker-predict.js index 8f935c7804..7562046049 100644 --- a/common/predictive-text/unit_tests/headless/worker-predict.js +++ b/common/predictive-text/unit_tests/headless/worker-predict.js @@ -27,7 +27,10 @@ describe('LMLayerWorker', function () { worker.onMessage(createMessageEventWithData({ message: 'load', - model: "./unit_tests/in_browser/resources/models/simple-dummy.js" + source: { + type: 'file', + file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + } })); sinon.assert.calledWithMatch(fakePostMessage.lastCall, { message: 'ready', diff --git a/common/predictive-text/unit_tests/in_browser/cases/worker.js b/common/predictive-text/unit_tests/in_browser/cases/worker.js index 253155db37..795adc3d06 100644 --- a/common/predictive-text/unit_tests/in_browser/cases/worker.js +++ b/common/predictive-text/unit_tests/in_browser/cases/worker.js @@ -30,7 +30,10 @@ describe('LMLayerWorker', function () { worker.postMessage({ message: 'load', // Since the worker's based in a blob, it's not on the 'same domain'. We need to absolute-path the model file. - model: document.location.protocol + '//' + document.location.host + "/resources/models/simple-dummy.js" + source: { + type: 'file', + file: document.location.protocol + '//' + document.location.host + "/resources/models/simple-dummy.js" + } }); }); }); diff --git a/common/predictive-text/worker/index.ts b/common/predictive-text/worker/index.ts index ffa1df429b..2ffc2c3198 100644 --- a/common/predictive-text/worker/index.ts +++ b/common/predictive-text/worker/index.ts @@ -80,11 +80,13 @@ class LMLayerWorker { */ private _importScripts: ImportScripts; + private self: any; + private _platformCapabilities: Capabilities; private _hostURL: string; - private _currentModelSource: string; + private _currentModelSource: ModelSourceSpec; constructor(options = { importScripts: null, @@ -130,14 +132,23 @@ class LMLayerWorker { let im = event.data as IncomingMessage; if(im.message == 'load') { let data = im as LoadMessage; - if(data.model == this._currentModelSource) { + let duplicated = false; + if(this._currentModelSource && data.source.type == this._currentModelSource.type) { + if(data.source.type == 'file' && data.source.file == (this._currentModelSource as ModelFile).file) { + duplicated = true; + } else if(data.source.type == 'raw' && data.source.code == (this._currentModelSource as ModelEval).code) { + duplicated = true; + } + } + + if(duplicated) { // Some JS implementations don't allow web workers access to the console. if(typeof console !== 'undefined') { console.warn("Duplicate model load message detected - squashing!"); } return; } else { - this._currentModelSource = data.model; + this._currentModelSource = data.source; } } else if(im.message == 'unload') { this._currentModelSource = null; @@ -251,6 +262,7 @@ class LMLayerWorker { * description and capabilities. */ private transitionToLoadingState() { + let _this = this; this.state = { name: 'modelless', handleMessage: (payload) => { @@ -260,7 +272,22 @@ class LMLayerWorker { } // TODO: validate configuration? - this.loadModelFile(payload.model); + if(payload.source.type == 'file') { + _this.loadModelFile(payload.source.file); + } else { + let code = payload.source.code; + + // let scope = { + // LMLayerWorker: LMLayerWorker, + // models: models, + // wordBreakers: wordBreakers, + // correction: correction + // }; + let evalInContext = function(LMLayerWorker, models, correction, wordBreakers) { + eval(code); + } + evalInContext(_this, models, correction, wordBreakers); + } } }; } @@ -328,6 +355,7 @@ class LMLayerWorker { static install(scope: DedicatedWorkerGlobalScope): LMLayerWorker { let worker = new LMLayerWorker({ postMessage: scope.postMessage, importScripts: scope.importScripts.bind(scope) }); scope.onmessage = worker.onMessage.bind(worker); + worker.self = scope; // Ensures that the worker instance is accessible for loaded model scripts. // Assists unit-testing. diff --git a/common/predictive-text/worker/worker-interfaces.ts b/common/predictive-text/worker/worker-interfaces.ts index 32e5c60515..2ffd338850 100644 --- a/common/predictive-text/worker/worker-interfaces.ts +++ b/common/predictive-text/worker/worker-interfaces.ts @@ -54,6 +54,27 @@ interface ConfigMessage { capabilities: Capabilities; } +interface ModelFile { + type: 'file'; + + /** + * The model should be loaded from a file via importScripts. + */ + file: string; +} + +interface ModelEval { + type: 'raw'; + + /** + * Rather than loading a file, this specifies the contents that would normally be within + * a .model.ts file. Useful for dynamically-compiled models that occur when testing. + */ + code: string; +} + +type ModelSourceSpec = ModelFile | ModelEval; + /** * The structure of an initialization message. It should include the model (either in * source code or parameter form), as well as the keyboard's capabilities. @@ -64,7 +85,7 @@ interface LoadMessage { /** * The model's compiled JS file. */ - model: string; + source: ModelSourceSpec; } /** From 69277feec18e36663bd7ef2d7543d390165848f8 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Nov 2020 16:10:18 +0700 Subject: [PATCH 05/13] feat(common/core/web): dynamically compiled model in auto test --- common/core/web/input-processor/package.json | 1 + .../src/text/prediction/languageProcessor.ts | 13 ++- .../tests/cases/languageProcessor.js | 79 ++++++++++++++++++- common/predictive-text/worker/index.ts | 12 ++- 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/common/core/web/input-processor/package.json b/common/core/web/input-processor/package.json index 6ce8f5f269..1ab168fbba 100644 --- a/common/core/web/input-processor/package.json +++ b/common/core/web/input-processor/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@keymanapp/keyboard-processor": "^14.0.155", + "@keymanapp/lexical-model-compiler": "^14.0.155", "@keymanapp/lexical-model-layer": "^14.0.155", "@keymanapp/models-types": "^14.0.155", "@keymanapp/web-environment": "^14.0.155", diff --git a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts index 20d0fc557e..53b5e0c4b2 100644 --- a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts @@ -17,9 +17,15 @@ namespace com.keyman.text.prediction { languages: string[]; /** - * The path/URL to the file that defines the model. + * The path/URL to the file that defines the model. If both `path` and `raw` are specified, + * `path` takes precedence. */ path: string; + + /** + * The raw JS script defining the model. Only used if `path` is not specified. + */ + code: string; } /** @@ -127,11 +133,12 @@ namespace com.keyman.text.prediction { throw new Error("Null reference not allowed."); } - let file = model.path; + let specType: 'file'|'raw' = model.path ? 'file' : 'raw'; + let source = specType == 'file' ? model.path : model.code; let lp = this; // We should wait until the model is successfully loaded before setting our state values. - return this.lmEngine.loadModel(file).then(function(config: Configuration) { + return this.lmEngine.loadModel(source, specType).then(function(config: Configuration) { lp.currentModel = model; lp.configuration = config; diff --git a/common/core/web/input-processor/tests/cases/languageProcessor.js b/common/core/web/input-processor/tests/cases/languageProcessor.js index 60bcbe7fc4..0fdfa5aa66 100644 --- a/common/core/web/input-processor/tests/cases/languageProcessor.js +++ b/common/core/web/input-processor/tests/cases/languageProcessor.js @@ -2,6 +2,13 @@ var assert = require('chai').assert; var fs = require("fs"); var vm = require("vm"); +/* + * Unit tests for the Dummy prediction model. + */ + +var LexicalModelCompiler = require('@keymanapp/lexical-model-compiler/dist/lexical-model-compiler/lexical-model-compiler').default; +var path = require('path'); + let InputProcessor = require('../../dist'); // Required initialization setup. @@ -10,24 +17,90 @@ global.keyman = {}; // So that keyboard-based checks against the global `keyman` // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. // Initialize supplementary plane string extensions -String.kmwEnableSupplementaryPlane(false); +String.kmwEnableSupplementaryPlane(false); + +let LanguageProcessor = com.keyman.text.prediction.LanguageProcessor; // Test the KeyboardProcessor interface. describe('LanguageProcessor', function() { describe('[[constructor]]', function () { it('should initialize without errors', function () { - let lp = new com.keyman.text.prediction.LanguageProcessor(); + let lp = new LanguageProcessor(); assert.isNotNull(lp); }); it('has expected default values after initialization', function () { - let languageProcessor = new com.keyman.text.prediction.LanguageProcessor(); + let languageProcessor = new LanguageProcessor(); // These checks are lifted from the keyboard-processor init checks found in // common/core/web/keyboard-processor/tests/cases/basic-init.js. + assert.isUndefined(languageProcessor.lmEngine); assert.isUndefined(languageProcessor.activeModel); assert.isFalse(languageProcessor.isActive); assert.isTrue(languageProcessor.mayPredict); + + // Some aspects of initialization must wait until after construction and overall + // load of the core. See /web/source/kmwbase.ts, in the final IIFE. + languageProcessor.init(); + assert.isOk(languageProcessor.lmEngine); + }); + }); + + describe('.predict', function() { + let compiler = new LexicalModelCompiler(); + const MODEL_ID = 'example.qaa.trivial'; + const PATH = path.join(__dirname, '../../node_modules/@keymanapp/lexical-model-compiler/tests/fixtures', MODEL_ID); + + describe('using angle brackets for quotes', function() { + let modelCode = compiler.generateLexicalModelCode(MODEL_ID, { + format: 'trie-1.0', + sources: ['wordlist.tsv'], + punctuation: { + quotesForKeepSuggestion: { open: `«`, close: `»`}, + insertAfterWord: " " , // OGHAM SPACE MARK + } + }, PATH); + + let modelSpec = { + id: MODEL_ID, + languages: ['en'], + code: modelCode + }; + + it("successfully loads the model", function(done) { + let languageProcessor = new LanguageProcessor(); + languageProcessor.init(); + + languageProcessor.loadModel(modelSpec).then(function() { + assert.isOk(languageProcessor.activeModel); // is only set after a successful load. + done(); + }, function(reason) { + assert.fail("Model did not load correctly: " + reason); + }); + }); + + it("generates the expected prediction set", function(done) { + let languageProcessor = new LanguageProcessor(); + languageProcessor.init(); + + let contextSource = new com.keyman.text.Mock("li", 2); + let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); + + languageProcessor.loadModel(modelSpec).then(function() { + languageProcessor.predict(transcription).then(function(suggestions) { + assert.isOk(suggestions); + assert.equal(suggestions[0].displayAs, '«li»'); + // Is not actually inserting what is expected at the moment. + //assert.equal(suggestions[0].transform.insert, ' '); + assert.equal(suggestions[1].displayAs, 'like'); + assert.equal(suggestions[1].transform.insert, 'like '); + done(); + }).catch(done); + }).catch(function() { + assert.fail("Unexpected model load failure"); + done(); + }); + }); }); }); }); \ No newline at end of file diff --git a/common/predictive-text/worker/index.ts b/common/predictive-text/worker/index.ts index 2ffc2c3198..f6abdce351 100644 --- a/common/predictive-text/worker/index.ts +++ b/common/predictive-text/worker/index.ts @@ -275,14 +275,12 @@ class LMLayerWorker { if(payload.source.type == 'file') { _this.loadModelFile(payload.source.file); } else { + // Creates a closure capturing all top-level names that the model must be able to reference. + // `eval` runs by scope rules; our virtualized worker needs a special scope for this to work. + // + // Reference: https://stackoverflow.com/a/40108685 + // Note that we don't need `this`, but we do need the namespaces seen below. let code = payload.source.code; - - // let scope = { - // LMLayerWorker: LMLayerWorker, - // models: models, - // wordBreakers: wordBreakers, - // correction: correction - // }; let evalInContext = function(LMLayerWorker, models, correction, wordBreakers) { eval(code); } From 5b9dec7936baba1ccc2da1ba1277959992bef276 Mon Sep 17 00:00:00 2001 From: jahorton Date: Fri, 6 Nov 2020 16:11:22 +0700 Subject: [PATCH 06/13] change(common/core/web): removes unused new resources --- .../resources/models/angle-punct-dummy.js | 119 ------------------ .../resources/models/quote-punct-dummy.js | 119 ------------------ 2 files changed, 238 deletions(-) delete mode 100644 common/core/web/tests/resources/models/angle-punct-dummy.js delete mode 100644 common/core/web/tests/resources/models/quote-punct-dummy.js diff --git a/common/core/web/tests/resources/models/angle-punct-dummy.js b/common/core/web/tests/resources/models/angle-punct-dummy.js deleted file mode 100644 index f2e1f6c65f..0000000000 --- a/common/core/web/tests/resources/models/angle-punct-dummy.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * While handwritten, this class is designed to mirror the potential results of TypeScript compilation of - * model TS source files. - */ -(function(){ - var Model = /** @class */ (function() { - function Model() { // implements Model - } - - Model.punctuation = { - // The key part that this model is intended to test. - quotesForKeepSuggestion: { open: '«', close: '»'}, - // Important! Set this, or else the model compositor will - // insert something for us! - insertAfterWord: "", - }; - - // A direct import/copy from i_got_distracted_by_hazel.json. - Model.futureSuggestions = [ - [ - { - "transform": { - "insert": "I ", - "deleteLeft": 0 - }, - "displayAs": "I" - }, - { - "transform": { - "insert": "I'm ", - "deleteLeft": 0 - }, - "displayAs": "I'm" - }, - { - "transform": { - "insert": "Oh ", - "deleteLeft": 0 - }, - "displayAs": "Oh" - } - ], - [ - { - "transform": { - "insert": "love ", - "deleteLeft": 0 - }, - "displayAs": "love" - }, - { - "transform": { - "insert": "am ", - "deleteLeft": 0 - }, - "displayAs": "am" - }, - { - "transform": { - "insert": "got ", - "deleteLeft": 0 - }, - "displayAs": "got" - } - ], - [ - { - "transform": { - "insert": "distracted ", - "deleteLeft": 0 - }, - "displayAs": "distracted by" - }, - { - "transform": { - "insert": "distracted ", - "deleteLeft": 0 - }, - "displayAs": "distracted" - }, - { - "transform": { - "insert": "a ", - "deleteLeft": 0 - }, - "displayAs": "a" - } - ], - [ - { - "transform": { - "insert": "Hazel ", - "deleteLeft": 0 - }, - "displayAs": "Hazel" - }, - { - "transform": { - "insert": "the ", - "deleteLeft": 0 - }, - "displayAs": "the" - }, - { - "transform": { - "insert": "a ", - "deleteLeft": 0 - }, - "displayAs": "a" - } - ] - ]; - - return Model; - }()); - - // It's a 'dummy' model, so there's no need for extra methods and such within the Model's class definition. - LMLayerWorker.loadModel(new models.DummyModel({futureSuggestions: Model.futureSuggestions, punctuation: Model.punctuation})); -})(); \ No newline at end of file diff --git a/common/core/web/tests/resources/models/quote-punct-dummy.js b/common/core/web/tests/resources/models/quote-punct-dummy.js deleted file mode 100644 index ec0d83e439..0000000000 --- a/common/core/web/tests/resources/models/quote-punct-dummy.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * While handwritten, this class is designed to mirror the potential results of TypeScript compilation of - * model TS source files. - */ -(function(){ - var Model = /** @class */ (function() { - function Model() { // implements Model - } - - Model.punctuation = { - // The key part that this model is intended to test. - quotesForKeepSuggestion: { open: '“', close: '”'}, - // Important! Set this, or else the model compositor will - // insert something for us! - insertAfterWord: "", - }; - - // A direct import/copy from i_got_distracted_by_hazel.json. - Model.futureSuggestions = [ - [ - { - "transform": { - "insert": "I ", - "deleteLeft": 0 - }, - "displayAs": "I" - }, - { - "transform": { - "insert": "I'm ", - "deleteLeft": 0 - }, - "displayAs": "I'm" - }, - { - "transform": { - "insert": "Oh ", - "deleteLeft": 0 - }, - "displayAs": "Oh" - } - ], - [ - { - "transform": { - "insert": "love ", - "deleteLeft": 0 - }, - "displayAs": "love" - }, - { - "transform": { - "insert": "am ", - "deleteLeft": 0 - }, - "displayAs": "am" - }, - { - "transform": { - "insert": "got ", - "deleteLeft": 0 - }, - "displayAs": "got" - } - ], - [ - { - "transform": { - "insert": "distracted ", - "deleteLeft": 0 - }, - "displayAs": "distracted by" - }, - { - "transform": { - "insert": "distracted ", - "deleteLeft": 0 - }, - "displayAs": "distracted" - }, - { - "transform": { - "insert": "a ", - "deleteLeft": 0 - }, - "displayAs": "a" - } - ], - [ - { - "transform": { - "insert": "Hazel ", - "deleteLeft": 0 - }, - "displayAs": "Hazel" - }, - { - "transform": { - "insert": "the ", - "deleteLeft": 0 - }, - "displayAs": "the" - }, - { - "transform": { - "insert": "a ", - "deleteLeft": 0 - }, - "displayAs": "a" - } - ] - ]; - - return Model; - }()); - - // It's a 'dummy' model, so there's no need for extra methods and such within the Model's class definition. - LMLayerWorker.loadModel(new models.DummyModel({futureSuggestions: Model.futureSuggestions, punctuation: Model.punctuation})); -})(); \ No newline at end of file From d614792df5eea42f4d079fbf30dc87c48dfe5ec4 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 10 Nov 2020 08:24:01 +0700 Subject: [PATCH 07/13] fix(common/core/web): ensures lexical-model-compiler is built for tests --- common/core/web/input-processor/package.json | 8 ++++---- common/core/web/input-processor/test.sh | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/core/web/input-processor/package.json b/common/core/web/input-processor/package.json index 1ab168fbba..da6e05ffbe 100644 --- a/common/core/web/input-processor/package.json +++ b/common/core/web/input-processor/package.json @@ -18,10 +18,13 @@ }, "homepage": "https://github.com/keymanapp/keyman#readme", "devDependencies": { + "@keymanapp/lexical-model-compiler": "^14.0.155", "@keymanapp/resources-gosh": "^14.0.155", + "@types/node": "^11.9.4", "chai": "^4.2.0", "mocha": "^5.2.0", "mocha-teamcity-reporter": "^3.0.0", + "ts-node": "^8.0.2", "typescript": "^3.8.3" }, "scripts": { @@ -32,13 +35,10 @@ }, "dependencies": { "@keymanapp/keyboard-processor": "^14.0.155", - "@keymanapp/lexical-model-compiler": "^14.0.155", "@keymanapp/lexical-model-layer": "^14.0.155", "@keymanapp/models-types": "^14.0.155", "@keymanapp/web-environment": "^14.0.155", "@keymanapp/web-utils": "^14.0.155", - "@types/node": "^11.9.4", - "eventemitter3": "^4.0.0", - "ts-node": "^8.0.2" + "eventemitter3": "^4.0.0" } } diff --git a/common/core/web/input-processor/test.sh b/common/core/web/input-processor/test.sh index 9595b8d8fa..eb2dc327ac 100755 --- a/common/core/web/input-processor/test.sh +++ b/common/core/web/input-processor/test.sh @@ -51,6 +51,12 @@ if [ $FETCH_DEPS = true ]; then verify_npm_setup fi +# Ensures that the lexical model compiler has been built locally. +echo "Preparing the lexical model compiler" +pushd $WORKING_DIRECTORY/node_modules/@keymanapp/lexical-model-compiler +npm run build +popd + test-headless ( ) { if (( CI_REPORTING )); then FLAGS="$FLAGS --reporter mocha-teamcity-reporter" From effd471dc674cf52c0cd368b9f27df9f8a9a50d9 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 10 Nov 2020 08:56:06 +0700 Subject: [PATCH 08/13] fix(common/core/web): forgot to define a shell var --- common/core/web/input-processor/test.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/core/web/input-processor/test.sh b/common/core/web/input-processor/test.sh index eb2dc327ac..d24fb3df27 100755 --- a/common/core/web/input-processor/test.sh +++ b/common/core/web/input-processor/test.sh @@ -3,6 +3,8 @@ # We should work within the script's directory, not the one we were called in. cd $(dirname "$BASH_SOURCE") +WORKING_DIRECTORY=`pwd` + # Include useful testing resource functions ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary @@ -52,7 +54,7 @@ if [ $FETCH_DEPS = true ]; then fi # Ensures that the lexical model compiler has been built locally. -echo "Preparing the lexical model compiler" +echo "${TERM_HEADING}Preparing Lexical Model Compiler for test use${NORMAL}" pushd $WORKING_DIRECTORY/node_modules/@keymanapp/lexical-model-compiler npm run build popd From 301af0434ac52801c68482f48bda84cb21a11fc7 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 10 Nov 2020 09:23:43 +0700 Subject: [PATCH 09/13] fix(common/models): post-merge test required id strip --- .../unit_tests/headless/worker-dummy-integration.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js index c1146c97a7..a894fad0fe 100644 --- a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js +++ b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js @@ -67,15 +67,19 @@ describe('LMLayer using dummy model', function () { }).then(function () { return lmLayer.predict(zeroTransform(), emptyContext()); }).then(function (suggestions) { + stripIDs(suggestions); assert.deepEqual(suggestions, iGotDistractedByHazel()[0]); return lmLayer.predict(zeroTransform(), emptyContext()); }).then(function (suggestions) { + stripIDs(suggestions); assert.deepEqual(suggestions, iGotDistractedByHazel()[1]); return lmLayer.predict(zeroTransform(), emptyContext()); }).then(function (suggestions) { + stripIDs(suggestions); assert.deepEqual(suggestions, iGotDistractedByHazel()[2]); return lmLayer.predict(zeroTransform(), emptyContext()); }).then(function (suggestions) { + stripIDs(suggestions); assert.deepEqual(suggestions, iGotDistractedByHazel()[3]); lmLayer.shutdown(); return Promise.resolve(); From 7f8fb5cbd0ae2ebc708f803a315aa52bf744d164 Mon Sep 17 00:00:00 2001 From: jahorton Date: Tue, 10 Nov 2020 10:36:26 +0700 Subject: [PATCH 10/13] fix(common/models): more post-merge patchwork --- .../unit_tests/headless/worker-dummy-integration.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js index a894fad0fe..19043e86bf 100644 --- a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js +++ b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js @@ -54,6 +54,12 @@ describe('LMLayer using dummy model', function () { it('will predict future suggestions (loaded from raw source)', function () { var lmLayer = new LMLayer(capabilities()); + var stripIDs = function(suggestions) { + suggestions.forEach(function(suggestion) { + delete suggestion.id; + }); + }; + // We're running headlessly, so the path can be relative to the npm root directory. let modelCode = fs.readFileSync("./unit_tests/in_browser/resources/models/simple-dummy.js").toString(); From 6424b6816adec49ee3ece72b740b763ea6bf1f78 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 16 Nov 2020 12:06:28 +0700 Subject: [PATCH 11/13] fix(developer): undoes unwanted CI-breaking local npm side-effect --- windows/src/developer/inst/download.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/windows/src/developer/inst/download.in b/windows/src/developer/inst/download.in index 5b99e09053..45647e876a 100644 --- a/windows/src/developer/inst/download.in +++ b/windows/src/developer/inst/download.in @@ -107,6 +107,11 @@ heat-model-compiler: del kmlmc.tgz del kmlmc.tar + # Reverts the package's package.json to its original version; npm pack sometimes modifies + # the local file, breaking a later-occurring KMW build. + git restore package.json + git restore package-lock.json + # Step 2 - the model compiler has one in-repo dependency that will also need to be packed. # Managing other in-repo dependencies will be simpler; they install into ModelCompiler's # extracted bundle. From d6c90d1ae01b382c1589fc53923fba5454d2977e Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Mon, 16 Nov 2020 14:13:49 +0700 Subject: [PATCH 12/13] fix(developer): more same-build local change reversion --- windows/src/test/unit-tests/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows/src/test/unit-tests/Makefile b/windows/src/test/unit-tests/Makefile index 86b9790a59..9fdacbeb68 100644 --- a/windows/src/test/unit-tests/Makefile +++ b/windows/src/test/unit-tests/Makefile @@ -34,6 +34,12 @@ lexical-model-compiler: start /wait ./build.sh -test !endif +# Revert any local changes to the original source; these changes can break +# later sub-builds. + cd $(KEYMAN_ROOT)\developer\js + git restore package.json + git restore package-lock.json + kmcomp-x64-structures: cd $(ROOT)\src\test\unit-tests\group-helper-rsp19902 $(MAKE) $(TARGET) From 1e52b5c5bafe27e60e9e1a8e381769795d88e93b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 17 Nov 2020 10:07:57 +0700 Subject: [PATCH 13/13] fix(developer): reverts first bugfix attempt --- windows/src/developer/inst/download.in | 5 ----- 1 file changed, 5 deletions(-) diff --git a/windows/src/developer/inst/download.in b/windows/src/developer/inst/download.in index 45647e876a..5b99e09053 100644 --- a/windows/src/developer/inst/download.in +++ b/windows/src/developer/inst/download.in @@ -107,11 +107,6 @@ heat-model-compiler: del kmlmc.tgz del kmlmc.tar - # Reverts the package's package.json to its original version; npm pack sometimes modifies - # the local file, breaking a later-occurring KMW build. - git restore package.json - git restore package-lock.json - # Step 2 - the model compiler has one in-repo dependency that will also need to be packed. # Managing other in-repo dependencies will be simpler; they install into ModelCompiler's # extracted bundle.