Skip to content

Commit 73bbc55

Browse files
committed
Working custom spoken form tests
1 parent 84df483 commit 73bbc55

File tree

8 files changed

+375
-27
lines changed

8 files changed

+375
-27
lines changed

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ScopeRangeProvider } from "./scopeProviders/ScopeRangeProvider";
2424
import { ScopeRangeWatcher } from "./scopeProviders/ScopeRangeWatcher";
2525
import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker";
2626
import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher";
27-
import { TalonSpokenFormsJsonReader } from "./scopeProviders/getSpokenFormEntries";
27+
import { TalonSpokenFormsJsonReader } from "./scopeProviders/TalonSpokenFormsJsonReader";
2828
import { injectIde } from "./singletons/ide.singleton";
2929

3030
export function createCursorlessEngine(
Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,68 @@
11
import { ScopeType, ScopeTypeInfo } from "@cursorless/common";
2-
import Sinon = require("sinon");
2+
import * as sinon from "sinon";
33
import { assert } from "chai";
44
import { sleepWithBackoff } from "../../endToEndTestSetup";
55
import { isEqual } from "lodash";
66

7-
export async function assertCalledWithScopeInfo<T extends ScopeTypeInfo>(
8-
fake: Sinon.SinonSpy<[scopeInfos: T[]], void>,
9-
expectedScopeInfo: T,
7+
async function sleepAndCheck<T>(
8+
fake: sinon.SinonSpy<[scopeInfos: T[]], void>,
9+
check: () => void,
1010
) {
1111
await sleepWithBackoff(25);
12-
Sinon.assert.called(fake);
13-
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
14-
isEqual(scopeInfo.scopeType, expectedScopeInfo.scopeType),
15-
);
16-
assert.isDefined(actualScopeInfo);
17-
assert.deepEqual(actualScopeInfo, expectedScopeInfo);
12+
sinon.assert.called(fake);
13+
14+
check();
15+
1816
fake.resetHistory();
1917
}
2018

21-
export async function assertCalledWithoutScopeInfo<T extends ScopeTypeInfo>(
22-
fake: Sinon.SinonSpy<[scopeInfos: T[]], void>,
23-
scopeType: ScopeType,
19+
export function assertCalled<T extends ScopeTypeInfo>(
20+
fake: sinon.SinonSpy<[scopeInfos: T[]], void>,
21+
expectedScopeInfos: T[],
22+
expectedNotToHaveScopeTypes: ScopeType[],
2423
) {
25-
await sleepWithBackoff(25);
26-
Sinon.assert.called(fake);
27-
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
28-
isEqual(scopeInfo.scopeType, scopeType),
29-
);
30-
assert.isUndefined(actualScopeInfo);
31-
fake.resetHistory();
24+
return sleepAndCheck(fake, () => {
25+
assertCalledWith(expectedScopeInfos, fake);
26+
assertCalledWithout(expectedNotToHaveScopeTypes, fake);
27+
});
28+
}
29+
30+
export function assertCalledWithScopeInfo<T extends ScopeTypeInfo>(
31+
fake: sinon.SinonSpy<[scopeInfos: T[]], void>,
32+
...expectedScopeInfos: T[]
33+
) {
34+
return sleepAndCheck(fake, () => assertCalledWith(expectedScopeInfos, fake));
35+
}
36+
37+
export async function assertCalledWithoutScopeType<T extends ScopeTypeInfo>(
38+
fake: sinon.SinonSpy<[scopeInfos: T[]], void>,
39+
...scopeTypes: ScopeType[]
40+
) {
41+
return sleepAndCheck(fake, () => assertCalledWithout(scopeTypes, fake));
42+
}
43+
44+
function assertCalledWith<T extends ScopeTypeInfo>(
45+
expectedScopeInfos: T[],
46+
fake: sinon.SinonSpy<[scopeInfos: T[]], void>,
47+
) {
48+
for (const expectedScopeInfo of expectedScopeInfos) {
49+
const actualScopeInfo = fake.lastCall.args[0].find((scopeInfo) =>
50+
isEqual(scopeInfo.scopeType, expectedScopeInfo.scopeType),
51+
);
52+
assert.isDefined(actualScopeInfo);
53+
assert.deepEqual(actualScopeInfo, expectedScopeInfo);
54+
}
55+
}
56+
57+
function assertCalledWithout<T extends ScopeTypeInfo>(
58+
scopeTypes: ScopeType[],
59+
fake: sinon.SinonSpy<[scopeInfos: T[]], void>,
60+
) {
61+
for (const scopeType of scopeTypes) {
62+
assert.isUndefined(
63+
fake.lastCall.args[0].find((scopeInfo) =>
64+
isEqual(scopeInfo.scopeType, scopeType),
65+
),
66+
);
67+
}
3268
}

packages/cursorless-vscode-e2e/src/suite/scopeProvider/runBasicScopeInfoTest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
ScopeSupportInfo,
55
ScopeSupportLevels,
66
} from "@cursorless/common";
7-
import Sinon = require("sinon");
7+
import * as sinon from "sinon";
88
import { Position, Range, TextDocument, commands } from "vscode";
99
import { assertCalledWithScopeInfo } from "./assertCalledWithScopeInfo";
1010

@@ -14,7 +14,7 @@ import { assertCalledWithScopeInfo } from "./assertCalledWithScopeInfo";
1414
*/
1515
export async function runBasicScopeInfoTest() {
1616
const { scopeProvider } = (await getCursorlessApi()).testHelpers!;
17-
const fake = Sinon.fake<[scopeInfos: ScopeSupportLevels], void>();
17+
const fake = sinon.fake<[scopeInfos: ScopeSupportLevels], void>();
1818

1919
await commands.executeCommand("workbench.action.closeAllEditors");
2020

packages/cursorless-vscode-e2e/src/suite/scopeProvider/runCustomRegexScopeInfoTest.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import {
77
ScopeType,
88
sleep,
99
} from "@cursorless/common";
10-
import Sinon = require("sinon");
10+
import * as sinon from "sinon";
1111
import {
1212
assertCalledWithScopeInfo,
13-
assertCalledWithoutScopeInfo as assertCalledWithoutScope,
13+
assertCalledWithoutScopeType as assertCalledWithoutScope,
1414
} from "./assertCalledWithScopeInfo";
1515
import { stat, unlink, writeFile } from "fs/promises";
1616
import { sleepWithBackoff } from "../../endToEndTestSetup";
17+
import { commands } from "vscode";
1718

1819
/**
1920
* Tests that the scope provider correctly reports the scope support for a
@@ -22,7 +23,9 @@ import { sleepWithBackoff } from "../../endToEndTestSetup";
2223
export async function runCustomRegexScopeInfoTest() {
2324
const { scopeProvider, spokenFormsJsonPath } = (await getCursorlessApi())
2425
.testHelpers!;
25-
const fake = Sinon.fake<[scopeInfos: ScopeSupportLevels], void>();
26+
const fake = sinon.fake<[scopeInfos: ScopeSupportLevels], void>();
27+
28+
await commands.executeCommand("workbench.action.closeAllEditors");
2629

2730
const disposable = scopeProvider.onDidChangeScopeSupport(fake);
2831

@@ -51,7 +54,7 @@ export async function runCustomRegexScopeInfoTest() {
5154
await unlink(spokenFormsJsonPath);
5255
// Sleep to ensure that the scope support provider has time to update
5356
// before the next test starts
54-
await sleep(50);
57+
await sleep(250);
5558
} catch (e) {
5659
// Do nothing
5760
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import { getCursorlessApi } from "@cursorless/vscode-common";
2+
import { LATEST_VERSION, ScopeTypeInfo, sleep } from "@cursorless/common";
3+
import * as sinon from "sinon";
4+
import {
5+
assertCalled,
6+
assertCalledWithScopeInfo,
7+
} from "./assertCalledWithScopeInfo";
8+
import { stat, unlink, writeFile } from "fs/promises";
9+
import { sleepWithBackoff } from "../../endToEndTestSetup";
10+
11+
/**
12+
* Tests that the scope provider correctly reports custom spoken forms
13+
*/
14+
export async function runCustomSpokenFormScopeInfoTest() {
15+
const { scopeProvider, spokenFormsJsonPath } = (await getCursorlessApi())
16+
.testHelpers!;
17+
const fake = sinon.fake<[scopeInfos: ScopeTypeInfo[]], void>();
18+
19+
const disposable = scopeProvider.onDidChangeScopeInfo(fake);
20+
21+
try {
22+
await assertCalled(
23+
fake,
24+
[
25+
roundStandard,
26+
namedFunctionStandard,
27+
lambdaStandard,
28+
statementStandard,
29+
squareStandard,
30+
subjectStandard,
31+
],
32+
[],
33+
);
34+
35+
await writeFile(
36+
spokenFormsJsonPath,
37+
JSON.stringify(spokenFormJsonContents),
38+
);
39+
await sleepWithBackoff(50);
40+
await assertCalledWithScopeInfo(
41+
fake,
42+
subjectCustom,
43+
roundCustom,
44+
namedFunctionCustom,
45+
lambdaCustom,
46+
statementMissing,
47+
squareMissing,
48+
);
49+
50+
await unlink(spokenFormsJsonPath);
51+
await sleepWithBackoff(50);
52+
await assertCalled(
53+
fake,
54+
[
55+
roundStandard,
56+
namedFunctionStandard,
57+
lambdaStandard,
58+
statementStandard,
59+
squareStandard,
60+
subjectStandard,
61+
],
62+
[],
63+
);
64+
} finally {
65+
disposable.dispose();
66+
67+
// Delete spokenFormsJsonPath if it exists
68+
try {
69+
await stat(spokenFormsJsonPath);
70+
await unlink(spokenFormsJsonPath);
71+
// Sleep to ensure that the scope support provider has time to update
72+
// before the next test starts
73+
await sleep(250);
74+
} catch (e) {
75+
// Do nothing
76+
}
77+
}
78+
}
79+
80+
const spokenFormJsonContents = {
81+
version: LATEST_VERSION,
82+
entries: [
83+
{
84+
type: "pairedDelimiter",
85+
id: "parentheses",
86+
spokenForms: ["custom round", "alternate custom round"],
87+
},
88+
{
89+
type: "simpleScopeTypeType",
90+
id: "switchStatementSubject",
91+
spokenForms: ["custom subject"],
92+
},
93+
{
94+
type: "simpleScopeTypeType",
95+
id: "namedFunction",
96+
spokenForms: ["custom funk"],
97+
},
98+
{
99+
type: "simpleScopeTypeType",
100+
id: "anonymousFunction",
101+
spokenForms: [],
102+
},
103+
],
104+
};
105+
106+
const subjectStandard: ScopeTypeInfo = {
107+
humanReadableName: "switch statement subject",
108+
isLanguageSpecific: true,
109+
scopeType: { type: "switchStatementSubject" },
110+
spokenForm: {
111+
isSecret: true,
112+
reason:
113+
"simple scope type type with id switchStatementSubject; please see https://www.cursorless.org/docs/user/customization/ for more information",
114+
requiresTalonUpdate: false,
115+
type: "error",
116+
},
117+
};
118+
119+
const subjectCustom: ScopeTypeInfo = {
120+
humanReadableName: "switch statement subject",
121+
isLanguageSpecific: true,
122+
scopeType: { type: "switchStatementSubject" },
123+
spokenForm: {
124+
alternatives: [],
125+
preferred: "custom subject",
126+
type: "success",
127+
},
128+
};
129+
130+
const roundStandard: ScopeTypeInfo = {
131+
humanReadableName: "Matching pair of parentheses",
132+
isLanguageSpecific: false,
133+
scopeType: { type: "surroundingPair", delimiter: "parentheses" },
134+
spokenForm: {
135+
alternatives: [],
136+
preferred: "round",
137+
type: "success",
138+
},
139+
};
140+
141+
const roundCustom: ScopeTypeInfo = {
142+
humanReadableName: "Matching pair of parentheses",
143+
isLanguageSpecific: false,
144+
scopeType: { type: "surroundingPair", delimiter: "parentheses" },
145+
spokenForm: {
146+
alternatives: ["alternate custom round"],
147+
preferred: "custom round",
148+
type: "success",
149+
},
150+
};
151+
152+
const squareStandard: ScopeTypeInfo = {
153+
humanReadableName: "Matching pair of square brackets",
154+
isLanguageSpecific: false,
155+
scopeType: { type: "surroundingPair", delimiter: "squareBrackets" },
156+
spokenForm: {
157+
alternatives: [],
158+
preferred: "box",
159+
type: "success",
160+
},
161+
};
162+
163+
const squareMissing: ScopeTypeInfo = {
164+
humanReadableName: "Matching pair of square brackets",
165+
isLanguageSpecific: false,
166+
scopeType: { type: "surroundingPair", delimiter: "squareBrackets" },
167+
spokenForm: {
168+
isSecret: false,
169+
reason:
170+
"paired delimiter with id squareBrackets; please see https://www.cursorless.org/docs/user/customization/ for more information",
171+
requiresTalonUpdate: true,
172+
type: "error",
173+
},
174+
};
175+
176+
const namedFunctionStandard: ScopeTypeInfo = {
177+
humanReadableName: "named function",
178+
isLanguageSpecific: true,
179+
scopeType: { type: "namedFunction" },
180+
spokenForm: {
181+
alternatives: [],
182+
preferred: "funk",
183+
type: "success",
184+
},
185+
};
186+
187+
const namedFunctionCustom: ScopeTypeInfo = {
188+
humanReadableName: "named function",
189+
isLanguageSpecific: true,
190+
scopeType: { type: "namedFunction" },
191+
spokenForm: {
192+
alternatives: [],
193+
preferred: "custom funk",
194+
type: "success",
195+
},
196+
};
197+
198+
const lambdaStandard: ScopeTypeInfo = {
199+
humanReadableName: "anonymous function",
200+
isLanguageSpecific: true,
201+
scopeType: { type: "anonymousFunction" },
202+
spokenForm: {
203+
alternatives: [],
204+
preferred: "lambda",
205+
type: "success",
206+
},
207+
};
208+
209+
const lambdaCustom: ScopeTypeInfo = {
210+
humanReadableName: "anonymous function",
211+
isLanguageSpecific: true,
212+
scopeType: { type: "anonymousFunction" },
213+
spokenForm: {
214+
isSecret: false,
215+
reason:
216+
"simple scope type type with id anonymousFunction; please see https://www.cursorless.org/docs/user/customization/ for more information",
217+
requiresTalonUpdate: false,
218+
type: "error",
219+
},
220+
};
221+
222+
const statementStandard: ScopeTypeInfo = {
223+
humanReadableName: "statement",
224+
isLanguageSpecific: true,
225+
scopeType: { type: "statement" },
226+
spokenForm: {
227+
alternatives: [],
228+
preferred: "state",
229+
type: "success",
230+
},
231+
};
232+
233+
const statementMissing: ScopeTypeInfo = {
234+
humanReadableName: "statement",
235+
isLanguageSpecific: true,
236+
scopeType: { type: "statement" },
237+
spokenForm: {
238+
isSecret: false,
239+
reason:
240+
"simple scope type type with id statement; please see https://www.cursorless.org/docs/user/customization/ for more information",
241+
requiresTalonUpdate: true,
242+
type: "error",
243+
},
244+
};

0 commit comments

Comments
 (0)