-
-
Notifications
You must be signed in to change notification settings - Fork 142
refactor(web): reorganize worker-thread test helpers and test fixtures 🚂 #15716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e72e9af
f78a7ba
7469f68
44fdb6e
79fd689
2ff1691
7aa4cc4
a09dc04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| /* | ||
| * Keyman is copyright (C) SIL Global. MIT License. | ||
| * | ||
| * Created by jahorton on 2026-03-09 | ||
| * | ||
| * This file tests the construction of the helper test-fixture function in | ||
| * buildAlphabeticClusteredFixture.ts. | ||
| */ | ||
|
|
||
| import { assert } from "chai"; | ||
|
|
||
| import { SearchQuotientNode } from "@keymanapp/lm-worker/test-index"; | ||
|
|
||
| import { constituentPaths } from "./constituentPaths.js"; | ||
| import { quotientPathHasInputs } from "./quotientPathHasInputs.js"; | ||
| import { buildAlphabeticClusterFixtures } from "./buildAlphabeticClusteredFixture.js"; | ||
|
|
||
| describe('buildAlphabeticClusteredFixture() fixture', () => { | ||
| it('constructs paths properly', () => { | ||
| const { clusters, paths, distributions } = buildAlphabeticClusterFixtures(); | ||
| assert.equal(clusters.cluster_k5c6.inputCount, 5); | ||
|
|
||
| const allPaths = Object.values(paths).map(set => Object.values(set)).flat() as SearchQuotientNode[]; | ||
| const allDists = Object.values(distributions).map(set => Object.values(set)).flat(); | ||
| const finalClusterPaths = constituentPaths(clusters.cluster_k5c6) as SearchQuotientNode[][]; | ||
|
|
||
| allPaths.forEach((spur) => assert.isOk(finalClusterPaths.find(seq => seq.indexOf(spur) > -1))); | ||
| allDists.forEach((dist) => assert.isOk(allPaths.find(path => quotientPathHasInputs(path, [dist])))); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| /* | ||
| * Keyman is copyright (C) SIL Global. MIT License. | ||
| * | ||
| * Created by jahorton on 2026-03-09 | ||
| * | ||
| * This file defines a unit-text fixture designed for testing | ||
| * behaviors related to the clustering of search quotient spurs | ||
| * based on just substitution/match edges. | ||
|
Comment on lines
+6
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment doesn't seem to match the content of the file.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any suggestions for improving this, then? That comment was intended for this file's header comment; it's not a copy-paste artifact.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But this file contains helper methods, not the test fixture. As it is the comment would fit on
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, that method is the test fixture - or rather, it constructs it anew on each call. It requires live construction, like the |
||
| */ | ||
|
|
||
| import { LexicalModelTypes } from '@keymanapp/common-types'; | ||
| import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; | ||
|
|
||
| import { | ||
| models, | ||
| LegacyQuotientSpur, | ||
| SearchQuotientCluster, | ||
| LegacyQuotientRoot | ||
| } from '@keymanapp/lm-worker/test-index'; | ||
|
|
||
| import Distribution = LexicalModelTypes.Distribution; | ||
| import Transform = LexicalModelTypes.Transform; | ||
| import TrieModel = models.TrieModel; | ||
|
|
||
| const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); | ||
|
|
||
| /** | ||
| * Builds a fixture for use in unit-testing against diverging and re-converging | ||
| * routes through the modeled correction-search graph and its SearchQuotientNode | ||
| * representation. | ||
| * @returns | ||
| */ | ||
| export const buildAlphabeticClusterFixtures = () => { | ||
| const rootPath = new LegacyQuotientRoot(testModel); | ||
|
|
||
| // consonant-cluster 1, insert 1, delete 0 | ||
| const distrib_c1_i1d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'b', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 0.3 }, // most likely for id 11 | ||
| { sample: { insert: 'c', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 0.2 }, | ||
| { sample: { insert: 'd', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 0.1 }, | ||
| ]; | ||
|
|
||
| // consonant-cluster 1, insert 2, delete 0 | ||
| const distrib_c1_i2d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'fg', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 0.16 }, | ||
| { sample: { insert: 'hj', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 0.14 }, | ||
| { sample: { insert: 'kl', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 0.1 }, | ||
| ]; | ||
|
|
||
| // keystrokes 1, codepoints 1, total inserts 1, delete 0 | ||
| const path_k1c1_i1d0 = new LegacyQuotientSpur(rootPath, distrib_c1_i1d0, distrib_c1_i1d0[0]); | ||
| // keystrokes 1, codepoints 2, total inserts 2, delete 0 | ||
| const path_k1c2_i2d0 = new LegacyQuotientSpur(rootPath, distrib_c1_i2d0, distrib_c1_i1d0[0]); | ||
|
|
||
| // Second input | ||
|
|
||
| const distrib_v1_i1d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'e', deleteLeft: 0, deleteRight: 0, id: 12 }, p: 0.4 }, // most likely for id 12 | ||
| { sample: { insert: 'a', deleteLeft: 0, deleteRight: 0, id: 12 }, p: 0.3 }, | ||
| { sample: { insert: 'i', deleteLeft: 0, deleteRight: 0, id: 12 }, p: 0.1 }, | ||
| { sample: { insert: 'o', deleteLeft: 0, deleteRight: 0, id: 12 }, p: 0.1 }, | ||
| { sample: { insert: 'u', deleteLeft: 0, deleteRight: 0, id: 12 }, p: 0.1 }, | ||
| ]; | ||
|
|
||
| const path_k2c2_i2d0 = new LegacyQuotientSpur(path_k1c1_i1d0, distrib_v1_i1d0, distrib_v1_i1d0[0]); | ||
| const path_k2c3_i3d0 = new LegacyQuotientSpur(path_k1c2_i2d0, distrib_v1_i1d0, distrib_v1_i1d0[0]); | ||
|
|
||
| // Third input | ||
| const distrib_v2_i1d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'e', deleteLeft: 0, deleteRight: 0, id: 13 }, p: 0.15 }, // most likely for id 13 | ||
| { sample: { insert: 'a', deleteLeft: 0, deleteRight: 0, id: 13 }, p: 0.13 }, | ||
| { sample: { insert: 'i', deleteLeft: 0, deleteRight: 0, id: 13 }, p: 0.12 }, | ||
| { sample: { insert: 'o', deleteLeft: 0, deleteRight: 0, id: 13 }, p: 0.11 }, | ||
| { sample: { insert: 'u', deleteLeft: 0, deleteRight: 0, id: 13 }, p: 0.09 }, | ||
| ]; // 0.60 total | ||
|
|
||
| const distrib_v2_i1d1: Distribution<Transform> = [ | ||
| { sample: { insert: 'é', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.06 }, | ||
| { sample: { insert: 'á', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.05 }, | ||
| { sample: { insert: 'í', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.04 }, | ||
| { sample: { insert: 'ó', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.03 }, | ||
| { sample: { insert: 'ú', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.02 }, | ||
| ]; // 0.2 total | ||
|
|
||
| const distrib_v2_i2d1: Distribution<Transform> = [ | ||
| { sample: { insert: 'éé', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.06 }, | ||
| { sample: { insert: 'áá', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.05 }, | ||
| { sample: { insert: 'íí', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.04 }, | ||
| { sample: { insert: 'óó', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.03 }, | ||
| { sample: { insert: 'úú', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.02 }, | ||
| ]; // 0.2 total | ||
|
|
||
| const path_k3c2_i3d1 = new LegacyQuotientSpur(path_k2c2_i2d0, distrib_v2_i1d1, distrib_v2_i1d0[0]); | ||
|
|
||
| const path_k3c3_i3d0 = new LegacyQuotientSpur(path_k2c2_i2d0, distrib_v2_i1d0, distrib_v2_i1d0[0]); | ||
| const path_k3c3_i4d1a = new LegacyQuotientSpur(path_k2c2_i2d0, distrib_v2_i2d1, distrib_v2_i1d0[0]); | ||
| const path_k3c3_i4d1b = new LegacyQuotientSpur(path_k2c3_i3d0, distrib_v2_i1d1, distrib_v2_i1d0[0]); | ||
|
|
||
| // both are built on path k1c2 (splits at index 1) | ||
| const path_k3c4_i4d0 = new LegacyQuotientSpur(path_k2c3_i3d0, distrib_v2_i1d0, distrib_v2_i1d0[0]); | ||
| const path_k3c4_i5d1 = new LegacyQuotientSpur(path_k2c3_i3d0, distrib_v2_i2d1, distrib_v2_i1d0[0]); | ||
|
|
||
| const cluster_k3c3 = new SearchQuotientCluster([path_k3c3_i3d0, path_k3c3_i4d1a, path_k3c3_i4d1b]); | ||
| // both are built on path k1c2. | ||
| const cluster_k3c4 = new SearchQuotientCluster([path_k3c4_i4d0, path_k3c4_i5d1]); | ||
|
|
||
| // Input 4 | ||
| const distrib_c2_i1d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'n', deleteLeft: 0, deleteRight: 0, id: 14 }, p: 0.12 }, | ||
| { sample: { insert: 'p', deleteLeft: 0, deleteRight: 0, id: 14 }, p: 0.08 }, | ||
| ]; | ||
|
|
||
| const distrib_c2_i2d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'qr', deleteLeft: 0, deleteRight: 0, id: 14 }, p: 0.3 }, // most likely for id 14 | ||
| { sample: { insert: 'st', deleteLeft: 0, deleteRight: 0, id: 14 }, p: 0.2 }, | ||
| { sample: { insert: 'vw', deleteLeft: 0, deleteRight: 0, id: 14 }, p: 0.1 } | ||
| ]; | ||
|
|
||
| const path_k4c4_i2 = new LegacyQuotientSpur(path_k3c2_i3d1, distrib_c2_i2d0, distrib_c2_i2d0[0]); | ||
| const path_k4c4_i1 = new LegacyQuotientSpur(cluster_k3c3, distrib_c2_i1d0, distrib_c2_i2d0[0]); | ||
|
|
||
| const path_k4c5_i2 = new LegacyQuotientSpur(cluster_k3c3, distrib_c2_i2d0, distrib_c2_i2d0[0]); | ||
| const path_k4c5_i1 = new LegacyQuotientSpur(cluster_k3c4, distrib_c2_i1d0, distrib_c2_i2d0[0]); | ||
|
|
||
| const path_k4c6 = new LegacyQuotientSpur(cluster_k3c4, distrib_c2_i2d0, distrib_c2_i2d0[0]); | ||
|
|
||
| const cluster_k4c4 = new SearchQuotientCluster([path_k4c4_i2, path_k4c4_i1]); | ||
| const cluster_k4c5 = new SearchQuotientCluster([path_k4c5_i2, path_k4c5_i1]); | ||
|
|
||
| // Input 5 (currently used only for merge tests) | ||
| const distrib_c3_i2d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'xy', deleteLeft: 0, deleteRight: 0, id: 15 }, p: 0.6 } // most likely for id 15 | ||
| ]; | ||
| const distrib_c3_i1d0: Distribution<Transform> = [ | ||
| { sample: { insert: 'z', deleteLeft: 0, deleteRight: 0, id: 15 }, p: 0.4 } | ||
| ]; | ||
|
|
||
| const path_k5c6_a = new LegacyQuotientSpur(cluster_k4c4, distrib_c3_i2d0, distrib_c3_i2d0[0]); | ||
| const path_k5c6_b = new LegacyQuotientSpur(cluster_k4c5, distrib_c3_i1d0, distrib_c3_i2d0[0]); | ||
|
|
||
| const cluster_k5c6 = new SearchQuotientCluster([path_k5c6_a, path_k5c6_b]); | ||
|
|
||
| return { | ||
| distributions: { | ||
| 1: { | ||
| distrib_c1_i1d0, | ||
| distrib_c1_i2d0 | ||
| }, | ||
| 2: { | ||
| distrib_v1_i1d0 | ||
| }, | ||
| 3: { | ||
| distrib_v2_i1d0, | ||
| distrib_v2_i1d1, | ||
| distrib_v2_i2d1 | ||
| }, | ||
| 4: { | ||
| distrib_c2_i1d0, | ||
| distrib_c2_i2d0 | ||
| }, | ||
| 5: { | ||
| distrib_c3_i1d0, | ||
| distrib_c3_i2d0 | ||
| } | ||
| }, | ||
| paths: { | ||
| 0: { | ||
| rootPath | ||
| }, | ||
| 1: { | ||
| path_k1c1_i1d0, | ||
| path_k1c2_i2d0, | ||
| }, | ||
| 2: { | ||
| path_k2c2_i2d0, | ||
| path_k2c3_i3d0, | ||
| }, | ||
| 3: { | ||
| path_k3c2_i3d1, | ||
| path_k3c3_i3d0, | ||
| path_k3c3_i4d1a, | ||
| path_k3c3_i4d1b, | ||
| path_k3c4_i4d0, | ||
| path_k3c4_i5d1, | ||
| }, | ||
| 4: { | ||
| path_k4c4_i2, | ||
| path_k4c4_i1, | ||
| path_k4c5_i2, | ||
| path_k4c5_i1, | ||
| path_k4c6 | ||
| }, | ||
| 5: { | ||
| path_k5c6_a, | ||
| path_k5c6_b | ||
| } | ||
| }, | ||
| clusters: { | ||
| cluster_k3c3, | ||
| cluster_k3c4, | ||
| cluster_k4c4, | ||
| cluster_k4c5, | ||
| cluster_k5c6 | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* | ||
| * Keyman is copyright (C) SIL Global. MIT License. | ||
| * | ||
| * Created by jahorton on 2026-03-09 | ||
| * | ||
| * This file defines unit tests for validating a unit-text fixture for behaviors | ||
| * related to a simple linear sequence of SearchQuotientSpurs, with no | ||
| * convergence or divergence occurring within the fixture. | ||
| */ | ||
|
|
||
| import { assert } from "chai"; | ||
|
jahorton marked this conversation as resolved.
|
||
|
|
||
| import { constituentPaths } from "./constituentPaths.js"; | ||
| import { quotientPathHasInputs } from "./quotientPathHasInputs.js"; | ||
| import { buildCantLinearFixture } from "./buildCantLinearFixture.js"; | ||
|
|
||
| describe('buildCantLinearFixture() fixture', () => { | ||
| it('constructs paths properly', () => { | ||
| const { paths, distributions } = buildCantLinearFixture(); | ||
| const pathToSplit = paths[4]; | ||
|
|
||
| assert.equal(pathToSplit.inputCount, 4); | ||
| assert.equal(distributions.length, pathToSplit.inputCount); | ||
| assert.equal(pathToSplit.codepointLength, 4); // one char per input, no deletions anywhere | ||
| // Per assertions documented in the setup above. | ||
| assert.deepEqual(pathToSplit.bestExample, distributions.reduce( | ||
| (constructing, current) => ({text: constructing.text + current[0].sample.insert, p: constructing.p * current[0].p}), | ||
| {text: '', p: 1}) | ||
| ); | ||
| assert.deepEqual(pathToSplit.parents[0].bestExample, distributions.slice(0, pathToSplit.inputCount-1).reduce( | ||
| (constructing, current) => ({text: constructing.text + current[0].sample.insert, p: constructing.p * current[0].p}), | ||
| {text: '', p: 1}) | ||
| ); | ||
| assert.isTrue(quotientPathHasInputs(pathToSplit, distributions)); | ||
| assert.equal(constituentPaths(pathToSplit).length, 1); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| /* | ||
| * Keyman is copyright (C) SIL Global. MIT License. | ||
| * | ||
| * Created by jahorton on 2026-03-09 | ||
| * | ||
| * This file defines a unit-text fixture for testing behaviors related to a | ||
| * simple linear sequence of SearchQuotientSpurs, with no convergence or | ||
| * divergence occurring within the fixture. | ||
| */ | ||
|
|
||
| import { LegacyQuotientRoot, LegacyQuotientSpur, models } from "@keymanapp/lm-worker/test-index"; | ||
|
jahorton marked this conversation as resolved.
|
||
| import { jsonFixture } from "@keymanapp/common-test-resources/model-helpers.mjs"; | ||
|
|
||
| import TrieModel = models.TrieModel; | ||
|
|
||
| const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); | ||
|
|
||
| /** | ||
| * Build a linear fixture that models the word 'cant' and words close to that. | ||
| */ | ||
| export function buildCantLinearFixture() { | ||
|
jahorton marked this conversation as resolved.
|
||
| const rootPath = new LegacyQuotientRoot(testModel); | ||
|
|
||
| const distrib1 = [ | ||
| { sample: {insert: 'c', deleteLeft: 0, id: 11}, p: 0.5 }, | ||
| { sample: {insert: 'r', deleteLeft: 0, id: 11}, p: 0.4 }, | ||
| { sample: {insert: 't', deleteLeft: 0, id: 11}, p: 0.1 } | ||
| ]; | ||
| const path1 = new LegacyQuotientSpur(rootPath, distrib1, distrib1[0]); | ||
|
|
||
| const distrib2 = [ | ||
| { sample: {insert: 'a', deleteLeft: 0, id: 12}, p: 0.7 }, | ||
| { sample: {insert: 'e', deleteLeft: 0, id: 12}, p: 0.3 } | ||
| ]; | ||
| const path2 = new LegacyQuotientSpur(path1, distrib2, distrib2[0]); | ||
|
|
||
| const distrib3 = [ | ||
| { sample: {insert: 'n', deleteLeft: 0, id: 13}, p: 0.8 }, | ||
| { sample: {insert: 'r', deleteLeft: 0, id: 13}, p: 0.2 } | ||
| ]; | ||
| const path3 = new LegacyQuotientSpur(path2, distrib3, distrib3[0]); | ||
|
|
||
| const distrib4 = [ | ||
| { sample: {insert: 't', deleteLeft: 0, id: 14}, p: 1 } | ||
| ]; | ||
| const path4 = new LegacyQuotientSpur(path3, distrib4, distrib4[0]); | ||
|
|
||
| return { | ||
| paths: [null, path1, path2, path3, path4], | ||
| distributions: [distrib1, distrib2, distrib3, distrib4] | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * Keyman is copyright (C) SIL Global. MIT License. | ||
| * | ||
| * Created by jahorton on 2026-03-10 | ||
| * | ||
| * This file adds unit tests for unit-test helper functions validating | ||
| * the correction-search modules of the Keyman predictive-text engine. | ||
| */ | ||
|
|
||
| import { assert } from 'chai'; | ||
|
|
||
| import { constituentPaths } from "./constituentPaths.js"; | ||
| import { buildCantLinearFixture } from './buildCantLinearFixture.js'; | ||
| import { buildAlphabeticClusterFixtures } from './buildAlphabeticClusteredFixture.js'; | ||
|
|
||
| describe('constituentPaths', () => { | ||
| it('includes a single entry array when all parents are SearchQuotientSpurs', () => { | ||
| const { paths } = buildCantLinearFixture(); | ||
| const finalPath = paths[4]; | ||
|
|
||
| assert.equal(constituentPaths(finalPath).length, 1); | ||
|
|
||
| const pathSequence = constituentPaths(finalPath)[0]; | ||
| assert.equal(pathSequence.length, 4); // 4 inputs; does not include root node | ||
|
|
||
| assert.sameOrderedMembers(pathSequence, paths.slice(1)); | ||
| }); | ||
|
|
||
| it('properly enumerates child paths when encountering SearchCluster ancestors', () => { | ||
| const fixture = buildAlphabeticClusterFixtures(); | ||
| const finalPath = fixture.paths[4].path_k4c6; | ||
|
|
||
| // The longest SearchPath at the end of that fixture's set is based on a | ||
| // lead-in cluster; all variants of that should be included. | ||
| assert.equal(constituentPaths(finalPath).length, constituentPaths(fixture.clusters.cluster_k3c4).length); | ||
|
|
||
| // That cluster holds the different potential penultimate paths; | ||
| // finalPath's inputs are added directly after any variation that may be | ||
| // output from the cluster. | ||
| assert.sameDeepMembers(constituentPaths(finalPath), constituentPaths(fixture.clusters.cluster_k3c4).map((p) => { | ||
| p.push(finalPath); | ||
| return p; | ||
| })); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.