Skip to content

Commit

Permalink
Add interactive mode to our test runner
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Apr 1, 2024
1 parent edcc653 commit 0ec733b
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 13 deletions.
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Expand Up @@ -11,13 +11,13 @@ case and compare the output to corresponding `.expected.ts` file. If the
output does not match the expected output, the test runner will fail.

The tests in `src/tests/fixtures` are unit tests that test the behavior of the
extraction. They extract GraphQL SDL from the file and write that as output.
extraction and code generation. They extract GraphQL SDL, generated `schema.ts` or any associated errors and code actions from the file and write that as output.

If the test includes a line like `// Locate: User.name` of `// Locate: SomeType`
then the test runner will instead locate the given entity and write the location
as output.

The tests in `src/tests/integrationFixtures` are integration tests that test the _runtime_ behavior of the tool. They expect each file to be a `.ts` file with `@gql` docblock tags which exports a root query class as the named export `Query` and a GraphQL query text under the named export `query`. The test runner will execute the query against the root query class and emit the returned response JSON as the test output.
The tests in `src/tests/integrationFixtures` are integration tests that test the _runtime_ behavior of the generated code. Each directory contains an `index.ts` file with `@gql` docblock tags which exports a root query class as the named export `Query` and a GraphQL query text under the named export `query`. The test runner will execute the query against the root query class and emit the returned response JSON as the test output.

```
Expand All @@ -29,25 +29,25 @@ To run a specific test case, you can use the `--filter` flag and provide a
substring match for the test fixture's path.

```
pnpm run test --filter=import
```

To update fixture files, you can use the `--write` flag.

```
pnpm run test --write
```

Interactive mode will prompt you to update the fixture files one by one for each failing fixture test.

```
pnpm run test --interactive
```

You an also get help with the CLI flags:

```
pnpm run test --help
```

All changes that affect the behavior of the tool, either new features of bug
Expand Down
28 changes: 24 additions & 4 deletions src/tests/TestRunner.ts
@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import { diff } from "jest-diff";
import { ask } from "./yesNo";

type Transformer = (
code: string,
Expand Down Expand Up @@ -47,9 +48,9 @@ export default class TestRunner {
}

// Returns true if the test passed
async run(): Promise<boolean> {
async run({ interactive }: { interactive: boolean }): Promise<boolean> {
for (const fixture of this._testFixtures) {
await this._testFixture(fixture);
await this._testFixture(fixture, { interactive });
}
console.log("");

Expand Down Expand Up @@ -80,7 +81,10 @@ export default class TestRunner {
return true;
}

async _testFixture(fixture: string) {
async _testFixture(
fixture: string,
{ interactive }: { interactive: boolean },
) {
const expectedFileName = fixture + ".expected";
const expectedFilePath = path.join(this._fixturesDir, expectedFileName);
if (this._otherFiles.has(expectedFileName)) {
Expand Down Expand Up @@ -115,7 +119,23 @@ OUTPUT
${actual}`;

if (actualOutput !== expectedContent) {
if (this._write) {
if (interactive) {
console.error("FAILURE: " + displayName);
console.log(diff(expectedContent, actualOutput));
console.log("Fixture did not match.");
console.log(
`(You can rerun just this test with: \`pnpm run test --filter=${fixture}\`)`,
);
const write = await ask(
"Would you like to update this fixture file? (y/n)",
);
if (write) {
console.error("UPDATED: " + displayName);
fs.writeFileSync(expectedFilePath, actualOutput, "utf-8");
} else {
this._failureCount++;
}
} else if (this._write) {
console.error("UPDATED: " + displayName);
fs.writeFileSync(expectedFilePath, actualOutput, "utf-8");
} else {
Expand Down
5 changes: 3 additions & 2 deletions src/tests/test.ts
Expand Up @@ -45,7 +45,8 @@ program
"-f, --filter <FILTER_REGEX>",
"A regex to filter the tests to run. Only tests with a file path matching the regex will be run.",
)
.action(async ({ filter, write }) => {
.option("-i, --interactive", "Run tests in interactive mode.")
.action(async ({ filter, write, interactive }) => {
const filterRegex = filter ?? null;
let failures = false;
for (const {
Expand All @@ -62,7 +63,7 @@ program
ignoreFilePattern,
transformer,
);
failures = !(await runner.run()) || failures;
failures = !(await runner.run({ interactive })) || failures;
}
if (failures) {
process.exit(1);
Expand Down
38 changes: 38 additions & 0 deletions src/tests/yesNo.ts
@@ -0,0 +1,38 @@
// Adapted from https://github.com/tcql/node-yesno/blob/master/yesno.js

import * as readline from "readline";

const options = {
yes: ["yes", "y"],
no: ["no", "n"],
};

export async function ask(question: string): Promise<boolean> {
const yValues = options.yes.map((v) => v.toLowerCase());
const nValues = options.no.map((v) => v.toLowerCase());

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

return new Promise(function (resolve) {
rl.question(question + " ", async function (answer) {
rl.close();

const cleaned = answer.trim().toLowerCase();

if (yValues.indexOf(cleaned) >= 0) return resolve(true);

if (nValues.indexOf(cleaned) >= 0) return resolve(false);

process.stdout.write("\nInvalid Response.\n");
process.stdout.write(
"Answer either yes : (" + yValues.join(", ") + ") \n",
);
process.stdout.write("Or no: (" + nValues.join(", ") + ") \n\n");
const result = await ask(question);
resolve(result);
});
});
}

0 comments on commit 0ec733b

Please sign in to comment.