From d80f82ab98857d23ed66d7c4d15389b3ccb3dfd4 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 19 Feb 2019 14:21:04 -0600 Subject: [PATCH] Move and refactor fs tests --- packages/ide/package.json | 7 +- packages/ide/src/fill/client.ts | 4 +- packages/ide/src/fill/fs.ts | 20 +- packages/ide/test/fs.test.ts | 584 +++++++++++++++ packages/ide/yarn.lock | 110 +++ packages/package.json | 2 +- .../src/browser/{command.ts => evaluate.ts} | 0 packages/protocol/src/index.ts | 2 +- packages/protocol/src/node/evaluate.ts | 7 + packages/protocol/test/index.ts | 1 + packages/protocol/test/modules/fs.test.ts | 671 ------------------ 11 files changed, 726 insertions(+), 682 deletions(-) create mode 100644 packages/ide/test/fs.test.ts rename packages/protocol/src/browser/{command.ts => evaluate.ts} (100%) create mode 100644 packages/protocol/test/index.ts delete mode 100644 packages/protocol/test/modules/fs.test.ts diff --git a/packages/ide/package.json b/packages/ide/package.json index ac53ecb0..f229782c 100644 --- a/packages/ide/package.json +++ b/packages/ide/package.json @@ -1,5 +1,10 @@ { "name": "@coder/ide", "description": "Browser-based IDE client abstraction.", - "main": "src/index.ts" + "main": "src/index.ts", + "dependencies": {}, + "devDependencies": { + "@types/rimraf": "^2.0.2", + "rimraf": "^2.6.3" + } } diff --git a/packages/ide/src/fill/client.ts b/packages/ide/src/fill/client.ts index 5286225f..c076bae3 100644 --- a/packages/ide/src/fill/client.ts +++ b/packages/ide/src/fill/client.ts @@ -7,7 +7,7 @@ import { retry } from "../retry"; * A connection based on a web socket. Automatically reconnects and buffers * messages during connection. */ -class Connection implements ReadWriteConnection { +class WebsocketConnection implements ReadWriteConnection { private activeSocket: WebSocket | undefined; private readonly messageBuffer = []; private readonly socketTimeoutDelay = 60 * 1000; @@ -129,4 +129,4 @@ class Connection implements ReadWriteConnection { } // Global instance so all fills can use the same client. -export const client = new Client(new Connection()); +export const client = new Client(new WebsocketConnection()); diff --git a/packages/ide/src/fill/fs.ts b/packages/ide/src/fill/fs.ts index b3220282..f5ada92b 100644 --- a/packages/ide/src/fill/fs.ts +++ b/packages/ide/src/fill/fs.ts @@ -116,7 +116,7 @@ class FS { const ae = this.client.run((ae, path, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const str = fs.createWriteStream(path, options); - ae.on("write", (d) => str.write(_Buffer.from(d, "utf8"))); + ae.on("write", (d: string) => str.write(_Buffer.from(d, "utf8"))); ae.on("close", () => str.close()); str.on("close", () => ae.emit("close")); str.on("open", (fd) => ae.emit("open", fd)); @@ -141,7 +141,7 @@ class FS { }, }); - ae.on("open", (a) => this.emit("open", a)); + ae.on("open", (fd: number) => this.emit("open", fd)); ae.on("close", () => this.emit("close")); } @@ -597,7 +597,15 @@ class FS { }); } - public write = (fd: number, buffer: TBuffer, offset: number | undefined, length: number | undefined, position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => { + public write = (fd: number, buffer: TBuffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => { + if (typeof offset === "function") { + callback = offset; + offset = undefined; + } + if (typeof length === "function") { + callback = length; + length = undefined; + } if (typeof position === "function") { callback = position; position = undefined; @@ -662,9 +670,9 @@ class FS { return new class Watcher extends EventEmitter implements fs.FSWatcher { public constructor() { super(); - ae.on("change", (event, filename) => this.emit("change", event, filename)); - ae.on("error", (error) => this.emit("error", error)); - ae.on("listener", (event, filename) => listener && listener(event, filename)); + ae.on("change", (event: string, filename: string) => this.emit("change", event, filename)); + ae.on("error", (error: Error) => this.emit("error", error)); + ae.on("listener", (event: string, filename: string) => listener && listener(event, filename)); } public close(): void { diff --git a/packages/ide/test/fs.test.ts b/packages/ide/test/fs.test.ts new file mode 100644 index 00000000..c4e84ad0 --- /dev/null +++ b/packages/ide/test/fs.test.ts @@ -0,0 +1,584 @@ +import * as nativeFs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as util from "util"; +import * as rimraf from "rimraf"; +import { createClient } from "@coder/protocol/test"; + +const client = createClient(); +jest.mock("../src/fill/client", () => ({ client })); +const fs = require("../src/fill/fs") as typeof import("fs"); + +describe("fs", () => { + let i = 0; + const coderDir = path.join(os.tmpdir(), "coder"); + const testFile = path.join(__dirname, "fs.test.ts"); + const tmpFile = (): string => path.join(coderDir, `${i++}`); + const createTmpFile = async (): Promise => { + const tf = tmpFile(); + await util.promisify(nativeFs.writeFile)(tf, ""); + + return tf; + }; + + beforeAll(async () => { + await util.promisify(rimraf)(coderDir); + await util.promisify(nativeFs.mkdir)(coderDir); + }); + + describe("access", () => { + it("should access existing file", async () => { + await expect(util.promisify(fs.access)(testFile)) + .resolves.toBeUndefined(); + }); + + it("should fail to access nonexistent file", async () => { + await expect(util.promisify(fs.access)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("append", () => { + it("should append to existing file", async () => { + const file = await 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 = 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(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 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)(tmpFile(), "755")) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("chown", () => { + it("should chown existing file", async () => { + const file = await 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)(tmpFile(), 1, 1)) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("close", () => { + it("should close opened file", async () => { + const file = await 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 createTmpFile(); + const destination = 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)(tmpFile(), tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("createWriteStream", () => { + it("should write to file", async () => { + const file = tmpFile(); + const content = "howdy\nhow\nr\nu"; + const stream = fs.createWriteStream(file); + stream.on("open", (fd) => { + expect(fd).toBeDefined(); + stream.write(content); + stream.close(); + }); + await expect(new Promise((resolve): void => { + stream.on("close", async () => { + resolve(await util.promisify(nativeFs.readFile)(file, "utf8")); + }); + })).resolves.toBe(content); + }); + }); + + describe("exists", () => { + it("should output file exists", async () => { + await expect(util.promisify(fs.exists)(testFile)) + .resolves.toBe(true); + }); + + it("should output file does not exist", async () => { + await expect(util.promisify(fs.exists)(tmpFile())) + .resolves.toBe(false); + }); + }); + + describe("fchmod", () => { + it("should fchmod existing file", async () => { + const file = await 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 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 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)(testFile, "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 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 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 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 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)(tmpFile(), "755")) + .resolves.toBeUndefined(); + }); + }); + + describe("lchown", () => { + it("should lchown existing file", async () => { + const file = await 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)(tmpFile(), 1, 1)) + .resolves.toBeUndefined(); + }); + }); + + describe("link", () => { + it("should link existing file", async () => { + const source = await createTmpFile(); + const destination = 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)(tmpFile(), tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("lstat", () => { + it("should lstat existing file", async () => { + const stat = await util.promisify(nativeFs.lstat)(testFile); + await expect(util.promisify(fs.lstat)(testFile)) + .resolves.toMatchObject({ + size: stat.size, + }); + }); + + it("should fail to lstat non-existent file", async () => { + await expect(util.promisify(fs.lstat)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("mkdir", () => { + const target = tmpFile(); + it("should create nonexistent directory", async () => { + 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)(coderDir + "/")) + .resolves.toMatch(/^\/tmp\/coder\/[a-zA-Z0-9]{6}/); + }); + }); + + describe("open", () => { + it("should open existing file", async () => { + const fd = await util.promisify(fs.open)(testFile, "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)(tmpFile(), "r")) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("read", () => { + it("should read existing file", async () => { + const fd = await util.promisify(nativeFs.open)(testFile, "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)(testFile, "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)(testFile, "utf8"); + await expect(util.promisify(fs.readFile)(testFile, "utf8")) + .resolves.toEqual(content); + }); + + it("should fail to read nonexistent file", async () => { + await expect(util.promisify(fs.readFile)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("readdir", () => { + it("should read existing directory", async () => { + const paths = await util.promisify(nativeFs.readdir)(coderDir); + await expect(util.promisify(fs.readdir)(coderDir)) + .resolves.toEqual(paths); + }); + + it("should fail to read nonexistent directory", async () => { + await expect(util.promisify(fs.readdir)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("readlink", () => { + it("should read existing link", async () => { + const source = await createTmpFile(); + const destination = 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)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("realpath", () => { + it("should read real path of existing file", async () => { + const source = await createTmpFile(); + const destination = 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)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("rename", () => { + it("should rename existing file", async () => { + const source = await createTmpFile(); + const destination = 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)(tmpFile(), tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("rmdir", () => { + it("should rmdir existing directory", async () => { + const dir = 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)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("stat", () => { + it("should stat existing file", async () => { + const nativeStat = await util.promisify(nativeFs.stat)(testFile); + const stat = await util.promisify(fs.stat)(testFile); + expect(stat).toMatchObject({ + size: nativeStat.size, + }); + expect(stat.isFile()).toBe(true); + }); + + it("should stat existing folder", async () => { + const dir = 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 () => { + await expect(util.promisify(fs.stat)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("symlink", () => { + it("should symlink existing file", async () => { + const source = await createTmpFile(); + const destination = 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)(tmpFile(), tmpFile())) + .resolves.toBeUndefined(); + }); + }); + + describe("truncate", () => { + it("should truncate existing file", async () => { + const file = 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)(tmpFile(), 0)) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("unlink", () => { + it("should unlink existing file", async () => { + const file = await 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)(tmpFile())) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("utimes", () => { + it("should update times on existing file", async () => { + const file = await 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)(tmpFile(), 100, 100)) + .rejects.toThrow("ENOENT"); + }); + }); + + describe("write", () => { + it("should write to existing file", async () => { + const file = await 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 createTmpFile(); + await expect(util.promisify(fs.writeFile)(file, "howdy")) + .resolves.toBeUndefined(); + await expect(util.promisify(nativeFs.readFile)(file, "utf8")) + .resolves.toBe("howdy"); + }); + }); +}); diff --git a/packages/ide/yarn.lock b/packages/ide/yarn.lock index fb57ccd1..56f12cc0 100644 --- a/packages/ide/yarn.lock +++ b/packages/ide/yarn.lock @@ -2,3 +2,113 @@ # yarn lockfile v1 +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/glob@*": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node@*": + version "11.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14" + integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA== + +"@types/rimraf@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" + integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +glob@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +rimraf@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/packages/package.json b/packages/package.json index d3e86dc5..8fdcb413 100644 --- a/packages/package.json +++ b/packages/package.json @@ -23,7 +23,7 @@ ], "moduleNameMapper": { "^.+\\.(s?css|png|svg)$": "/../scripts/dummy.js", - "@coder/(.*)/testing": "/$1/testing", + "@coder/(.*)/test": "/$1/test", "@coder/(.*)": "/$1/src" }, "transform": { diff --git a/packages/protocol/src/browser/command.ts b/packages/protocol/src/browser/evaluate.ts similarity index 100% rename from packages/protocol/src/browser/command.ts rename to packages/protocol/src/browser/evaluate.ts diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index f0b57711..81904fa9 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -1,4 +1,4 @@ export * from "./browser/client"; -export { ActiveEval } from "./browser/command"; +export * from "./browser/evaluate"; export * from "./common/connection"; export * from "./common/util"; diff --git a/packages/protocol/src/node/evaluate.ts b/packages/protocol/src/node/evaluate.ts index 31067ae5..fa441be5 100644 --- a/packages/protocol/src/node/evaluate.ts +++ b/packages/protocol/src/node/evaluate.ts @@ -80,7 +80,14 @@ export const evaluate = (connection: SendableConnection, message: NewEvalMessage // tslint:enable no-any } : undefined, _Buffer: Buffer, + // When the client is ran from Webpack, it will replace + // __non_webpack_require__ with require, which we then need to provide to + // the sandbox. Since the server might also be using Webpack, we need to set + // it to the non-Webpack version when that's the case. Then we need to also + // provide __non_webpack_require__ for when the client doesn't run through + // Webpack meaning it doesn't get replaced with require (Jest for example). require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require, + __non_webpack_require__: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require, setTimeout, setInterval, clearTimeout, diff --git a/packages/protocol/test/index.ts b/packages/protocol/test/index.ts new file mode 100644 index 00000000..d4e09d7b --- /dev/null +++ b/packages/protocol/test/index.ts @@ -0,0 +1 @@ +export * from "./helpers"; diff --git a/packages/protocol/test/modules/fs.test.ts b/packages/protocol/test/modules/fs.test.ts deleted file mode 100644 index e668a2bd..00000000 --- a/packages/protocol/test/modules/fs.test.ts +++ /dev/null @@ -1,671 +0,0 @@ -import * as nativeFs from "fs"; -import * as os from "os"; -import * as path from "path"; -import { createClient } from "../helpers"; -import { FS } from "../../src/browser/modules/fs"; - -describe("fs", () => { - const client = createClient(); - const fs = new FS(client); - const testFile = path.join(__dirname, "fs.test.ts"); - const tmpFile = () => path.join(os.tmpdir(), `tmpfile-${Math.random()}`); - const createTmpFile = (): string => { - const tf = tmpFile(); - nativeFs.writeFileSync(tf, ""); - return tf; - }; - - describe("access", () => { - it("should access file", (done) => { - fs.access(testFile, undefined, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to access file", (done) => { - fs.access(tmpFile(), undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("append", () => { - it("should append to file", (done) => { - const file = createTmpFile(); - fs.appendFile(file, "howdy", undefined, (err) => { - expect(err).toBeUndefined(); - const content = nativeFs.readFileSync(file).toString(); - expect(content).toEqual("howdy"); - done(); - }); - }); - - it("should fail to append to file", (done) => { - fs.appendFile(tmpFile(), "howdy", undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("chmod", () => { - it("should chmod file", (done) => { - fs.chmod(createTmpFile(), "755", (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to chmod file", (done) => { - fs.chmod(tmpFile(), "755", (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("chown", () => { - it("should chown file", (done) => { - fs.chown(createTmpFile(), 1, 1, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to chown file", (done) => { - fs.chown(tmpFile(), 1, 1, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("close", () => { - it("should close file", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "r"); - fs.close(id, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to close file", (done) => { - fs.close(99999999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("copyFile", () => { - it("should copy file", (done) => { - const file = createTmpFile(); - fs.copyFile(file, tmpFile(), (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to copy file", (done) => { - fs.copyFile(tmpFile(), tmpFile(), (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("createWriteStream", () => { - it("should write to file", (done) => { - const file = 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.on("close", () => { - expect(nativeFs.readFileSync(file).toString()).toEqual(content); - done(); - }); - }); - }); - - describe("exists", () => { - it("should output file exists", (done) => { - fs.exists(testFile, (exists) => { - expect(exists).toBeTruthy(); - done(); - }); - }); - - it("should output file does not exist", (done) => { - fs.exists(tmpFile(), (exists) => { - expect(exists).toBeFalsy(); - done(); - }); - }); - }); - - describe("fchmod", () => { - it("should fchmod", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "r"); - fs.fchmod(id, "755", (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to fchmod", (done) => { - fs.fchmod(2242342, "755", (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("fchown", () => { - it("should fchown", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "r"); - fs.fchown(id, 1, 1, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to fchown", (done) => { - fs.fchown(99999, 1, 1, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("fdatasync", () => { - it("should fdatasync", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "r"); - fs.fdatasync(id, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to fdatasync", (done) => { - fs.fdatasync(99999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("fstat", () => { - it("should fstat", (done) => { - const id = nativeFs.openSync(testFile, "r"); - fs.fstat(id, (err, stats) => { - expect(err).toBeUndefined(); - expect(stats.size).toBeGreaterThan(0); - done(); - }); - }); - - it("should fail to fstat", (done) => { - fs.fstat(9999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("fsync", () => { - it("should fsync", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "r"); - fs.fsync(id, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to fsync", (done) => { - fs.fsync(99999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("ftruncate", () => { - it("should ftruncate", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "w"); - fs.ftruncate(id, 1, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to ftruncate", (done) => { - fs.ftruncate(99999, 9999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("futimes", () => { - it("should futimes", (done) => { - const file = createTmpFile(); - const id = nativeFs.openSync(file, "w"); - fs.futimes(id, 1, 1, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to futimes", (done) => { - fs.futimes(99999, 9999, 9999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("lchmod", () => { - it("should lchmod file", (done) => { - fs.lchmod(createTmpFile(), "755", (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to lchmod file", (done) => { - fs.lchmod(tmpFile(), "755", (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("lchown", () => { - it("should lchown file", (done) => { - fs.lchown(createTmpFile(), 1, 1, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to lchown file", (done) => { - fs.lchown(tmpFile(), 1, 1, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("link", () => { - it("should link file", (done) => { - const newFile = createTmpFile(); - const targetFile = tmpFile(); - fs.link(newFile, targetFile, (err) => { - expect(err).toBeUndefined(); - expect(nativeFs.existsSync(targetFile)).toBeTruthy(); - done(); - }); - }); - - it("should fail to link file", (done) => { - fs.link(tmpFile(), tmpFile(), (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("lstat", () => { - it("should lstat", (done) => { - fs.lstat(testFile, (err, stats) => { - expect(err).toBeUndefined(); - expect(stats.size).toBeGreaterThan(0); - done(); - }); - }); - - it("should fail to lstat", (done) => { - fs.lstat(path.join(__dirname, "no-exist"), (err, stats) => { - expect(err).toBeDefined(); - expect(stats).toBeUndefined(); - done(); - }); - }); - }); - - describe("mkdir", () => { - const target = tmpFile(); - it("should create directory", (done) => { - fs.mkdir(target, undefined, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to create directory", (done) => { - fs.mkdir(target, undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("mkdtemp", () => { - it("should create temp dir", (done) => { - fs.mkdtemp(path.join(os.tmpdir(), "example"), undefined, (err, folder) => { - expect(err).toBeUndefined(); - done(); - }); - }); - }); - - describe("open", () => { - it("should open file", (done) => { - fs.open(testFile, "r", undefined, (err, fd) => { - expect(err).toBeUndefined(); - expect(fd).toBeDefined(); - done(); - }); - }); - - it("should fail to open file", (done) => { - fs.open("asdfoksfg", "r", undefined, (err, fd) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("read", () => { - it("should read file", async () => { - const fd = nativeFs.openSync(testFile, "r"); - const stat = nativeFs.fstatSync(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 new Promise((res, rej) => { - fs.read(fd, buffer, bytesRead, chunkSize, bytesRead, (err) => { - if (err) { - rej(err); - } else { - res(); - } - }); - }); - bytesRead += chunkSize; - } - - expect(buffer.toString()).toEqual(nativeFs.readFileSync(testFile).toString()); - }); - - it("should fail to read file", (done) => { - fs.read(99999, new Buffer(10), 9999, 999, 999, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("readFile", () => { - it("should read file", (done) => { - fs.readFile(testFile, undefined, (err, data) => { - expect(err).toBeUndefined(); - expect(data.toString()).toEqual(nativeFs.readFileSync(testFile).toString()); - done(); - }); - }); - - it("should fail to read file", (done) => { - fs.readFile("donkey", undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("readdir", () => { - it("should read directory", (done) => { - fs.readdir(__dirname, undefined, (err, paths) => { - expect(err).toBeUndefined(); - expect(paths.length).toBeGreaterThan(0); - done(); - }); - }); - - it("should fail to read directory", (done) => { - fs.readdir("moocow", undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("readlink", () => { - it("should read link", (done) => { - const srcFile = createTmpFile(); - const linkedFile = tmpFile(); - nativeFs.symlinkSync(srcFile, linkedFile); - fs.readlink(linkedFile, undefined, (err, link) => { - expect(err).toBeUndefined(); - expect(link).toEqual(srcFile); - done(); - }); - }); - - it("should fail to read link", (done) => { - fs.readlink(tmpFile(), undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("realpath", () => { - it("should read real path", (done) => { - const srcFile = createTmpFile(); - const linkedFile = tmpFile(); - nativeFs.symlinkSync(srcFile, linkedFile); - fs.realpath(linkedFile, undefined, (err, link) => { - expect(err).toBeUndefined(); - expect(link).toEqual(srcFile); - done(); - }); - }); - - it("should fail to read real path", (done) => { - fs.realpath(tmpFile(), undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("rename", () => { - it("should rename file", (done) => { - const srcFile = createTmpFile(); - const targetFile = tmpFile(); - fs.rename(srcFile, targetFile, (err) => { - expect(err).toBeUndefined(); - expect(nativeFs.existsSync(targetFile)).toBeTruthy(); - done(); - }); - }); - - it("should fail to rename file", (done) => { - fs.rename(tmpFile(), tmpFile(), (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("rmdir", () => { - it("should rmdir", (done) => { - const srcFile = tmpFile(); - nativeFs.mkdirSync(srcFile); - fs.rmdir(srcFile, (err) => { - expect(err).toBeUndefined(); - expect(nativeFs.existsSync(srcFile)).toBeFalsy(); - done(); - }); - }); - - it("should fail to rmdir", (done) => { - fs.rmdir(tmpFile(), (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("stat", () => { - it("should stat file", (done) => { - fs.stat(testFile, (err, stats) => { - expect(err).toBeUndefined(); - expect(stats.size).toBeGreaterThan(0); - expect(stats.isFile()).toBeTruthy(); - expect(stats.isFIFO()).toBeFalsy(); - done(); - }); - }); - - it("should stat folder", (done) => { - const dir = tmpFile(); - nativeFs.mkdirSync(dir); - - fs.stat(dir, (err, stats) => { - expect(err).toBeUndefined(); - expect(stats.isDirectory()).toBeTruthy(); - done(); - }); - }); - - it("should fail to stat", (done) => { - fs.stat(path.join(__dirname, "no-exist"), (err, stats) => { - expect(err).toBeDefined(); - expect(stats).toBeUndefined(); - done(); - }); - }); - }); - - describe("symlink", () => { - it("should symlink file", (done) => { - const newFile = createTmpFile(); - const targetFile = tmpFile(); - fs.symlink(newFile, targetFile, "file", (err) => { - expect(err).toBeUndefined(); - expect(nativeFs.existsSync(targetFile)).toBeTruthy(); - done(); - }); - }); - - it("should fail to symlink file", (done) => { - fs.symlink(tmpFile(), tmpFile(), "file", (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("truncate", () => { - it("should truncate file", (done) => { - const newFile = tmpFile(); - nativeFs.writeFileSync(newFile, "hiiiiii"); - fs.truncate(newFile, 2, (err) => { - expect(err).toBeUndefined(); - expect(nativeFs.statSync(newFile).size).toEqual(2); - done(); - }); - }); - - it("should fail to truncate file", (done) => { - fs.truncate(tmpFile(), 0, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("unlink", () => { - it("should unlink file", (done) => { - const newFile = createTmpFile(); - const targetFile = tmpFile(); - nativeFs.symlinkSync(newFile, targetFile, "file"); - fs.unlink(targetFile, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to unlink file", (done) => { - fs.unlink(tmpFile(), (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("utimes", () => { - it("should update times on file", (done) => { - fs.utimes(createTmpFile(), 100, 100, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - - it("should fail to update times", (done) => { - fs.utimes(tmpFile(), 100, 100, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("write", () => { - it("should write to file", (done) => { - const file = createTmpFile(); - const fd = nativeFs.openSync(file, "w"); - fs.write(fd, Buffer.from("hi"), undefined, undefined, undefined, (err, written) => { - expect(err).toBeUndefined(); - expect(written).toEqual(2); - nativeFs.closeSync(fd); - expect(nativeFs.readFileSync(file).toString()).toEqual("hi"); - done(); - }); - }); - - it("should fail to write to file", (done) => { - fs.write(100000, Buffer.from("wowow"), undefined, undefined, undefined, (err) => { - expect(err).toBeDefined(); - done(); - }); - }); - }); - - describe("writeFile", () => { - it("should write file", (done) => { - fs.writeFile(createTmpFile(), "howdy", undefined, (err) => { - expect(err).toBeUndefined(); - done(); - }); - }); - }); -}); \ No newline at end of file