Skip to content
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

Allow for assertions against intentional non-match #705

Merged
merged 11 commits into from
Jun 3, 2022
8 changes: 7 additions & 1 deletion docs/contributing/test-case-recorder.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ command run, and the final state, all in the form of a yaml document. See
1. Add a voice command for recording to your personal talon files:
- `cursorless record: user.vscode("cursorless.recordTestCase")`
- We don't want to commit this so add it to your own repository.
1. If you'd like to be able to do tests which check the navigation map, you should also add the following to your personal talon files:
1. If you'd like to be able to record tests which check the navigation map, please add the following to your personal talon files:

- https://github.com/pokey/pokey_talon/blob/9298c25dd6d28fd9fcf5ed39f305bc6b93e5f229/apps/vscode/vscode.talon#L468
- https://github.com/pokey/pokey_talon/blob/49643bfa8f62cbec18b5ddad1658f5a28785eb01/apps/vscode/vscode.py#L203-L205

It is quite unlikely you'll need this second step. Most tests don't check the navigation map.

1. If you'd like to be able to record tests which assert on non-matches, please add another command to your personal talon files. See the two files links above for context. Add the command below to your to your `vscode.py` and ensure that there is a matching Talon command.

```
actions.user.vscode_with_plugin("cursorless.recordTestCase", {"isErrorTest": True})
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
```

## Recording new tests

1. Start debugging (F5)
Expand Down
6 changes: 3 additions & 3 deletions src/core/commandRunner/CommandRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ export default class CommandRunner {
getNodeAtLocation: this.graph.getNodeAtLocation,
};

const selections = processTargets(processedTargetsContext, targets);

if (this.graph.testCaseRecorder.isActive()) {
const context = {
targets,
Expand All @@ -122,6 +120,8 @@ export default class CommandRunner {
);
}

const selections = processTargets(processedTargetsContext, targets);

const {
returnValue,
thatMark: newThatMark,
Expand All @@ -137,7 +137,7 @@ export default class CommandRunner {

return returnValue;
} catch (e) {
this.graph.testCaseRecorder.commandErrorHook();
await this.graph.testCaseRecorder.commandErrorHook(e as Error);
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
const err = e as Error;
if ((err as Error).name === "ActionableError") {
(err as ActionableError).showErrorMessage();
Expand Down
19 changes: 19 additions & 0 deletions src/test/suite/fixtures/recorded/nonMatchErrors/takeFunk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
languageId: ruby
command:
version: 1
spokenForm: take funk
pokey marked this conversation as resolved.
Show resolved Hide resolved
action: setSelection
targets:
- type: primitive
modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}
initialState:
documentContents: |2
[1, 2, 3, [4, 5, 6]]
selections:
- anchor: {line: 0, character: 11}
active: {line: 0, character: 11}
marks: {}
finalState: null
returnValue: null
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: namedFunction, includeSiblings: false}, isImplicit: false}]
thrownError: Error
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 19 additions & 0 deletions src/test/suite/fixtures/recorded/nonMatchErrors/takeList.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
languageId: ruby
command:
version: 1
spokenForm: take list
action: setSelection
targets:
- type: primitive
modifier: {type: containingScope, scopeType: list, includeSiblings: false}
initialState:
documentContents: |2
[1, 2, 3, [4, 5, 6]]
selections:
- anchor: {line: 0, character: 0}
pokey marked this conversation as resolved.
Show resolved Hide resolved
active: {line: 0, character: 0}
marks: {}
finalState: null
returnValue: null
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: list, includeSiblings: false}, isImplicit: false}]
thrownError: Error
13 changes: 10 additions & 3 deletions src/test/suite/recorded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,25 @@ async function runTest(file: string) {

// Assert that recorded decorations are present
checkMarks(fixture.initialState.marks, readableHatMap);

if (fixture.thrownError) {
return assert.rejects(async () => {
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
await vscode.commands.executeCommand(
"cursorless.command",
fixture.command
);
});
}
const returnValue = await vscode.commands.executeCommand(
"cursorless.command",
fixture.command
);

const marks =
fixture.finalState.marks == null
fixture.finalState!.marks == null
? undefined
: marksToPlainObject(
extractTargetedMarks(
Object.keys(fixture.finalState.marks) as string[],
Object.keys(fixture.finalState!.marks) as string[],
readableHatMap
)
);
Expand Down
16 changes: 14 additions & 2 deletions src/testUtil/TestCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export type TestCaseContext = {
hatTokenMap: ReadOnlyHatMap;
};

export type ErrorInfo = {
name: string;
};

export type TestCaseFixture = {
languageId: string;
command: TestCaseCommand;
Expand All @@ -36,7 +40,10 @@ export type TestCaseFixture = {
marksToCheck?: string[];

initialState: TestCaseSnapshot;
finalState: TestCaseSnapshot;
/** The final state after a command is issued. If we are testing a non-match(error) case this is null. */
finalState: TestCaseSnapshot | null;
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
/** Used to assert if an error has been thrown. */
thrownError?: string;
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
returnValue: unknown;
/** Inferred full targets added for context; not currently used in testing */
fullTargets: Target[];
Expand All @@ -47,6 +54,7 @@ export class TestCase {
fullTargets: Target[];
initialState: TestCaseSnapshot | null = null;
finalState: TestCaseSnapshot | null = null;
thrownError: ErrorInfo | null = null;
returnValue: unknown = null;
targetKeys: string[];
private _awaitingFinalMarkInfo: boolean;
Expand Down Expand Up @@ -132,7 +140,10 @@ export class TestCase {
}

toYaml() {
if (this.initialState == null || this.finalState == null) {
if (
this.initialState == null ||
(this.finalState == null && this.thrownError == null)
) {
throw Error("Two snapshots must be taken before serializing");
}
const fixture: TestCaseFixture = {
Expand All @@ -143,6 +154,7 @@ export class TestCase {
finalState: this.finalState,
returnValue: this.returnValue,
fullTargets: this.fullTargets,
thrownError: this.thrownError?.name,
};
return serialize(fixture);
}
Expand Down
23 changes: 18 additions & 5 deletions src/testUtil/TestCaseRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ interface RecordTestCaseCommandArg {
* Whether to flash a background for calibrating a video recording
*/
showCalibrationDisplay?: boolean;

/**
* Whether we should record a test which yields an error
*/

isErrorTest?: boolean;
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
}

export class TestCaseRecorder {
Expand All @@ -62,6 +68,7 @@ export class TestCaseRecorder {
private startTimestamp?: bigint;
private extraSnapshotFields?: ExtraSnapshotField[];
private paused: boolean = false;
private isErrorTest: boolean = false;
private calibrationStyle = vscode.window.createTextEditorDecorationType({
backgroundColor: CALIBRATION_DISPLAY_BACKGROUND_COLOR,
});
Expand Down Expand Up @@ -168,6 +175,7 @@ export class TestCaseRecorder {
isSilent = false,
extraSnapshotFields = [],
showCalibrationDisplay = false,
isErrorTest = false,
} = arg ?? {};

if (directory != null) {
Expand All @@ -187,6 +195,7 @@ export class TestCaseRecorder {
this.isHatTokenMapTest = isHatTokenMapTest;
this.isSilent = isSilent;
this.extraSnapshotFields = extraSnapshotFields;
this.isErrorTest = isErrorTest;
this.paused = false;

vscode.window.showInformationMessage(
Expand Down Expand Up @@ -248,10 +257,9 @@ export class TestCaseRecorder {
// command for a navigation map test
return;
}
await this.testCase!.recordFinalState(returnValue);
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-add

await this.testCase.recordFinalState(returnValue);

if (this.testCase.awaitingFinalMarkInfo) {
if (this.testCase!.awaitingFinalMarkInfo) {
Will-Sommers marked this conversation as resolved.
Show resolved Hide resolved
// We don't finish the test case here in the case of a navigation map
// test because we'll do it after we get the follow up command indicating
// which marks we wanted to track
Expand Down Expand Up @@ -351,8 +359,13 @@ export class TestCaseRecorder {
return filePath;
}

commandErrorHook() {
this.testCase = null;
async commandErrorHook(error: Error) {
if (this.isErrorTest && this.testCase) {
this.testCase.thrownError = { name: error.name };
await this.finishTestCase();
} else {
this.testCase = null;
}
}

dispose() {
Expand Down