Skip to content

Commit

Permalink
feat: add terminal trace support (#20)
Browse files Browse the repository at this point in the history
* feat: add terminal trace support

Signed-off-by: Chapman Pendery <[email protected]>

* fix: readline/promises not supported in node16

Signed-off-by: Chapman Pendery <[email protected]>

---------

Signed-off-by: Chapman Pendery <[email protected]>
  • Loading branch information
cpendery committed Mar 7, 2024
1 parent daf7c68 commit 269030e
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules/
lib/
.tui-test/
*.tgz
t*.md
t*.md
tui-traces/
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ test("make a regex assertion", async ({ terminal }) => {
});
```

## Traces

TUI Test provides a rich tracing system to help you debug and diagnose issues with your tests. You can enable traces by setting the `trace` value in your `tui-test.config.ts` to `true` or by running the cli with the `-t/--trace` flag.

Traces are contain a replay of everything the terminal received and can be used to diagnose issues with your tests, especially when issues happen on different machines. Traces are stored in by default in the `tui-traces` folder in the root of your project and can be replayed via the `show-trace` command.

## Configuration

TUI Test can be configured via the `tui-test.config.[ts|js]` file in the root of your project. The following is an example of a configuration file:

```ts
import { defineConfig } from "@microsoft/tui-test";

export default defineConfig({
retries: 3,
trace: true
});

```

## Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a
Expand Down
9 changes: 7 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ import { Command } from "commander";
import { getVersion } from "./version.js";
import { executableName } from "../utils/constants.js";
import { run } from "../runner/runner.js";
import showTrace from "./show-trace.js";

type CommandOptions = {
updateSnapshot: boolean | undefined;
trace: boolean | undefined;
};

const action = async (
testFilter: string[] | undefined,
options: CommandOptions
) => {
const { updateSnapshot } = options;
await run({ updateSnapshot: updateSnapshot ?? false, testFilter });
const { updateSnapshot, trace } = options;
await run({ updateSnapshot: updateSnapshot ?? false, testFilter, trace });
};

export const program = new Command();
Expand All @@ -35,6 +37,9 @@ Examples:
"Pass an argument to filter test files. Each argument is treated as a regular expression. Matching is performed against the absolute file paths"
)
.option("-u, --updateSnapshot", `use this flag to re-record snapshots`)
.option("-t, --trace", `enable traces for test execution`)
.version(await getVersion(), "-v, --version", "output the current version")
.action(action)
.showHelpAfterError("(add --help for additional information)");

program.addCommand(showTrace);
20 changes: 20 additions & 0 deletions src/cli/show-trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Command } from "commander";

import { loadTrace } from "../trace/tracer.js";
import { play } from "../trace/viewer.js";

const action = async (traceFile: string) => {
const trace = await loadTrace(traceFile);
await play(trace);
};

const cmd = new Command("show-trace")
.description(`view traces in the console`)
.argument("<trace-file>", "the trace to replay in the terminal");

cmd.action(action);

export default cmd;
37 changes: 37 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const loadConfig = async (): Promise<Required<TestConfig>> => {
userConfig.workers ?? Math.max(Math.floor(os.cpus().length / 2), 1),
1
),
trace: userConfig.trace ?? false,
traceFolder:
userConfig.traceFolder ?? path.join(process.cwd(), "tui-traces"),
use: {
shell: userConfig.use?.shell ?? defaultShell,
rows: userConfig.use?.rows ?? 30,
Expand Down Expand Up @@ -220,6 +223,40 @@ export declare type TestConfig = {
*/
workers?: number;

/**
* Record each test run for replay.
*
* **Usage**
*
* ```js
* // tui-test.config.ts
* import { defineConfig } from '@microsoft/tui-test';
*
* export default defineConfig({
* trace: true,
* });
* ```
*
*/
trace?: boolean;

/**
* Folder to store the traces in. Defaults to `tui-traces`
*
* **Usage**
*
* ```js
* // tui-test.config.ts
* import { defineConfig } from '@microsoft/tui-test';
*
* export default defineConfig({
* traceFolder: "tui-traces",
* });
* ```
*
*/
traceFolder?: string;

/**
* TUI Test supports running multiple test projects at the same time.
*
Expand Down
19 changes: 16 additions & 3 deletions src/runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import chalk from "chalk";

import { Suite, getRootSuite } from "../test/suite.js";
import { transformFiles } from "./transform.js";
import { getRetries, getTimeout, loadConfig } from "../config/config.js";
import {
TestConfig,
getRetries,
getTimeout,
loadConfig,
} from "../config/config.js";
import { runTestWorker } from "./worker.js";
import { Shell, setupZshDotfiles } from "../terminal/shell.js";
import { ListReporter } from "../reporter/list.js";
Expand All @@ -28,6 +33,7 @@ declare global {

type ExecutionOptions = {
updateSnapshot: boolean;
trace?: boolean;
testFilter?: string[];
};

Expand All @@ -52,9 +58,12 @@ const runSuites = async (
allSuites: Suite[],
filteredTestIds: Set<string>,
reporter: BaseReporter,
{ updateSnapshot }: ExecutionOptions,
options: ExecutionOptions,
config: Required<TestConfig>,
pool: Pool
) => {
const { updateSnapshot } = options;
const trace = options.trace ?? config.trace;
const tasks: Promise<void>[] = [];
const suites = [...allSuites];
while (suites.length != 0) {
Expand All @@ -70,8 +79,11 @@ const runSuites = async (
test,
test.sourcePath()!,
{ timeout: getTimeout(), updateSnapshot },
trace,
pool,
reporter
reporter,
i,
config.traceFolder
);
test.results.push(testResult);
reporter.endTest(test, testResult);
Expand Down Expand Up @@ -237,6 +249,7 @@ export const run = async (options: ExecutionOptions) => {
new Set(allTests.map((test) => test.id)),
reporter,
options,
config,
pool
);
try {
Expand Down
62 changes: 51 additions & 11 deletions src/runner/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import process from "node:process";
import { EventEmitter } from "node:events";
import workerpool from "workerpool";

import { Suite } from "../test/suite.js";
Expand All @@ -12,6 +13,7 @@ import { expect } from "../test/test.js";
import { BaseReporter } from "../reporter/base.js";
import { poll } from "../utils/poll.js";
import { flushSnapshotExecutionCache } from "../test/matchers/toMatchSnapshot.js";
import { saveTrace, TracePoint } from "../trace/tracer.js";

type WorkerResult = {
error?: string;
Expand All @@ -33,6 +35,8 @@ const runTest = async (
testId: string,
testSuite: Suite,
updateSnapshot: boolean,
trace: boolean,
tracePoints: TracePoint[],
importPath: string
) => {
process.setSourceMapsEnabled(true);
Expand All @@ -45,13 +49,24 @@ const runTest = async (
}
const test = globalThis.tests[testId];
const { shell, rows, columns, env, program } = test.suite.options ?? {};
const terminal = await spawn({
shell: shell ?? defaultShell,
rows: rows ?? 30,
cols: columns ?? 80,
env,
program,
});
const traceEmitter = new EventEmitter();
traceEmitter.on("data", (data: string, time: number) =>
tracePoints.push({ data, time })
);
traceEmitter.on("size", (rows: number, cols: number) =>
tracePoints.push({ rows, cols })
);
const terminal = await spawn(
{
shell: shell ?? defaultShell,
rows: rows ?? 30,
cols: columns ?? 80,
env,
program,
},
trace,
traceEmitter
);

const allTests = Object.values(globalThis.tests);
const testPath = test.filePath();
Expand Down Expand Up @@ -102,8 +117,11 @@ export async function runTestWorker(
test: TestCase,
importPath: string,
{ timeout, updateSnapshot }: WorkerExecutionOptions,
trace: boolean,
pool: workerpool.Pool,
reporter: BaseReporter
reporter: BaseReporter,
attempt: number,
traceFolder: string
): Promise<WorkerResult> {
const snapshots: Snapshot[] = [];
if (test.expectedStatus === "skipped") {
Expand All @@ -127,7 +145,15 @@ export async function runTestWorker(
try {
const poolPromise = pool.exec(
"testWorker",
[test.id, getMockSuite(test), updateSnapshot, importPath],
[
test.id,
getMockSuite(test),
updateSnapshot,
trace,
importPath,
attempt,
traceFolder,
],
{
on: (payload) => {
if (payload.stdout) {
Expand Down Expand Up @@ -243,16 +269,27 @@ const testWorker = async (
testId: string,
testSuite: Suite,
updateSnapshot: boolean,
importPath: string
trace: boolean,
importPath: string,
attempt: number,
traceFolder: string
): Promise<void> => {
flushSnapshotExecutionCache();

const startTime = Date.now();
const tracePoints = [{ data: "", time: startTime }];
workerpool.workerEmit({
startTime,
});
try {
await runTest(testId, testSuite, updateSnapshot, importPath);
await runTest(
testId,
testSuite,
updateSnapshot,
trace,
tracePoints,
importPath
);
} catch (e) {
let errorMessage;
if (typeof e == "string") {
Expand All @@ -268,6 +305,9 @@ const testWorker = async (
});
}
}
if (trace) {
await saveTrace(tracePoints, testId, attempt, traceFolder);
}
};

if (!workerpool.isMainThread) {
Expand Down
11 changes: 11 additions & 0 deletions src/terminal/ansi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

const ESC = "\u001B";
const CSI = "\u001B[";
const SEP = ";";

const keyUp = CSI + "A";
const keyDown = CSI + "B";
Expand All @@ -14,6 +15,12 @@ const keyBackspace = "\u007F";
const keyDelete = CSI + "3~";
const keyCtrlC = String.fromCharCode(3);
const keyCtrlD = String.fromCharCode(4);
const saveScreen = CSI + "?47h";
const restoreScreen = CSI + "?47l";
const clearScreen = CSI + "2J";
const cursorTo = (x: number, y: number) => {
return CSI + (y + 1) + SEP + (x + 1) + "H";
};

export default {
keyUp,
Expand All @@ -25,4 +32,8 @@ export default {
keyDelete,
keyCtrlC,
keyCtrlD,
saveScreen,
restoreScreen,
clearScreen,
cursorTo,
};
Loading

0 comments on commit 269030e

Please sign in to comment.