diff --git a/web/package.json b/web/package.json index 3d73175202..cfbab0fcbf 100644 --- a/web/package.json +++ b/web/package.json @@ -85,7 +85,8 @@ }, "imports": { "#gesture-tools": "./src/engine/osk/gesture-processor/build/tools/obj/index.js", - "#recorder": "./build/tools/testing/recorder/obj/index.js" + "#recorder": "./build/tools/testing/recorder/obj/index.js", + "#test-resources/*.js": "./build/test/resources/*.js" }, "repository": { "type": "git", diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts index e9cc96a73a..c1859b1128 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts @@ -10,12 +10,8 @@ import { LexicalModelTypes } from "@keymanapp/common-types"; import { SearchNode, SearchResult } from "./distance-modeler.js"; -import { SearchQuotientSpur } from "./search-quotient-spur.js"; -import { SearchQuotientRoot } from "./search-quotient-root.js"; -import Distribution = LexicalModelTypes.Distribution; import LexicalModel = LexicalModelTypes.LexicalModel; -import Transform = LexicalModelTypes.Transform; let SPACE_ID_SEED = 0; @@ -211,103 +207,4 @@ export interface SearchQuotientNode { * @param charIndex */ split(charIndex: number): [SearchQuotientNode, SearchQuotientNode]; -} - -/** - * Denotes whether or not the represented search-space quotient path includes - * paths built from the specified set of keystroke input distributions. The - * distribution count should match .inputCount - no omissions or extras are - * permitted. - * - * Designed explicitly for use in unit testing; it's not super-efficient, so - * avoid live use. - * - * @param keystrokeDistributions - * @internal - */ -function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistributions: Distribution[]): boolean { - if(!(node instanceof SearchQuotientSpur)) { - for(const p of node.parents) { - if(quotientPathHasInputs(p, keystrokeDistributions)) { - return true; - } - } - - return node.parents.length == 0 && keystrokeDistributions.length == 0; - } - - if(node.inputCount == 0) { - return keystrokeDistributions.length == 0; - } else if(keystrokeDistributions.length != node.inputCount) { - return false; - } - - const tailInput = [...keystrokeDistributions[keystrokeDistributions.length - 1]]; - keystrokeDistributions = keystrokeDistributions.slice(0, keystrokeDistributions.length - 1); - const localInput = node.lastInput; - - const parentHasInput = () => !!node.parents.find(p => quotientPathHasInputs(p, keystrokeDistributions)); - - // Actual reference match? Easy mode. - if(localInput == tailInput) { - return parentHasInput(); - } else if(localInput.length != tailInput.length) { - return false; - } else { - for(let entry of tailInput) { - const matchIndex = localInput.findIndex((x) => { - const s1 = x.sample; - const s2 = entry.sample; - // Check for equal reference first before the other checks; it makes a nice shortcut. - if(x == entry) { - return true; - } - - if(x.p == entry.p && s1.deleteLeft == s2.deleteLeft - && s1.id == s2.id && ((s1.deleteRight ?? 0) == (s2.deleteRight ?? 0)) && s1.insert == s2.insert - ) { - return true; - } - - return false; - }); - - if(matchIndex == -1) { - return false; - } else { - tailInput.splice(matchIndex, 1); - } - } - - return parentHasInput(); - } -} - -/** - * Enumerates the different potential SearchQuotientSpur sequences that lead - * to a SearchQuotientNode. - * - * Intended only for use during unit testing. Does not include the root node. - */ -function constituentPaths(node: SearchQuotientNode): SearchQuotientSpur[][] { - if(node instanceof SearchQuotientRoot) { - return []; - } else if(node instanceof SearchQuotientSpur) { - const parentPaths = constituentPaths(node.parents[0]); - if(parentPaths.length > 0) { - return parentPaths.map(p => { - p.push(node); - return p; - }); - } else { - return [[node]]; - } - } else { - throw new Error("constituentPaths is unable to handle a new, unexpected SearchQuotientNode type"); - } -} - -export const unitTestEndpoints = { - quotientPathHasInputs, - constituentPaths } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts b/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts index c6a05e7694..291e9296b7 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts @@ -20,10 +20,4 @@ export { ModelCompositor } from './model-compositor.js'; export * from './predict-helpers.js'; export { default as TransformUtils } from './transformUtils.js' export { default as LMLayerWorker } from './index.js' -export * from './transform-subsets.js'; - -// unitTestEndpoints -import { unitTestEndpoints as SearchQuotientNodeEndpoints } from './correction/search-quotient-node.js'; -export const unitTestEndpoints: typeof SearchQuotientNodeEndpoints = { - ...SearchQuotientNodeEndpoints -} \ No newline at end of file +export * from './transform-subsets.js'; \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts index cfb9fa0903..d9ac75f073 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts @@ -15,15 +15,15 @@ import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs' import { LexicalModelTypes } from '@keymanapp/common-types'; import { KMWString } from '@keymanapp/web-utils'; -import { ContextToken, correction, generateSubsetId, getBestMatches, InputSegment, models, SearchQuotientSpur, unitTestEndpoints } from '@keymanapp/lm-worker/test-index'; +import { ContextToken, correction, generateSubsetId, getBestMatches, InputSegment, models, SearchQuotientSpur } from '@keymanapp/lm-worker/test-index'; + +import { quotientPathHasInputs } from "#test-resources/searchQuotientUtils.js"; import Distribution = LexicalModelTypes.Distribution; import ExecutionTimer = correction.ExecutionTimer; import Transform = LexicalModelTypes.Transform; import TrieModel = models.TrieModel; -const { quotientPathHasInputs } = unitTestEndpoints; - var plainModel = new TrieModel(jsonFixture('models/tries/english-1000'), {wordBreaker: defaultBreaker}); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts index 7e50825b57..9d3a474d9e 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts @@ -1,11 +1,11 @@ import { assert } from 'chai'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; -import { LegacyQuotientRoot, models, unitTestEndpoints } from '@keymanapp/lm-worker/test-index'; +import { LegacyQuotientRoot, models } from '@keymanapp/lm-worker/test-index'; import { buildSimplePathSplitFixture } from './search-quotient-spur.tests.js'; -const { quotientPathHasInputs } = unitTestEndpoints; +import { quotientPathHasInputs } from '#test-resources/searchQuotientUtils.js'; import TrieModel = models.TrieModel; diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts index f2f3f74183..2da326a18c 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts @@ -20,15 +20,14 @@ import { PathInputProperties, SearchQuotientNode, SearchQuotientRoot, - SearchQuotientSpur, - unitTestEndpoints + SearchQuotientSpur } from '@keymanapp/lm-worker/test-index'; import Distribution = LexicalModelTypes.Distribution; import Transform = LexicalModelTypes.Transform; import TrieModel = models.TrieModel; -const { constituentPaths, quotientPathHasInputs } = unitTestEndpoints; +import { constituentPaths, quotientPathHasInputs } from '#test-resources/searchQuotientUtils.js'; const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); diff --git a/web/src/test/auto/resources/searchQuotientUtils.ts b/web/src/test/auto/resources/searchQuotientUtils.ts new file mode 100644 index 0000000000..0aa6f170c4 --- /dev/null +++ b/web/src/test/auto/resources/searchQuotientUtils.ts @@ -0,0 +1,109 @@ +/** + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by jahorton on 2026-02-05 + * + * This file adds helper functions useful for developing test assertions against + * different types of SearchQuotientNodes and their properties. + */ + +import { LexicalModelTypes } from "@keymanapp/common-types"; + +import { SearchQuotientNode, SearchQuotientRoot, SearchQuotientSpur } from "@keymanapp/lm-worker/test-index"; + +import Distribution = LexicalModelTypes.Distribution; +import Transform = LexicalModelTypes.Transform; + +/** + * Denotes whether or not the represented search-space quotient path includes + * paths built from the specified set of keystroke input distributions. The + * distribution count should match .inputCount - no omissions or extras are + * permitted. + * + * Designed explicitly for use in unit testing; it's not super-efficient, so + * avoid live use. + * + * @param keystrokeDistributions + * @internal + */ +export function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistributions: Distribution[]): boolean { + if(!(node instanceof SearchQuotientSpur)) { + for(const p of node.parents) { + if(quotientPathHasInputs(p, keystrokeDistributions)) { + return true; + } + } + + return node.parents.length == 0 && keystrokeDistributions.length == 0; + } + + if(node.inputCount == 0) { + return keystrokeDistributions.length == 0; + } else if(keystrokeDistributions.length != node.inputCount) { + return false; + } + + const tailInput = [...keystrokeDistributions[keystrokeDistributions.length - 1]]; + keystrokeDistributions = keystrokeDistributions.slice(0, keystrokeDistributions.length - 1); + const localInput = node.lastInput; + + const parentHasInput = () => !!node.parents.find(p => quotientPathHasInputs(p, keystrokeDistributions)); + + // Actual reference match? Easy mode. + if(localInput == tailInput) { + return parentHasInput(); + } else if(localInput.length != tailInput.length) { + return false; + } else { + for(let entry of tailInput) { + const matchIndex = localInput.findIndex((x) => { + const s1 = x.sample; + const s2 = entry.sample; + // Check for equal reference first before the other checks; it makes a nice shortcut. + if(x == entry) { + return true; + } + + if(x.p == entry.p && s1.deleteLeft == s2.deleteLeft + && s1.id == s2.id && ((s1.deleteRight ?? 0) == (s2.deleteRight ?? 0)) && s1.insert == s2.insert + ) { + return true; + } + + return false; + }); + + if(matchIndex == -1) { + return false; + } else { + tailInput.splice(matchIndex, 1); + } + } + + return parentHasInput(); + } +} + +/** + * Enumerates the different potential SearchQuotientSpur sequences that lead + * to a SearchQuotientNode. + * + * Intended only for use during unit testing. Does not include the root node. + */ +export function constituentPaths(node: SearchQuotientNode): SearchQuotientSpur[][] { + if(node instanceof SearchQuotientRoot) { + return []; + } else if(node instanceof SearchQuotientSpur) { + const parentPaths = constituentPaths(node.parents[0]); + if(parentPaths.length > 0) { + return parentPaths.map(p => { + p.push(node); + return p; + }); + } else { + return [[node]]; + } + } else { + throw new Error("constituentPaths is unable to handle a new, unexpected SearchQuotientNode type"); + } +} \ No newline at end of file