-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #66 from TAServers/LEST-94-lest-package-api
LEST-94 - Add public API to lest package
- Loading branch information
Showing
6 changed files
with
233 additions
and
36 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |