Refactor evaluations (#285)
* Replace evaluations with proxies and messages * Return proxies synchronously Otherwise events can be lost. * Ensure events cannot be missed * Refactor remaining fills * Use more up-to-date version of util For callbackify. * Wait for dispose to come back before removing This prevents issues with the "done" event not always being the last event fired. For example a socket might close and then end, but only if the caller called end. * Remove old node-pty tests * Fix emitting events twice on duplex streams * Preserve environment when spawning processes * Throw a better error if the proxy doesn't exist * Remove rimraf dependency from ide * Update net.Server.listening * Use exit event instead of killed Doesn't look like killed is even a thing. * Add response timeout to server * Fix trash * Require node-pty & spdlog after they get unpackaged This fixes an error when running in the binary. * Fix errors in down emitter preventing reconnecting * Fix disposing proxies when nothing listens to "error" event * Refactor event tests to use jest.fn() * Reject proxy call when disconnected Otherwise it'll wait for the timeout which is a waste of time since we already know the connection is dead. * Use nbin for binary packaging * Remove additional module requires * Attempt to remove require for local bootstrap-fork * Externalize fsevents
This commit is contained in:
98
packages/protocol/test/child_process.test.ts
Normal file
98
packages/protocol/test/child_process.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { ChildProcess } from "child_process";
|
||||
import * as path from "path";
|
||||
import { Readable } from "stream";
|
||||
import * as util from "util";
|
||||
import { createClient } from "@coder/protocol/test";
|
||||
import { Module } from "../src/common/proxy";
|
||||
|
||||
describe("child_process", () => {
|
||||
const client = createClient();
|
||||
const cp = client.modules[Module.ChildProcess];
|
||||
|
||||
const getStdout = async (proc: ChildProcess): Promise<string> => {
|
||||
return new Promise((r): Readable => proc.stdout.on("data", r))
|
||||
.then((s) => s.toString());
|
||||
};
|
||||
|
||||
describe("exec", () => {
|
||||
it("should get exec stdout", async () => {
|
||||
await expect(util.promisify(cp.exec)("echo test", { encoding: "utf8" }))
|
||||
.resolves.toEqual({
|
||||
stdout: "test\n",
|
||||
stderr: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawn", () => {
|
||||
it("should get spawn stdout", async () => {
|
||||
const proc = cp.spawn("echo", ["test"]);
|
||||
await expect(Promise.all([
|
||||
getStdout(proc),
|
||||
new Promise((r): ChildProcess => proc.on("exit", r)),
|
||||
]).then((values) => values[0])).resolves.toEqual("test\n");
|
||||
});
|
||||
|
||||
it("should cat", async () => {
|
||||
const proc = cp.spawn("cat", []);
|
||||
expect(proc.pid).toBe(-1);
|
||||
proc.stdin.write("banana");
|
||||
await expect(getStdout(proc)).resolves.toBe("banana");
|
||||
|
||||
proc.stdin.end();
|
||||
proc.kill();
|
||||
|
||||
expect(proc.pid).toBeGreaterThan(-1);
|
||||
await new Promise((r): ChildProcess => proc.on("exit", r));
|
||||
});
|
||||
|
||||
it("should print env", async () => {
|
||||
const proc = cp.spawn("env", [], {
|
||||
env: { hi: "donkey" },
|
||||
});
|
||||
|
||||
await expect(getStdout(proc)).resolves.toContain("hi=donkey\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fork", () => {
|
||||
it("should echo messages", async () => {
|
||||
const proc = cp.fork(path.join(__dirname, "forker.js"));
|
||||
|
||||
proc.send({ bananas: true });
|
||||
|
||||
await expect(new Promise((r): ChildProcess => proc.on("message", r)))
|
||||
.resolves.toMatchObject({
|
||||
bananas: true,
|
||||
});
|
||||
|
||||
proc.kill();
|
||||
|
||||
await new Promise((r): ChildProcess => proc.on("exit", r));
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", (done) => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it("should disconnect", async () => {
|
||||
const client = createClient();
|
||||
const cp = client.modules[Module.ChildProcess];
|
||||
const proc = cp.fork(path.join(__dirname, "forker.js"));
|
||||
const fn = jest.fn();
|
||||
proc.on("error", fn);
|
||||
|
||||
proc.send({ bananas: true });
|
||||
await expect(new Promise((r): ChildProcess => proc.on("message", r)))
|
||||
.resolves.toMatchObject({
|
||||
bananas: true,
|
||||
});
|
||||
|
||||
client.dispose();
|
||||
expect(fn).toHaveBeenCalledWith(new Error("disconnected"));
|
||||
});
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
import { createClient } from "./helpers";
|
||||
|
||||
describe("Evaluate", () => {
|
||||
const client = createClient();
|
||||
|
||||
it("should transfer string", async () => {
|
||||
const value = await client.evaluate(() => {
|
||||
return "hi";
|
||||
});
|
||||
|
||||
expect(value).toEqual("hi");
|
||||
}, 100);
|
||||
|
||||
it("should compute from string", async () => {
|
||||
const start = "ban\%\$\"``a,,,,asdasd";
|
||||
const value = await client.evaluate((_helper, a) => {
|
||||
return a;
|
||||
}, start);
|
||||
|
||||
expect(value).toEqual(start);
|
||||
}, 100);
|
||||
|
||||
it("should compute from object", async () => {
|
||||
const value = await client.evaluate((_helper, arg) => {
|
||||
return arg.bananas * 2;
|
||||
}, { bananas: 1 });
|
||||
|
||||
expect(value).toEqual(2);
|
||||
}, 100);
|
||||
|
||||
it("should transfer object", async () => {
|
||||
const value = await client.evaluate(() => {
|
||||
return { alpha: "beta" };
|
||||
});
|
||||
|
||||
expect(value.alpha).toEqual("beta");
|
||||
}, 100);
|
||||
|
||||
it("should require", async () => {
|
||||
const value = await client.evaluate(() => {
|
||||
const fs = require("fs") as typeof import("fs");
|
||||
|
||||
return Object.keys(fs).filter((f) => f === "readFileSync");
|
||||
});
|
||||
|
||||
expect(value[0]).toEqual("readFileSync");
|
||||
}, 100);
|
||||
|
||||
it("should resolve with promise", async () => {
|
||||
const value = await client.evaluate(async () => {
|
||||
await new Promise((r): number => setTimeout(r, 100));
|
||||
|
||||
return "donkey";
|
||||
});
|
||||
|
||||
expect(value).toEqual("donkey");
|
||||
}, 250);
|
||||
|
||||
it("should do active process", (done) => {
|
||||
const runner = client.run((ae) => {
|
||||
ae.on("first", () => {
|
||||
ae.emit("first:response");
|
||||
ae.on("second", () => ae.emit("second:response"));
|
||||
});
|
||||
|
||||
const disposeCallbacks = <Array<() => void>>[];
|
||||
const dispose = (): void => {
|
||||
disposeCallbacks.forEach((cb) => cb());
|
||||
ae.emit("disposed");
|
||||
};
|
||||
|
||||
return {
|
||||
onDidDispose: (cb: () => void): number => disposeCallbacks.push(cb),
|
||||
dispose,
|
||||
};
|
||||
});
|
||||
|
||||
runner.emit("first");
|
||||
runner.on("first:response", () => runner.emit("second"));
|
||||
runner.on("second:response", () => client.dispose());
|
||||
|
||||
runner.on("disposed", () => done());
|
||||
});
|
||||
});
|
3
packages/protocol/test/forker.js
Normal file
3
packages/protocol/test/forker.js
Normal file
@ -0,0 +1,3 @@
|
||||
process.on("message", (data) => {
|
||||
process.send(data);
|
||||
});
|
581
packages/protocol/test/fs.test.ts
Normal file
581
packages/protocol/test/fs.test.ts
Normal file
@ -0,0 +1,581 @@
|
||||
import * as nativeFs from "fs";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("fs", () => {
|
||||
const client = createClient();
|
||||
// tslint:disable-next-line no-any
|
||||
const fs = client.modules[Module.Fs] as any as typeof import("fs");
|
||||
const helper = new Helper("fs");
|
||||
|
||||
beforeAll(async () => {
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
describe("access", () => {
|
||||
it("should access existing file", async () => {
|
||||
await expect(util.promisify(fs.access)(__filename))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to access nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.access)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("append", () => {
|
||||
it("should append to existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.appendFile)(file, "howdy"))
|
||||
.resolves.toBeUndefined();
|
||||
expect(await util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
.toEqual("howdy");
|
||||
});
|
||||
|
||||
it("should create then append to nonexistent file", async () => {
|
||||
const file = helper.tmpFile();
|
||||
await expect(util.promisify(fs.appendFile)(file, "howdy"))
|
||||
.resolves.toBeUndefined();
|
||||
expect(await util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
.toEqual("howdy");
|
||||
});
|
||||
|
||||
it("should fail to append to file in nonexistent directory", async () => {
|
||||
const file = path.join(helper.tmpFile(), "nope");
|
||||
await expect(util.promisify(fs.appendFile)(file, "howdy"))
|
||||
.rejects.toThrow("ENOENT");
|
||||
expect(await util.promisify(nativeFs.exists)(file))
|
||||
.toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chmod", () => {
|
||||
it("should chmod existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.chmod)(file, "755"))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to chmod nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.chmod)(helper.tmpFile(), "755"))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("chown", () => {
|
||||
it("should chown existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.chown)(file, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to chown nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.chown)(helper.tmpFile(), 1, 1))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("close", () => {
|
||||
it("should close opened file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.close)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to close non-opened file", async () => {
|
||||
await expect(util.promisify(fs.close)(99999999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("copyFile", () => {
|
||||
it("should copy existing file", async () => {
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.copyFile)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(fs.exists)(destination))
|
||||
.resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to copy nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.copyFile)(helper.tmpFile(), helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createWriteStream", () => {
|
||||
it("should write to file", async () => {
|
||||
const file = helper.tmpFile();
|
||||
const content = "howdy\nhow\nr\nu";
|
||||
const stream = fs.createWriteStream(file);
|
||||
stream.on("open", (fd) => {
|
||||
expect(fd).toBeDefined();
|
||||
stream.write(content);
|
||||
stream.close();
|
||||
stream.end();
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
new Promise((resolve): nativeFs.WriteStream => stream.on("close", resolve)),
|
||||
new Promise((resolve): nativeFs.WriteStream => stream.on("finish", resolve)),
|
||||
]);
|
||||
|
||||
await expect(util.promisify(nativeFs.readFile)(file, "utf8")).resolves.toBe(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exists", () => {
|
||||
it("should output file exists", async () => {
|
||||
await expect(util.promisify(fs.exists)(__filename))
|
||||
.resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should output file does not exist", async () => {
|
||||
await expect(util.promisify(fs.exists)(helper.tmpFile()))
|
||||
.resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fchmod", () => {
|
||||
it("should fchmod existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fchmod)(fd, "755"))
|
||||
.resolves.toBeUndefined();
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to fchmod nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.fchmod)(2242342, "755"))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fchown", () => {
|
||||
it("should fchown existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fchown)(fd, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to fchown nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.fchown)(99999, 1, 1))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fdatasync", () => {
|
||||
it("should fdatasync existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fdatasync)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to fdatasync nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.fdatasync)(99999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fstat", () => {
|
||||
it("should fstat existing file", async () => {
|
||||
const fd = await util.promisify(nativeFs.open)(__filename, "r");
|
||||
const stat = await util.promisify(nativeFs.fstat)(fd);
|
||||
await expect(util.promisify(fs.fstat)(fd))
|
||||
.resolves.toMatchObject({
|
||||
size: stat.size,
|
||||
});
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to fstat", async () => {
|
||||
await expect(util.promisify(fs.fstat)(9999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fsync", () => {
|
||||
it("should fsync existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "r");
|
||||
await expect(util.promisify(fs.fsync)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to fsync nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.fsync)(99999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ftruncate", () => {
|
||||
it("should ftruncate existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "w");
|
||||
await expect(util.promisify(fs.ftruncate)(fd, 1))
|
||||
.resolves.toBeUndefined();
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to ftruncate nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.ftruncate)(99999, 9999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("futimes", () => {
|
||||
it("should futimes existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "w");
|
||||
await expect(util.promisify(fs.futimes)(fd, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to futimes nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.futimes)(99999, 9999, 9999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lchmod", () => {
|
||||
it("should lchmod existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.lchmod)(file, "755"))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
// TODO: Doesn't fail on my system?
|
||||
it("should fail to lchmod nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.lchmod)(helper.tmpFile(), "755"))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("lchown", () => {
|
||||
it("should lchown existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.lchown)(file, 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
// TODO: Doesn't fail on my system?
|
||||
it("should fail to lchown nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.lchown)(helper.tmpFile(), 1, 1))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("link", () => {
|
||||
it("should link existing file", async () => {
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.link)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(fs.exists)(destination))
|
||||
.resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to link nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.link)(helper.tmpFile(), helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lstat", () => {
|
||||
it("should lstat existing file", async () => {
|
||||
const stat = await util.promisify(nativeFs.lstat)(__filename);
|
||||
await expect(util.promisify(fs.lstat)(__filename))
|
||||
.resolves.toMatchObject({
|
||||
size: stat.size,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to lstat non-existent file", async () => {
|
||||
await expect(util.promisify(fs.lstat)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mkdir", () => {
|
||||
let target: string;
|
||||
it("should create nonexistent directory", async () => {
|
||||
target = helper.tmpFile();
|
||||
await expect(util.promisify(fs.mkdir)(target))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to create existing directory", async () => {
|
||||
await expect(util.promisify(fs.mkdir)(target))
|
||||
.rejects.toThrow("EEXIST");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mkdtemp", () => {
|
||||
it("should create temp dir", async () => {
|
||||
await expect(util.promisify(fs.mkdtemp)(helper.coderDir + "/"))
|
||||
.resolves.toMatch(/^\/tmp\/coder\/fs\/[a-zA-Z0-9]{6}/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("open", () => {
|
||||
it("should open existing file", async () => {
|
||||
const fd = await util.promisify(fs.open)(__filename, "r");
|
||||
expect(fd).not.toBeNaN();
|
||||
await expect(util.promisify(fs.close)(fd))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to open nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.open)(helper.tmpFile(), "r"))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("read", () => {
|
||||
it("should read existing file", async () => {
|
||||
const fd = await util.promisify(nativeFs.open)(__filename, "r");
|
||||
const stat = await util.promisify(nativeFs.fstat)(fd);
|
||||
const buffer = new Buffer(stat.size);
|
||||
let bytesRead = 0;
|
||||
let chunkSize = 2048;
|
||||
while (bytesRead < stat.size) {
|
||||
if ((bytesRead + chunkSize) > stat.size) {
|
||||
chunkSize = stat.size - bytesRead;
|
||||
}
|
||||
|
||||
await util.promisify(fs.read)(fd, buffer, bytesRead, chunkSize, bytesRead);
|
||||
bytesRead += chunkSize;
|
||||
}
|
||||
|
||||
const content = await util.promisify(nativeFs.readFile)(__filename, "utf8");
|
||||
expect(buffer.toString()).toEqual(content);
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.read)(99999, new Buffer(10), 9999, 999, 999))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readFile", () => {
|
||||
it("should read existing file", async () => {
|
||||
const content = await util.promisify(nativeFs.readFile)(__filename, "utf8");
|
||||
await expect(util.promisify(fs.readFile)(__filename, "utf8"))
|
||||
.resolves.toEqual(content);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.readFile)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readdir", () => {
|
||||
it("should read existing directory", async () => {
|
||||
const paths = await util.promisify(nativeFs.readdir)(helper.coderDir);
|
||||
await expect(util.promisify(fs.readdir)(helper.coderDir))
|
||||
.resolves.toEqual(paths);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent directory", async () => {
|
||||
await expect(util.promisify(fs.readdir)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readlink", () => {
|
||||
it("should read existing link", async () => {
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await util.promisify(nativeFs.symlink)(source, destination);
|
||||
await expect(util.promisify(fs.readlink)(destination))
|
||||
.resolves.toBe(source);
|
||||
});
|
||||
|
||||
it("should fail to read nonexistent link", async () => {
|
||||
await expect(util.promisify(fs.readlink)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("realpath", () => {
|
||||
it("should read real path of existing file", async () => {
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
nativeFs.symlinkSync(source, destination);
|
||||
await expect(util.promisify(fs.realpath)(destination))
|
||||
.resolves.toBe(source);
|
||||
});
|
||||
|
||||
it("should fail to read real path of nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.realpath)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rename", () => {
|
||||
it("should rename existing file", async () => {
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.rename)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(nativeFs.exists)(source))
|
||||
.resolves.toBe(false);
|
||||
await expect(util.promisify(nativeFs.exists)(destination))
|
||||
.resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to rename nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.rename)(helper.tmpFile(), helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rmdir", () => {
|
||||
it("should rmdir existing directory", async () => {
|
||||
const dir = helper.tmpFile();
|
||||
await util.promisify(nativeFs.mkdir)(dir);
|
||||
await expect(util.promisify(fs.rmdir)(dir))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(nativeFs.exists)(dir))
|
||||
.resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should fail to rmdir nonexistent directory", async () => {
|
||||
await expect(util.promisify(fs.rmdir)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stat", () => {
|
||||
it("should stat existing file", async () => {
|
||||
const nativeStat = await util.promisify(nativeFs.stat)(__filename);
|
||||
const stat = await util.promisify(fs.stat)(__filename);
|
||||
expect(stat).toMatchObject({
|
||||
size: nativeStat.size,
|
||||
});
|
||||
expect(stat.isFile()).toBe(true);
|
||||
});
|
||||
|
||||
it("should stat existing folder", async () => {
|
||||
const dir = helper.tmpFile();
|
||||
await util.promisify(nativeFs.mkdir)(dir);
|
||||
const nativeStat = await util.promisify(nativeFs.stat)(dir);
|
||||
const stat = await util.promisify(fs.stat)(dir);
|
||||
expect(stat).toMatchObject({
|
||||
size: nativeStat.size,
|
||||
});
|
||||
expect(stat.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to stat nonexistent file", async () => {
|
||||
const error = await util.promisify(fs.stat)(helper.tmpFile()).catch((e) => e);
|
||||
expect(error.message).toContain("ENOENT");
|
||||
expect(error.code).toBe("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("symlink", () => {
|
||||
it("should symlink existing file", async () => {
|
||||
const source = await helper.createTmpFile();
|
||||
const destination = helper.tmpFile();
|
||||
await expect(util.promisify(fs.symlink)(source, destination))
|
||||
.resolves.toBeUndefined();
|
||||
expect(util.promisify(nativeFs.exists)(source))
|
||||
.resolves.toBe(true);
|
||||
});
|
||||
|
||||
// TODO: Seems to be happy to do this on my system?
|
||||
it("should fail to symlink nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.symlink)(helper.tmpFile(), helper.tmpFile()))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncate", () => {
|
||||
it("should truncate existing file", async () => {
|
||||
const file = helper.tmpFile();
|
||||
await util.promisify(nativeFs.writeFile)(file, "hiiiiii");
|
||||
await expect(util.promisify(fs.truncate)(file, 2))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
.resolves.toBe("hi");
|
||||
});
|
||||
|
||||
it("should fail to truncate nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.truncate)(helper.tmpFile(), 0))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlink", () => {
|
||||
it("should unlink existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.unlink)(file))
|
||||
.resolves.toBeUndefined();
|
||||
expect(util.promisify(nativeFs.exists)(file))
|
||||
.resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("should fail to unlink nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.unlink)(helper.tmpFile()))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("utimes", () => {
|
||||
it("should update times on existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.utimes)(file, 100, 100))
|
||||
.resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to update times on nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.utimes)(helper.tmpFile(), 100, 100))
|
||||
.rejects.toThrow("ENOENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("write", () => {
|
||||
it("should write to existing file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const fd = await util.promisify(nativeFs.open)(file, "w");
|
||||
await expect(util.promisify(fs.write)(fd, Buffer.from("hi")))
|
||||
.resolves.toBe(2);
|
||||
await expect(util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
.resolves.toBe("hi");
|
||||
await util.promisify(nativeFs.close)(fd);
|
||||
});
|
||||
|
||||
it("should fail to write to nonexistent file", async () => {
|
||||
await expect(util.promisify(fs.write)(100000, Buffer.from("wowow")))
|
||||
.rejects.toThrow("EBADF");
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeFile", () => {
|
||||
it("should write file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await expect(util.promisify(fs.writeFile)(file, "howdy"))
|
||||
.resolves.toBeUndefined();
|
||||
await expect(util.promisify(nativeFs.readFile)(file, "utf8"))
|
||||
.resolves.toBe("howdy");
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", () => {
|
||||
client.dispose();
|
||||
});
|
||||
});
|
@ -1,7 +1,54 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as rimraf from "rimraf";
|
||||
import * as util from "util";
|
||||
import { IDisposable } from "@coder/disposable";
|
||||
import { Emitter } from "@coder/events";
|
||||
import { Client } from "../src/browser/client";
|
||||
import { Server, ServerOptions } from "../src/node/server";
|
||||
|
||||
// So we only make the directory once when running multiple tests.
|
||||
let mkdirPromise: Promise<void> | undefined;
|
||||
|
||||
export class Helper {
|
||||
private i = 0;
|
||||
public coderDir: string;
|
||||
private baseDir = path.join(os.tmpdir(), "coder");
|
||||
|
||||
public constructor(directoryName: string) {
|
||||
if (!directoryName.trim()) {
|
||||
throw new Error("no directory name");
|
||||
}
|
||||
|
||||
this.coderDir = path.join(this.baseDir, directoryName);
|
||||
}
|
||||
|
||||
public tmpFile(): string {
|
||||
return path.join(this.coderDir, `${this.i++}`);
|
||||
}
|
||||
|
||||
public async createTmpFile(): Promise<string> {
|
||||
const tf = this.tmpFile();
|
||||
await util.promisify(fs.writeFile)(tf, "");
|
||||
|
||||
return tf;
|
||||
}
|
||||
|
||||
public async prepare(): Promise<void> {
|
||||
if (!mkdirPromise) {
|
||||
mkdirPromise = util.promisify(fs.mkdir)(this.baseDir).catch((error) => {
|
||||
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
await mkdirPromise;
|
||||
await util.promisify(rimraf)(this.coderDir);
|
||||
await util.promisify(fs.mkdir)(this.coderDir);
|
||||
}
|
||||
}
|
||||
|
||||
export const createClient = (serverOptions?: ServerOptions): Client => {
|
||||
const s2c = new Emitter<Uint8Array | Buffer>();
|
||||
const c2s = new Emitter<Uint8Array | Buffer>();
|
||||
@ -10,19 +57,19 @@ export const createClient = (serverOptions?: ServerOptions): Client => {
|
||||
// tslint:disable-next-line no-unused-expression
|
||||
new Server({
|
||||
close: (): void => closeCallbacks.forEach((cb) => cb()),
|
||||
onDown: (_cb: () => void): void => undefined,
|
||||
onUp: (_cb: () => void): void => undefined,
|
||||
onClose: (cb: () => void): number => closeCallbacks.push(cb),
|
||||
onMessage: (cb): void => {
|
||||
c2s.event((d) => cb(d));
|
||||
},
|
||||
onMessage: (cb): IDisposable => c2s.event((d) => cb(d)),
|
||||
send: (data): NodeJS.Timer => setTimeout(() => s2c.emit(data), 0),
|
||||
}, serverOptions);
|
||||
|
||||
const client = new Client({
|
||||
close: (): void => closeCallbacks.forEach((cb) => cb()),
|
||||
onDown: (_cb: () => void): void => undefined,
|
||||
onUp: (_cb: () => void): void => undefined,
|
||||
onClose: (cb: () => void): number => closeCallbacks.push(cb),
|
||||
onMessage: (cb): void => {
|
||||
s2c.event((d) => cb(d));
|
||||
},
|
||||
onMessage: (cb): IDisposable => s2c.event((d) => cb(d)),
|
||||
send: (data): NodeJS.Timer => setTimeout(() => c2s.emit(data), 0),
|
||||
});
|
||||
|
||||
|
145
packages/protocol/test/net.test.ts
Normal file
145
packages/protocol/test/net.test.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as nativeNet from "net";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("net", () => {
|
||||
const client = createClient();
|
||||
const net = client.modules[Module.Net];
|
||||
const helper = new Helper("net");
|
||||
|
||||
beforeAll(async () => {
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
describe("Socket", () => {
|
||||
const socketPath = helper.tmpFile();
|
||||
let server: nativeNet.Server;
|
||||
|
||||
beforeAll(async () => {
|
||||
await new Promise((r): void => {
|
||||
server = nativeNet.createServer().listen(socketPath, r);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it("should fail to connect", async () => {
|
||||
const socket = new net.Socket();
|
||||
|
||||
const fn = jest.fn();
|
||||
socket.on("error", fn);
|
||||
|
||||
socket.connect("/tmp/t/e/s/t/d/o/e/s/n/o/t/e/x/i/s/t");
|
||||
|
||||
await new Promise((r): nativeNet.Socket => socket.on("close", r));
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should connect", async () => {
|
||||
await new Promise((resolve): void => {
|
||||
const socket = net.createConnection(socketPath, () => {
|
||||
socket.end();
|
||||
socket.addListener("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
const socket = new net.Socket();
|
||||
socket.connect(socketPath, () => {
|
||||
socket.end();
|
||||
socket.addListener("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get data", (done) => {
|
||||
server.once("connection", (socket: nativeNet.Socket) => {
|
||||
socket.write("hi how r u");
|
||||
});
|
||||
|
||||
const socket = net.createConnection(socketPath);
|
||||
|
||||
socket.addListener("data", (data) => {
|
||||
expect(data.toString()).toEqual("hi how r u");
|
||||
socket.end();
|
||||
socket.addListener("close", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should send data", (done) => {
|
||||
const clientSocket = net.createConnection(socketPath);
|
||||
clientSocket.write(Buffer.from("bananas"));
|
||||
server.once("connection", (socket: nativeNet.Socket) => {
|
||||
socket.addListener("data", (data) => {
|
||||
expect(data.toString()).toEqual("bananas");
|
||||
socket.end();
|
||||
clientSocket.addListener("end", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Server", () => {
|
||||
it("should listen", (done) => {
|
||||
const s = net.createServer();
|
||||
s.on("listening", () => s.close());
|
||||
s.on("close", () => done());
|
||||
s.listen(helper.tmpFile());
|
||||
});
|
||||
|
||||
it("should get connection", async () => {
|
||||
let constructorListener: (() => void) | undefined;
|
||||
const s = net.createServer(() => {
|
||||
if (constructorListener) {
|
||||
constructorListener();
|
||||
}
|
||||
});
|
||||
|
||||
const socketPath = helper.tmpFile();
|
||||
s.listen(socketPath);
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
s.on("listening", resolve);
|
||||
});
|
||||
|
||||
const makeConnection = async (): Promise<void> => {
|
||||
net.createConnection(socketPath);
|
||||
await Promise.all([
|
||||
new Promise((resolve): void => {
|
||||
constructorListener = resolve;
|
||||
}),
|
||||
new Promise((resolve): void => {
|
||||
s.once("connection", (socket) => {
|
||||
socket.destroy();
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
await makeConnection();
|
||||
await makeConnection();
|
||||
|
||||
s.close();
|
||||
await new Promise((r): nativeNet.Server => s.on("close", r));
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", (done) => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
99
packages/protocol/test/node-pty.test.ts
Normal file
99
packages/protocol/test/node-pty.test.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { IPty } from "node-pty";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient } from "./helpers";
|
||||
|
||||
describe("node-pty", () => {
|
||||
const client = createClient();
|
||||
const pty = client.modules[Module.NodePty];
|
||||
|
||||
/**
|
||||
* Returns a function that when called returns a promise that resolves with
|
||||
* the next chunk of data from the process.
|
||||
*/
|
||||
const promisifyData = (proc: IPty): (() => Promise<string>) => {
|
||||
// Use a persistent callback instead of creating it in the promise since
|
||||
// otherwise we could lose data that comes in while no promise is listening.
|
||||
let onData: (() => void) | undefined;
|
||||
let buffer: string | undefined;
|
||||
proc.on("data", (data) => {
|
||||
// Remove everything that isn't a letter, number, or $ to avoid issues
|
||||
// with ANSI escape codes printing inside the test output.
|
||||
buffer = (buffer || "") + data.toString().replace(/[^a-zA-Z0-9$]/g, "");
|
||||
if (onData) {
|
||||
onData();
|
||||
}
|
||||
});
|
||||
|
||||
return (): Promise<string> => new Promise((resolve): void => {
|
||||
onData = (): void => {
|
||||
if (typeof buffer !== "undefined") {
|
||||
const data = buffer;
|
||||
buffer = undefined;
|
||||
onData = undefined;
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
onData();
|
||||
});
|
||||
};
|
||||
|
||||
it("should create shell", async () => {
|
||||
// Setting the config file to something that shouldn't exist so the test
|
||||
// isn't affected by custom configuration.
|
||||
const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
cols: 100,
|
||||
rows: 10,
|
||||
});
|
||||
|
||||
const getData = promisifyData(proc);
|
||||
|
||||
// Wait for [hostname@user]$
|
||||
let data = "";
|
||||
while (!data.includes("$")) {
|
||||
data = await getData();
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
});
|
||||
|
||||
it("should resize", async () => {
|
||||
// Requires the `tput lines` cmd to be available.
|
||||
// Setting the config file to something that shouldn't exist so the test
|
||||
// isn't affected by custom configuration.
|
||||
const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
cols: 10,
|
||||
rows: 912,
|
||||
});
|
||||
|
||||
const getData = promisifyData(proc);
|
||||
|
||||
proc.write("tput lines\n");
|
||||
|
||||
let data = "";
|
||||
while (!data.includes("912")) {
|
||||
data = await getData();
|
||||
}
|
||||
proc.resize(10, 219);
|
||||
proc.write("tput lines\n");
|
||||
|
||||
while (!data.includes("219")) {
|
||||
data = await getData();
|
||||
}
|
||||
|
||||
proc.kill();
|
||||
await new Promise((resolve): void => {
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispose", (done) => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
35
packages/protocol/test/spdlog.test.ts
Normal file
35
packages/protocol/test/spdlog.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("spdlog", () => {
|
||||
const client = createClient();
|
||||
const spdlog = client.modules[Module.Spdlog];
|
||||
const helper = new Helper("spdlog");
|
||||
|
||||
beforeAll(async () => {
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
it("should log to a file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
const logger = new spdlog.RotatingLogger("test logger", file, 10000, 10);
|
||||
logger.trace("trace");
|
||||
logger.debug("debug");
|
||||
logger.info("info");
|
||||
logger.warn("warn");
|
||||
logger.error("error");
|
||||
logger.critical("critical");
|
||||
logger.flush();
|
||||
await new Promise((resolve): number | NodeJS.Timer => setTimeout(resolve, 1000));
|
||||
expect(await util.promisify(fs.readFile)(file, "utf8"))
|
||||
.toContain("critical");
|
||||
});
|
||||
|
||||
it("should dispose", () => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
}, 100);
|
||||
});
|
||||
});
|
26
packages/protocol/test/trash.test.ts
Normal file
26
packages/protocol/test/trash.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import * as fs from "fs";
|
||||
import * as util from "util";
|
||||
import { Module } from "../src/common/proxy";
|
||||
import { createClient, Helper } from "./helpers";
|
||||
|
||||
describe("trash", () => {
|
||||
const client = createClient();
|
||||
const trash = client.modules[Module.Trash];
|
||||
const helper = new Helper("trash");
|
||||
|
||||
beforeAll(async () => {
|
||||
await helper.prepare();
|
||||
});
|
||||
|
||||
it("should trash a file", async () => {
|
||||
const file = await helper.createTmpFile();
|
||||
await trash.trash(file);
|
||||
expect(await util.promisify(fs.exists)(file)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should dispose", () => {
|
||||
setTimeout(() => {
|
||||
client.dispose();
|
||||
}, 100);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user