@tui-sandbox/library 7.2.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.3.0](https://github.com/mikavilpas/tui-sandbox/compare/library-v7.2.1...library-v7.3.0) (2024-11-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * can run a headless neovim ex-command before tests ([700d83c](https://github.com/mikavilpas/tui-sandbox/commit/700d83c6b19875e946b38ef382b9eb48e22cb5f6))
9
+
3
10
  ## [7.2.1](https://github.com/mikavilpas/tui-sandbox/compare/library-v7.2.0...library-v7.2.1) (2024-11-28)
4
11
 
5
12
 
@@ -1,26 +1,111 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import assert from "node:assert";
1
54
  import { stat } from "node:fs/promises";
2
55
  import path from "node:path";
3
56
  import { createCypressSupportFile } from "../server/cypress-support/createCypressSupportFile.js";
4
57
  import { startTestServer, updateTestdirectorySchemaFile } from "../server/index.js";
58
+ import { NeovimApplication } from "../server/neovim/NeovimApplication.js";
59
+ import { prepareNewTestDirectory } from "../server/neovim/index.js";
5
60
  //
6
61
  // This is the main entrypoint to tui-sandbox
7
62
  //
8
- // the arguments passed to this script start at index 2
9
- const args = process.argv.slice(2);
10
- if (args[0] !== "start") {
11
- throw new Error(`Usage: tui start`);
12
- }
13
63
  const outputFileName = "MyTestDirectory.ts";
14
64
  /** The cwd in the user's directory when they are running this script. Not the
15
65
  * cwd of the script itself. */
16
66
  const cwd = process.cwd();
67
+ const config = {
68
+ testEnvironmentPath: path.join(cwd, "test-environment/"),
69
+ outputFilePath: path.join(cwd, outputFileName),
70
+ };
71
+ // the arguments passed to this script start at index 2
72
+ const args = process.argv.slice(2);
73
+ if (args[0] === "neovim") {
74
+ if (!(args[1] === "exec" && args.length === 3)) {
75
+ showUsageAndExit();
76
+ }
77
+ const command = args[2];
78
+ assert(command, "No command provided");
79
+ {
80
+ const env_1 = { stack: [], error: void 0, hasError: false };
81
+ try {
82
+ // automatically dispose of the neovim instance when done
83
+ const app = __addDisposableResource(env_1, new NeovimApplication(config.testEnvironmentPath), true);
84
+ app.events.on("stdout", data => {
85
+ console.log(` neovim output: ${data}`);
86
+ });
87
+ const testDirectory = await prepareNewTestDirectory(config);
88
+ await app.startNextAndKillCurrent(testDirectory, { filename: "empty.txt", headlessCmd: command }, { cols: 80, rows: 24 });
89
+ await app.application.untilExit();
90
+ }
91
+ catch (e_1) {
92
+ env_1.error = e_1;
93
+ env_1.hasError = true;
94
+ }
95
+ finally {
96
+ const result_1 = __disposeResources(env_1);
97
+ if (result_1)
98
+ await result_1;
99
+ }
100
+ }
101
+ process.exit(0);
102
+ }
103
+ if (args[0] !== "start") {
104
+ showUsageAndExit();
105
+ }
17
106
  console.log(`🚀 Starting test server in ${cwd} - this should be the root of your integration-tests directory 🤞🏻`);
18
107
  await stat(path.join(cwd, outputFileName));
19
108
  try {
20
- const config = {
21
- testEnvironmentPath: path.join(cwd, "test-environment/"),
22
- outputFilePath: path.join(cwd, outputFileName),
23
- };
24
109
  await createCypressSupportFile({
25
110
  cypressSupportDirectoryPath: path.join(cwd, "cypress", "support"),
26
111
  supportFileName: "tui-sandbox.ts",
@@ -31,3 +116,12 @@ try {
31
116
  catch (e) {
32
117
  console.error(e);
33
118
  }
119
+ function showUsageAndExit() {
120
+ console.log([
121
+ //
122
+ `Usage (pick one):`,
123
+ ` tui start`,
124
+ ` tui neovim exec '<ex-command>'`,
125
+ ].join("\n"));
126
+ process.exit(1);
127
+ }
@@ -3,12 +3,14 @@ import type { NeovimClient as NeovimApiClient } from "neovim";
3
3
  import type { TestDirectory } from "../types.js";
4
4
  import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication.js";
5
5
  import type { Lazy } from "../utilities/Lazy.js";
6
- export type StdoutMessage = "stdout";
6
+ export type StdoutOrStderrMessage = "stdout";
7
7
  export type StartNeovimGenericArguments = {
8
8
  filename: string | {
9
9
  openInVerticalSplits: string[];
10
10
  };
11
11
  startupScriptModifications?: string[];
12
+ /** Executes the given command with --headless -c <command> -c qa */
13
+ headlessCmd?: string;
12
14
  /** Additions to the environment variables for the Neovim process. These
13
15
  * override any already existing environment variables. */
14
16
  additionalEnvironmentVariables?: Record<string, string> | undefined;
@@ -48,6 +48,12 @@ export class NeovimApplication {
48
48
  neovimArguments.push(filePath);
49
49
  }
50
50
  }
51
+ if (startArgs.headlessCmd) {
52
+ // NOTE: update the doc comment above if this changes
53
+ neovimArguments.push("--headless");
54
+ neovimArguments.push("-c", startArgs.headlessCmd);
55
+ neovimArguments.push("-c", "qa");
56
+ }
51
57
  const id = Math.random().toString().slice(2, 8);
52
58
  const socketPath = `${tmpdir()}/tui-sandbox-nvim-socket-${id}`;
53
59
  neovimArguments.push("--listen", socketPath);
@@ -8,11 +8,11 @@ export function connectNeovimApi(socketPath) {
8
8
  for (let i = 0; i < 100; i++) {
9
9
  try {
10
10
  await access(socketPath);
11
- console.log(`socket file ${socketPath} created after at attempt ${i + 1}`);
11
+ // console.log(`socket file ${socketPath} created after at attempt ${i + 1}`)
12
12
  break;
13
13
  }
14
14
  catch (e) {
15
- console.log(`polling for socket file ${socketPath} to be created (attempt ${i + 1})`);
15
+ // console.log(`polling for socket file ${socketPath} to be created (attempt ${i + 1})`)
16
16
  await new Promise(resolve => setTimeout(resolve, 100));
17
17
  }
18
18
  }
@@ -2,12 +2,15 @@ import "core-js/proposals/async-explicit-resource-management.js";
2
2
  import type { BlockingCommandInput, ExCommandInput, LuaCodeInput } from "../server.js";
3
3
  import type { BlockingShellCommandOutput, RunExCommandOutput, RunLuaCodeOutput, StartNeovimGenericArguments, TestDirectory } from "../types.js";
4
4
  import type { TestServerConfig } from "../updateTestdirectorySchemaFile.js";
5
+ import { Lazy } from "../utilities/Lazy.js";
5
6
  import type { TabId } from "../utilities/tabId.js";
6
7
  import type { TerminalDimensions } from "./NeovimApplication.js";
7
- export declare function onStdout(options: {
8
+ export declare const resources: Lazy<AsyncDisposableStack>;
9
+ export declare function initializeStdout(options: {
8
10
  client: TabId;
9
11
  }, signal: AbortSignal | undefined, testEnvironmentPath: string): Promise<AsyncGenerator<string, void, unknown>>;
10
12
  export declare function start(options: StartNeovimGenericArguments, terminalDimensions: TerminalDimensions, tabId: TabId, config: TestServerConfig): Promise<TestDirectory>;
13
+ export declare function prepareNewTestDirectory(config: TestServerConfig): Promise<TestDirectory>;
11
14
  export declare function sendStdin(options: {
12
15
  tabId: TabId;
13
16
  data: string;
@@ -3,14 +3,21 @@ import { exec } from "child_process";
3
3
  import "core-js/proposals/async-explicit-resource-management.js";
4
4
  import util from "util";
5
5
  import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js";
6
+ import { Lazy } from "../utilities/Lazy.js";
6
7
  import { createTempDir, removeTestDirectories } from "./environment/createTempDir.js";
7
8
  import { NeovimApplication } from "./NeovimApplication.js";
8
9
  const neovims = new Map();
9
- export async function onStdout(options, signal, testEnvironmentPath) {
10
+ export const resources = new Lazy(() => {
11
+ return new AsyncDisposableStack();
12
+ });
13
+ export async function initializeStdout(options, signal, testEnvironmentPath) {
10
14
  const tabId = options.client.tabId;
11
15
  const neovim = neovims.get(tabId) ?? new NeovimApplication(testEnvironmentPath);
12
16
  if (neovims.get(tabId) === undefined) {
13
17
  neovims.set(tabId, neovim);
18
+ resources.get().adopt(neovim, async (n) => {
19
+ await n[Symbol.asyncDispose]();
20
+ });
14
21
  }
15
22
  const stdout = convertEventEmitterToAsyncGenerator(neovim.events, "stdout");
16
23
  if (signal) {
@@ -25,9 +32,13 @@ export async function onStdout(options, signal, testEnvironmentPath) {
25
32
  export async function start(options, terminalDimensions, tabId, config) {
26
33
  const neovim = neovims.get(tabId.tabId);
27
34
  assert(neovim, `Neovim instance not found for client id ${tabId.tabId}`);
35
+ const testDirectory = await prepareNewTestDirectory(config);
36
+ await neovim.startNextAndKillCurrent(testDirectory, options, terminalDimensions);
37
+ return testDirectory;
38
+ }
39
+ export async function prepareNewTestDirectory(config) {
28
40
  await removeTestDirectories(config.testEnvironmentPath);
29
41
  const testDirectory = await createTempDir(config);
30
- await neovim.startNextAndKillCurrent(testDirectory, options, terminalDimensions);
31
42
  return testDirectory;
32
43
  }
33
44
  export async function sendStdin(options) {
@@ -48,7 +48,7 @@ export async function createAppRouter(config) {
48
48
  return neovim.start(options.input.startNeovimArguments, options.input.startNeovimArguments.terminalDimensions, options.input.tabId, config);
49
49
  }),
50
50
  onStdout: trpc.procedure.input(z.object({ client: tabIdSchema })).subscription(options => {
51
- return neovim.onStdout(options.input, options.signal, config.testEnvironmentPath);
51
+ return neovim.initializeStdout(options.input, options.signal, config.testEnvironmentPath);
52
52
  }),
53
53
  sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
54
54
  return neovim.sendStdin(options.input);
@@ -1,5 +1,5 @@
1
- import type { TerminalApplication } from "./TerminalApplication.js";
2
- export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait">;
1
+ import type { ExitInfo, TerminalApplication } from "./TerminalApplication.js";
2
+ export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait" | "untilExit">;
3
3
  /** A testable application that can be started, killed, and given input. For a
4
4
  * single instance of this interface, only a single instance can be running at
5
5
  * a time.
@@ -7,6 +7,7 @@ export type StartableApplication = Pick<TerminalApplication, "write" | "processI
7
7
  export declare class DisposableSingleApplication implements AsyncDisposable {
8
8
  protected application: StartableApplication | undefined;
9
9
  startNextAndKillCurrent(startNext: () => Promise<StartableApplication>): Promise<void>;
10
+ untilExit(): Promise<ExitInfo>;
10
11
  write(input: string): Promise<void>;
11
12
  processId(): number | undefined;
12
13
  /** Kill the current application if it exists. */
@@ -9,6 +9,10 @@ export class DisposableSingleApplication {
9
9
  await this[Symbol.asyncDispose]();
10
10
  this.application = await startNext();
11
11
  }
12
+ async untilExit() {
13
+ assert(this.application, "The application not started yet. It makes no sense to wait for it to exit, so this looks like a bug.");
14
+ return this.application.untilExit;
15
+ }
12
16
  async write(input) {
13
17
  assert(this.application, "The application not started yet. It makes no sense to write to it, so this looks like a bug.");
14
18
  this.application.write(input);
@@ -9,6 +9,7 @@ const fakeApp = {
9
9
  processId: 123,
10
10
  write: vi.fn(),
11
11
  killAndWait: vi.fn(),
12
+ untilExit: Promise.resolve({ exitCode: 0, signal: undefined }),
12
13
  };
13
14
  describe("DisposableSingleApplication", () => {
14
15
  it("has no application when created", () => {
@@ -34,6 +35,23 @@ describe("DisposableSingleApplication", () => {
34
35
  const app = new TestDisposableSingleApplication();
35
36
  await expect(app.write("hello")).rejects.toThrowErrorMatchingInlineSnapshot(`[AssertionError: The application not started yet. It makes no sense to write to it, so this looks like a bug.]`);
36
37
  });
38
+ describe("untilExit allows waiting for the application to exit", () => {
39
+ it("successful exit works", async () => {
40
+ const app = new TestDisposableSingleApplication();
41
+ await app.startNextAndKillCurrent(async () => fakeApp);
42
+ fakeApp.untilExit = Promise.resolve({ exitCode: 1, signal: 9 });
43
+ await expect(app.untilExit()).resolves.toStrictEqual({
44
+ exitCode: 1,
45
+ signal: 9,
46
+ });
47
+ });
48
+ it("when the application throws an error, the error is propagated", async () => {
49
+ const app = new TestDisposableSingleApplication();
50
+ await app.startNextAndKillCurrent(async () => fakeApp);
51
+ fakeApp.untilExit = Promise.reject(new Error("fake error"));
52
+ await expect(app.untilExit()).rejects.toThrowError(new Error("fake error"));
53
+ });
54
+ });
37
55
  describe("disposing", () => {
38
56
  it("disposes the application when disposed", async () => {
39
57
  // it's important to make sure there are no dangling applications when
@@ -1,9 +1,14 @@
1
1
  import type winston from "winston";
2
2
  import type { ITerminalDimensions } from "@xterm/addon-fit";
3
3
  import type { StartableApplication } from "./DisposableSingleApplication.js";
4
+ export type ExitInfo = {
5
+ exitCode: number;
6
+ signal: number | undefined;
7
+ };
4
8
  export declare class TerminalApplication implements StartableApplication {
5
9
  private readonly subProcess;
6
10
  readonly onStdoutOrStderr: (data: string) => void;
11
+ readonly untilExit: Promise<ExitInfo>;
7
12
  readonly processId: number;
8
13
  readonly logger: winston.Logger;
9
14
  private constructor();
@@ -5,11 +5,13 @@ import pty from "node-pty";
5
5
  export class TerminalApplication {
6
6
  subProcess;
7
7
  onStdoutOrStderr;
8
+ untilExit;
8
9
  processId;
9
10
  logger;
10
- constructor(subProcess, onStdoutOrStderr) {
11
+ constructor(subProcess, onStdoutOrStderr, untilExit) {
11
12
  this.subProcess = subProcess;
12
13
  this.onStdoutOrStderr = onStdoutOrStderr;
14
+ this.untilExit = untilExit;
13
15
  this.processId = subProcess.pid;
14
16
  this.logger = createLogger({
15
17
  transports: [new transports.Console()],
@@ -32,11 +34,20 @@ export class TerminalApplication {
32
34
  cols: dimensions.cols,
33
35
  rows: dimensions.rows,
34
36
  });
37
+ ptyProcess.onExit(({ exitCode, signal }) => {
38
+ console.log(`Child process exited with code ${exitCode} and signal ${signal}`);
39
+ });
35
40
  const processId = ptyProcess.pid;
36
41
  if (!processId) {
37
42
  throw new Error("Failed to spawn child process");
38
43
  }
39
- return new TerminalApplication(ptyProcess, onStdoutOrStderr);
44
+ const untilExit = new Promise(resolve => {
45
+ ptyProcess.onExit(({ exitCode, signal }) => {
46
+ // console.log(`Child process ${processId} exited with code ${exitCode} and signal ${signal}`)
47
+ resolve({ exitCode, signal });
48
+ });
49
+ });
50
+ return new TerminalApplication(ptyProcess, onStdoutOrStderr, untilExit);
40
51
  }
41
52
  /** Write to the terminal's stdin. */
42
53
  write(data) {