import { logger } from "@coder/logger" import { promises as fs } from "fs" import * as cp from "child_process" import * as net from "net" import * as os from "os" import * as path from "path" /** * Spy on the logger and console and replace with mock implementations to * suppress the output. */ export function mockLogger() { jest.spyOn(logger, "debug").mockImplementation() jest.spyOn(logger, "error").mockImplementation() jest.spyOn(logger, "info").mockImplementation() jest.spyOn(logger, "trace").mockImplementation() jest.spyOn(logger, "warn").mockImplementation() jest.spyOn(console, "log").mockImplementation() jest.spyOn(console, "debug").mockImplementation() jest.spyOn(console, "error").mockImplementation() jest.spyOn(console, "info").mockImplementation() jest.spyOn(console, "trace").mockImplementation() jest.spyOn(console, "warn").mockImplementation() } /** * Clean up directories left by a test. It is recommended to do this when a test * starts to avoid potentially accumulating infinite test directories. */ export async function clean(testName: string): Promise { const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`) await fs.rm(dir, { force: true, recursive: true }) } /** * Create a uniquely named temporary directory for a test. * * `tmpdir` should usually be preceeded by at least one call to `clean`. */ export async function tmpdir(testName: string): Promise { const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`) await fs.mkdir(dir, { recursive: true }) return await fs.mkdtemp(path.join(dir, `${testName}-`), { encoding: "utf8" }) } /** * @description Helper function to use an environment variable. * * @returns an array (similar to useState in React) with a function * to set the value and reset the value */ export function useEnv(key: string): [(nextValue: string | undefined) => string | undefined, () => void] { const initialValue = process.env[key] const setValue = (nextValue: string | undefined) => (process.env[key] = nextValue) // Node automatically converts undefined to string 'undefined' // when assigning an environment variable. // which is why we need to delete it if it's supposed to be undefined // Source: https://stackoverflow.com/a/60147167 const resetValue = () => { if (initialValue !== undefined) { process.env[key] = initialValue } else { delete process.env[key] } } return [setValue, resetValue] } /** * Helper function to get a random port. * * Source: https://github.com/sindresorhus/get-port/blob/main/index.js#L23-L33 */ export const getAvailablePort = (options?: net.ListenOptions): Promise => new Promise((resolve, reject) => { const server = net.createServer() server.unref() server.on("error", reject) server.listen(options, () => { // NOTE@jsjoeio: not a huge fan of the type assertion // but it works for now. const { port } = server.address() as net.AddressInfo server.close(() => { resolve(port) }) }) }) /** * Return a timer that will not reject as long as it is disposed or continually * reset before the delay elapses. */ export function idleTimer(message: string, reject: (error: Error) => void, delay = 5000) { const start = () => setTimeout(() => reject(new Error(message)), delay) let timeout = start() return { reset: () => { clearTimeout(timeout) timeout = start() }, dispose: () => { clearTimeout(timeout) }, } } /** * A helper function which returns a boolean indicating whether * the given address is AddressInfo and has .address * and a .port property. */ export function isAddressInfo(address: unknown): address is net.AddressInfo { return ( address !== null && typeof address !== "string" && (address as net.AddressInfo).port !== undefined && (address as net.AddressInfo).address !== undefined ) } // From https://github.com/chalk/ansi-regex const pattern = [ "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", ].join("|") const re = new RegExp(pattern, "g") type OnLineCallback = (strippedLine: string, originalLine: string) => void /** * Split stdout on newlines and strip ANSI codes. */ export const onLine = (proc: cp.ChildProcess, callback: OnLineCallback): void => { let buffer = "" if (!proc.stdout) { throw new Error("no stdout") } proc.stdout.setEncoding("utf8") proc.stdout.on("data", (d) => { const data = buffer + d const split = data.split("\n") const last = split.length - 1 for (let i = 0; i < last; ++i) { callback(split[i].replace(re, ""), split[i]) } // The last item will either be an empty string (the data ended with a // newline) or a partial line (did not end with a newline) and we must // wait to parse it until we get a full line. buffer = split[last] }) }