Skip to content

Commit

Permalink
Merge pull request #66 from TAServers/LEST-94-lest-package-api
Browse files Browse the repository at this point in the history
LEST-94 - Add public API to lest package
  • Loading branch information
Derpius authored Jul 13, 2023
2 parents 160e504 + f89be25 commit ca0f1b8
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 36 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions packages/lest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
"url": "https://github.com/TAServers/lest",
"directory": "packages/lest"
},
"main": "dist/index.js",
"exports": {
".": "dist/index.js"
},
"types": "dist/index.d.ts",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"lest": "dist/index.js"
"lest": "./dist/bin/lest.js"
},
"scripts": {
"build:lua": "luabundler bundle src/lua/lest.lua -p \"./?.lua\" -o dist/lua/lest.lua",
Expand All @@ -25,7 +23,7 @@
"test:ts": "jest",
"test": "npm run test:lua && npm run test:ts",
"clean": "rimraf dist",
"start": "ts-node src/index.ts"
"start": "ts-node src/bin/lest.ts"
},
"devDependencies": {
"@types/jest": "^29.5.2",
Expand Down
26 changes: 26 additions & 0 deletions packages/lest/src/bin/lest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node

import minimist from "minimist";
import { LestProcess, LestEvent } from "../lest-process";

const { luaCommand } = minimist(process.argv.slice(2));

const lestProcess = new LestProcess(luaCommand);

lestProcess
.addListener(LestEvent.StdOut, (data: Buffer) => {
process.stdout.write(data);
})
.addListener(LestEvent.StdErr, (data: Buffer) => {
process.stderr.write(data);
})
.addListener(LestEvent.Error, (error) => {
console.error(`Failed to run command:\n${error.message}`);
});

lestProcess
.run(process.argv.slice(2))
.then((success) => process.exit(success ? 0 : 1))
.catch((error) => {
console.error(`Unhandled error occurred:\n${error.message}`);
});
29 changes: 1 addition & 28 deletions packages/lest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1 @@
#!/usr/bin/env node

import minimist from "minimist";
import path from "path";
import { findLuaExecutable } from "./helpers";
import { spawn } from "child_process";

const { luaCommand } = minimist(process.argv.slice(2));

const getLestLuaPath = () => path.join(__dirname, "lua", "lest.lua");

findLuaExecutable(luaCommand).then((executablePath) => {
const scriptPath = getLestLuaPath();

const lestProcess = spawn(executablePath, [scriptPath, ...process.argv.slice(2)]);

lestProcess.stdout.on("data", (data: Buffer) => {
process.stdout.write(data);
});

lestProcess.stderr.on("data", (data: Buffer) => {
process.stderr.write(data);
});

lestProcess.on("error", (error) => {
console.error(`Failed to run command:\n${error.message}`);
});
});
export * from "./lest-process";
73 changes: 73 additions & 0 deletions packages/lest/src/lest-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import path from "path";
import { findLuaExecutable } from "./helpers";
import { spawn } from "child_process";

export enum LestEvent {
StdOut,
StdErr,
Error,
}

export type LestEventListener<Event extends LestEvent> = Event extends LestEvent.StdOut
? (data: Buffer) => void
: Event extends LestEvent.StdErr
? (data: Buffer) => void
: Event extends LestEvent.Error
? (error: Error) => void
: never;

export class LestProcess {
private eventListeners: { [K in LestEvent]: Set<LestEventListener<K>> } = {
[LestEvent.StdOut]: new Set(),
[LestEvent.StdErr]: new Set(),
[LestEvent.Error]: new Set(),
};

readonly scriptPath: string;

constructor(readonly luaCommand?: string) {
this.scriptPath = path.join(__dirname, "lua", "lest.lua");
}

async run(args: string[] = []): Promise<boolean> {
const executablePath = await findLuaExecutable(this.luaCommand);
const process = spawn(executablePath, [this.scriptPath, ...args]);

process.stdout.on("data", (data: Buffer) => {
this.triggerEvent(LestEvent.StdOut, data);
});

process.stderr.on("data", (data: Buffer) => {
this.triggerEvent(LestEvent.StdErr, data);
});

process.on("error", (error) => {
this.triggerEvent(LestEvent.Error, error);
});

return new Promise((resolve) => {
process.on("exit", (code) => {
resolve(code === 0);
});
});
}

private triggerEvent<Event extends LestEvent>(event: Event, ...args: Parameters<LestEventListener<Event>>) {
this.eventListeners[event].forEach((listener: LestEventListener<Event>) =>
listener(
// @ts-expect-error
...args
)
);
}

addListener<Event extends LestEvent>(event: Event, listener: LestEventListener<Event>): this {
this.eventListeners[event].add(listener);
return this;
}

removeListener<Event extends LestEvent>(event: Event, listener: LestEventListener<Event>): this {
this.eventListeners[event].delete(listener);
return this;
}
}
127 changes: 127 additions & 0 deletions packages/lest/tests/lest-process.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { when, WhenMockWithMatchers } from "jest-when";
import { LestEvent, LestProcess } from "../src";
import { spawn } from "child_process";
import { findLuaExecutable } from "../src/helpers";

const mockStdOutOn = jest.fn();
const mockStdErrOn = jest.fn();
const mockOn = jest.fn();

const SCRIPT_PATH = expect.stringMatching(/.+[\/\\]lua[\/\\]lest\.lua/);

jest.mock("child_process", () => ({
spawn: jest.fn(() => ({
stdout: { on: mockStdOutOn },
stderr: { on: mockStdErrOn },
on: mockOn,
})),
}));

jest.mock("../src/helpers", () => ({
findLuaExecutable: jest.fn(),
}));

const runLestProcess = (
process: LestProcess,
{ exitCode = 0, args = [] }: { exitCode?: number; args?: string[] } = {}
) => {
when(mockOn)
.calledWith("exit", expect.anything())
.mockImplementation((_, callback) => {
callback(exitCode);
});

return process.run(args);
};

test("spawns the Lest process with the given args", async () => {
const luaPath = "/path/to/lua.exe";
const args = ["--testMatch", "1234"];

jest.mocked(findLuaExecutable).mockResolvedValue(luaPath);

const process = new LestProcess();
await runLestProcess(process, { args });

expect(jest.mocked(spawn)).toHaveBeenCalledWith(luaPath, [SCRIPT_PATH, ...args]);
});

test("spawns the Lest process with the given luaCommand", async () => {
jest.mocked(findLuaExecutable).mockImplementation((cmd = "") => Promise.resolve(cmd));
const luaCommand = "/path/to/lua.exe";

const process = new LestProcess(luaCommand);
await runLestProcess(process);

expect(jest.mocked(spawn)).toHaveBeenCalledWith(luaCommand, [SCRIPT_PATH]);
});

test("returns true when the process exits successfully", async () => {
const process = new LestProcess();

await expect(runLestProcess(process, { exitCode: 0 })).resolves.toBe(true);
});

test("returns false when the process exits unsuccessfully", async () => {
const process = new LestProcess();

await expect(runLestProcess(process, { exitCode: 1 })).resolves.toBe(false);
});

interface ListenerTestCase {
event: keyof typeof LestEvent;
onMock: jest.Mock | WhenMockWithMatchers;
args: any[];
expected: any[];
}

const listenerTestCases: ListenerTestCase[] = [
{
event: "StdOut",
onMock: when(mockStdOutOn).calledWith("data", expect.anything()),
args: [Buffer.from("blah")],
expected: [Buffer.from("blah")],
},
{
event: "StdErr",
onMock: when(mockStdErrOn).calledWith("data", expect.anything()),
args: [Buffer.from("blah")],
expected: [Buffer.from("blah")],
},
{
event: "Error",
onMock: when(mockOn).calledWith("error", expect.anything()),
args: [new Error("blah")],
expected: [new Error("blah")],
},
];

describe.each(listenerTestCases)("event: $event", ({ event, onMock, args, expected }) => {
test("calls listeners", async () => {
onMock.mockImplementation((_, callback) => callback(...args));
const listener1 = jest.fn();
const listener2 = jest.fn();

const process = new LestProcess();

process.addListener(LestEvent[event], listener1);
process.addListener(LestEvent[event], listener2);
await runLestProcess(process);

expect(listener1).toHaveBeenCalledWith(...expected);
expect(listener2).toHaveBeenCalledWith(...expected);
});

test("doesn't call listeners that have been removed", async () => {
onMock.mockImplementation((_, callback) => callback(...args));
const listener = jest.fn();

const process = new LestProcess();

process.addListener(LestEvent.StdOut, listener);
process.removeListener(LestEvent.StdOut, listener);
await runLestProcess(process);

expect(listener).not.toHaveBeenCalled();
});
});

0 comments on commit ca0f1b8

Please sign in to comment.