Skip to content

Commit 235b1b9

Browse files
authored
Fix test case recorder (#1341)
The test case recorder broke in #1281 due to the fact we're now launching extension in subdirectory called `dist` rather than top-level repo. This PR fixes it ## Checklist - [x] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [ ] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [ ] I have not broken the cheatsheet
1 parent 3193c76 commit 235b1b9

File tree

7 files changed

+179
-25
lines changed

7 files changed

+179
-25
lines changed

packages/common/src/ide/fake/FakeIDE.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default class FakeIDE implements IDE {
3535
workspaceFolders: readonly WorkspaceFolder[] | undefined = undefined;
3636
private disposables: Disposable[] = [];
3737
private assetsRoot_: string | undefined;
38+
private quickPickReturnValue: string | undefined = undefined;
3839

3940
async flashRanges(_flashDescriptors: FlashDescriptor[]): Promise<void> {
4041
// empty
@@ -93,11 +94,15 @@ export default class FakeIDE implements IDE {
9394
throw Error("Not implemented");
9495
}
9596

96-
public showQuickPick(
97+
public setQuickPickReturnValue(value: string | undefined) {
98+
this.quickPickReturnValue = value;
99+
}
100+
101+
public async showQuickPick(
97102
_items: readonly string[],
98103
_options?: QuickPickOptions,
99104
): Promise<string | undefined> {
100-
throw new Error("Method not implemented.");
105+
return this.quickPickReturnValue;
101106
}
102107

103108
public showInputBox(_options?: any): Promise<string | undefined> {

packages/common/src/ide/normalized/NormalizedIDE.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import FakeIDE from "../fake/FakeIDE";
77
import PassthroughIDEBase from "../PassthroughIDEBase";
88
import { FlashDescriptor } from "../types/FlashDescriptor";
99
import type { IDE } from "../types/ide.types";
10+
import { QuickPickOptions } from "../types/QuickPickOptions";
1011

1112
export class NormalizedIDE extends PassthroughIDEBase {
1213
configuration: FakeConfiguration;
@@ -15,7 +16,7 @@ export class NormalizedIDE extends PassthroughIDEBase {
1516

1617
constructor(
1718
original: IDE,
18-
private fakeIde: FakeIDE,
19+
public fakeIde: FakeIDE,
1920
private isSilent: boolean,
2021
) {
2122
super(original);
@@ -61,4 +62,13 @@ export class NormalizedIDE extends PassthroughIDEBase {
6162
? this.fakeIde.setHighlightRanges(highlightId, editor, ranges)
6263
: super.setHighlightRanges(highlightId, editor, ranges);
6364
}
65+
66+
public async showQuickPick(
67+
_items: readonly string[],
68+
_options?: QuickPickOptions,
69+
): Promise<string | undefined> {
70+
return this.isSilent
71+
? this.fakeIde.showQuickPick(_items, _options)
72+
: super.showQuickPick(_items, _options);
73+
}
6474
}

packages/common/src/testUtil/getFixturePaths.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ export function getFixturePath(fixturePath: string) {
1717
return path.join(getFixturesPath(), fixturePath);
1818
}
1919

20+
export function getRecordedTestsDirPath() {
21+
return path.join(getFixturesPath(), "recorded");
22+
}
23+
2024
export function getRecordedTestPaths() {
21-
const directory = path.join(getFixturesPath(), "recorded");
25+
const directory = getRecordedTestsDirPath();
2226

2327
return walkFilesSync(directory).filter(
2428
(path) => path.endsWith(".yml") || path.endsWith(".yaml"),

packages/cursorless-engine/src/testCaseRecorder/TestCaseRecorder.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import {
1515
toLineRange,
1616
walkDirsSync,
1717
TestCaseCommand,
18+
showError,
1819
} from "@cursorless/common";
1920
import * as fs from "fs";
20-
import { readFile } from "fs/promises";
21+
import { access, readFile } from "fs/promises";
2122
import { invariant } from "immutability-helper";
2223
import { merge } from "lodash";
2324
import * as path from "path";
@@ -79,8 +80,6 @@ const TIMING_CALIBRATION_HIGHLIGHT_ID = "timingCalibration";
7980

8081
export class TestCaseRecorder {
8182
private active: boolean = false;
82-
private workspacePath: string | null;
83-
private workspaceName: string | null;
8483
private fixtureRoot: string | null;
8584
private targetDirectory: string | null = null;
8685
private testCase: TestCase | null = null;
@@ -100,18 +99,14 @@ export class TestCaseRecorder {
10099
constructor(private graph: Graph) {
101100
const { runMode, assetsRoot, workspaceFolders } = ide();
102101

103-
this.workspacePath =
104-
runMode === "development"
105-
? assetsRoot
102+
const workspacePath =
103+
runMode === "development" || runMode === "test"
104+
? path.join(assetsRoot, "../../..")
106105
: workspaceFolders?.[0].uri.path ?? null;
107106

108-
this.workspaceName = this.workspacePath
109-
? path.basename(this.workspacePath)
110-
: null;
111-
112-
this.fixtureRoot = this.workspacePath
107+
this.fixtureRoot = workspacePath
113108
? path.join(
114-
this.workspacePath,
109+
workspacePath,
115110
"packages/cursorless-vscode-e2e/src/suite/fixtures/recorded",
116111
)
117112
: null;
@@ -377,14 +372,16 @@ export class TestCaseRecorder {
377372
}
378373

379374
private async promptSubdirectory(): Promise<string | undefined> {
380-
if (
381-
this.workspaceName == null ||
382-
this.fixtureRoot == null ||
383-
!["cursorless-vscode", "cursorless"].includes(this.workspaceName)
384-
) {
385-
throw new Error(
386-
'"Cursorless record" must be run from within cursorless directory',
387-
);
375+
try {
376+
if (this.fixtureRoot == null) {
377+
throw Error();
378+
}
379+
await access(this.fixtureRoot);
380+
} catch (err) {
381+
const errorMessage =
382+
'"Cursorless record" must be run from within cursorless directory';
383+
showError(ide().messages, "promptSubdirectoryError", errorMessage);
384+
throw new Error(errorMessage);
388385
}
389386

390387
const subdirectorySelection = await ide().showQuickPick(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
languageId: plaintext
2+
command:
3+
version: 4
4+
spokenForm: take harp
5+
action: {name: setSelection}
6+
targets:
7+
- type: primitive
8+
mark: {type: decoratedSymbol, symbolColor: default, character: h}
9+
usePrePhraseSnapshot: false
10+
initialState:
11+
documentContents: hello world
12+
selections:
13+
- anchor: {line: 0, character: 11}
14+
active: {line: 0, character: 11}
15+
marks:
16+
default.h:
17+
start: {line: 0, character: 0}
18+
end: {line: 0, character: 5}
19+
finalState:
20+
documentContents: hello world
21+
selections:
22+
- anchor: {line: 0, character: 0}
23+
active: {line: 0, character: 5}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
getFixturePath,
3+
getRecordedTestsDirPath,
4+
HatTokenMap,
5+
} from "@cursorless/common";
6+
import {
7+
getCursorlessApi,
8+
openNewEditor,
9+
runCursorlessCommand,
10+
} from "@cursorless/vscode-common";
11+
import { assert } from "chai";
12+
import * as crypto from "crypto";
13+
import { mkdir, readdir, readFile, rm } from "fs/promises";
14+
import * as path from "path";
15+
import * as os from "os";
16+
import { basename } from "path";
17+
import * as vscode from "vscode";
18+
import { endToEndTestSetup } from "../endToEndTestSetup";
19+
20+
// Ensure that the test case recorder works
21+
suite("testCaseRecorder", async function () {
22+
endToEndTestSetup(this);
23+
24+
test("no args", testCaseRecorderNoArgs);
25+
test("path arg", testCaseRecorderPathArg);
26+
});
27+
28+
async function testCaseRecorderNoArgs() {
29+
const {
30+
hatTokenMap,
31+
ide: { fakeIde },
32+
} = (await getCursorlessApi()).testHelpers!;
33+
const dirName = crypto.randomBytes(16).toString("hex");
34+
fakeIde.setQuickPickReturnValue(dirName);
35+
const tmpdir = path.join(getRecordedTestsDirPath(), dirName);
36+
37+
try {
38+
await runAndCheckTestCaseRecorder(hatTokenMap, tmpdir);
39+
} finally {
40+
fakeIde.setQuickPickReturnValue(undefined);
41+
await rm(tmpdir, { recursive: true, force: true });
42+
}
43+
}
44+
45+
async function testCaseRecorderPathArg() {
46+
const { hatTokenMap } = (await getCursorlessApi()).testHelpers!;
47+
const tmpdir = path.join(os.tmpdir(), crypto.randomBytes(16).toString("hex"));
48+
await mkdir(tmpdir, { recursive: true });
49+
50+
try {
51+
await runAndCheckTestCaseRecorder(hatTokenMap, tmpdir, {
52+
directory: tmpdir,
53+
});
54+
} finally {
55+
await rm(tmpdir, { recursive: true, force: true });
56+
}
57+
}
58+
59+
async function runAndCheckTestCaseRecorder(
60+
hatTokenMap: HatTokenMap,
61+
tmpdir: string,
62+
...extraArgs: unknown[]
63+
) {
64+
const editor = await openNewEditor("hello world");
65+
66+
editor.selections = [new vscode.Selection(0, 11, 0, 11)];
67+
68+
await hatTokenMap.allocateHats();
69+
70+
await vscode.commands.executeCommand(
71+
"cursorless.recordTestCase",
72+
...extraArgs,
73+
);
74+
75+
await runCursorlessCommand({
76+
version: 4,
77+
action: { name: "setSelection" },
78+
targets: [
79+
{
80+
type: "primitive",
81+
mark: {
82+
type: "decoratedSymbol",
83+
symbolColor: "default",
84+
character: "h",
85+
},
86+
},
87+
],
88+
usePrePhraseSnapshot: false,
89+
spokenForm: "take harp",
90+
});
91+
92+
await vscode.commands.executeCommand("cursorless.recordTestCase");
93+
94+
const paths = await readdir(tmpdir);
95+
assert.lengthOf(paths, 1);
96+
97+
const actualRecordedTestPath = paths[0];
98+
assert.equal(basename(actualRecordedTestPath), "takeHarp.yml");
99+
100+
const expected = (
101+
await readFile(
102+
getFixturePath("recorded/testCaseRecorder/takeHarp.yml"),
103+
"utf8",
104+
)
105+
)
106+
// We use this to ensure that the test works on Windows. Depending on user
107+
// / CI git config, the file might be checked out with CRLF line endings
108+
.replaceAll("\r\n", "\n");
109+
const actualRecordedTest = await readFile(
110+
path.join(tmpdir, actualRecordedTestPath),
111+
"utf8",
112+
);
113+
114+
assert.equal(actualRecordedTest, expected);
115+
}

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"moduleResolution": "nodenext",
55
"moduleDetection": "force",
66
"target": "es6",
7-
"lib": ["es2020"],
7+
"lib": ["es2022"],
88
"sourceMap": true,
99
"declarationMap": true,
1010
"resolveJsonModule": true,

0 commit comments

Comments
 (0)