diff options
Diffstat (limited to 'cli/tests/unit')
70 files changed, 12570 insertions, 0 deletions
diff --git a/cli/tests/unit/README.md b/cli/tests/unit/README.md new file mode 100644 index 000000000..c333b0046 --- /dev/null +++ b/cli/tests/unit/README.md @@ -0,0 +1,80 @@ +# Deno runtime tests + +Files in this directory are unit tests for Deno runtime. + +They are run under compiled Deno binary as opposed to files in `cli/js/` which +are bundled and snapshotted using `deno_typescript` crate. + +Testing Deno runtime code requires checking API under different runtime +permissions (ie. running with different `--allow-*` flags). To accomplish this +all tests exercised are created using `unitTest()` function. + +``` +import { unitTest } from "./test_util.ts"; + +unitTest(function simpleTestFn(): void { + // test code here +}); + +unitTest({ + ignore: Deno.build.os === "windows", + perms: { read: true, write: true }, + }, + function complexTestFn(): void { + // test code here + } +); +``` + +`unitTest` is is a wrapper function that enhances `Deno.test()` API in several +ways: + +- ability to conditionally skip tests using `UnitTestOptions.skip` +- ability to register required set of permissions for given test case using + `UnitTestOptions.perms` +- sanitization of resources - ensuring that tests close all opened resources + preventing interference between tests +- sanitization of async ops - ensuring that tests don't leak async ops by + ensuring that all started async ops are done before test finishes + +## Running tests + +`unit_test_runner.ts` is the main script used to run unit tests. + +Runner discovers required permissions combinations by loading +`cli/tests/unit/unit_tests.ts` and going through all registered instances of +`unitTest`. + +There are three ways to run `unit_test_runner.ts`: + +``` +# Run all tests. Spawns worker processes for each discovered permission +# combination: +target/debug/deno run -A cli/tests/unit/unit_test_runner.ts --master + +# By default all output of worker processes is discarded; for debug purposes +# the --verbose flag preserves output from the worker +target/debug/deno run -A cli/tests/unit/unit_test_runner.ts --master --verbose + +# Run subset of tests that don't require any permissions +target/debug/deno run --unstable cli/tests/unit/unit_test_runner.ts + +# Run subset tests that require "net" and "read" permissions +target/debug/deno run --unstable --allow-net --allow-read cli/tests/unit/unit_test_runner.ts + +# "worker" mode communicates with parent using TCP socket on provided address; +# after initial setup drops permissions to specified set. It shouldn't be used +# directly, only be "master" process. +target/debug/deno run -A cli/tests/unit/unit_test_runner.ts --worker --addr=127.0.0.1:4500 --perms=net,write,run + +# Run specific tests +target/debug/deno run --unstable --allow-net cli/tests/unit/unit_test_runner.ts -- netTcpListenClose + +RUST_BACKTRACE=1 cargo run -- run --unstable --allow-read --allow-write cli/tests/unit/unit_test_runner.ts -- netUnixDialListen +``` + +### Http server + +`tools/http_server.py` is required to run when one's running unit tests. During +CI it's spawned automatically, but if you want to run tests manually make sure +that server is spawned otherwise there'll be cascade of test failures. diff --git a/cli/tests/unit/abort_controller_test.ts b/cli/tests/unit/abort_controller_test.ts new file mode 100644 index 000000000..ecc1abb88 --- /dev/null +++ b/cli/tests/unit/abort_controller_test.ts @@ -0,0 +1,56 @@ +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest(function basicAbortController() { + const controller = new AbortController(); + assert(controller); + const { signal } = controller; + assert(signal); + assertEquals(signal.aborted, false); + controller.abort(); + assertEquals(signal.aborted, true); +}); + +unitTest(function signalCallsOnabort() { + const controller = new AbortController(); + const { signal } = controller; + let called = false; + signal.onabort = (evt): void => { + assert(evt); + assertEquals(evt.type, "abort"); + called = true; + }; + controller.abort(); + assert(called); +}); + +unitTest(function signalEventListener() { + const controller = new AbortController(); + const { signal } = controller; + let called = false; + signal.addEventListener("abort", function (ev) { + assert(this === signal); + assertEquals(ev.type, "abort"); + called = true; + }); + controller.abort(); + assert(called); +}); + +unitTest(function onlyAbortsOnce() { + const controller = new AbortController(); + const { signal } = controller; + let called = 0; + signal.addEventListener("abort", () => called++); + signal.onabort = (): void => { + called++; + }; + controller.abort(); + assertEquals(called, 2); + controller.abort(); + assertEquals(called, 2); +}); + +unitTest(function controllerHasProperToString() { + const actual = Object.prototype.toString.call(new AbortController()); + assertEquals(actual, "[object AbortController]"); +}); diff --git a/cli/tests/unit/blob_test.ts b/cli/tests/unit/blob_test.ts new file mode 100644 index 000000000..70f8fb3d5 --- /dev/null +++ b/cli/tests/unit/blob_test.ts @@ -0,0 +1,92 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; +import { concat } from "../../../std/bytes/mod.ts"; +import { decode } from "../../../std/encoding/utf8.ts"; + +unitTest(function blobString(): void { + const b1 = new Blob(["Hello World"]); + const str = "Test"; + const b2 = new Blob([b1, str]); + assertEquals(b2.size, b1.size + str.length); +}); + +unitTest(function blobBuffer(): void { + const buffer = new ArrayBuffer(12); + const u8 = new Uint8Array(buffer); + const f1 = new Float32Array(buffer); + const b1 = new Blob([buffer, u8]); + assertEquals(b1.size, 2 * u8.length); + const b2 = new Blob([b1, f1]); + assertEquals(b2.size, 3 * u8.length); +}); + +unitTest(function blobSlice(): void { + const blob = new Blob(["Deno", "Foo"]); + const b1 = blob.slice(0, 3, "Text/HTML"); + assert(b1 instanceof Blob); + assertEquals(b1.size, 3); + assertEquals(b1.type, "text/html"); + const b2 = blob.slice(-1, 3); + assertEquals(b2.size, 0); + const b3 = blob.slice(100, 3); + assertEquals(b3.size, 0); + const b4 = blob.slice(0, 10); + assertEquals(b4.size, blob.size); +}); + +unitTest(function blobInvalidType(): void { + const blob = new Blob(["foo"], { + type: "\u0521", + }); + + assertEquals(blob.type, ""); +}); + +unitTest(function blobShouldNotThrowError(): void { + let hasThrown = false; + + try { + const options1: object = { + ending: "utf8", + hasOwnProperty: "hasOwnProperty", + }; + const options2: object = Object.create(null); + new Blob(["Hello World"], options1); + new Blob(["Hello World"], options2); + } catch { + hasThrown = true; + } + + assertEquals(hasThrown, false); +}); + +unitTest(function nativeEndLine(): void { + const options: object = { + ending: "native", + }; + const blob = new Blob(["Hello\nWorld"], options); + + assertEquals(blob.size, Deno.build.os === "windows" ? 12 : 11); +}); + +unitTest(async function blobText(): Promise<void> { + const blob = new Blob(["Hello World"]); + assertEquals(await blob.text(), "Hello World"); +}); + +unitTest(async function blobStream(): Promise<void> { + const blob = new Blob(["Hello World"]); + const stream = blob.stream(); + assert(stream instanceof ReadableStream); + const reader = stream.getReader(); + let bytes = new Uint8Array(); + const read = async (): Promise<void> => { + const { done, value } = await reader.read(); + if (!done && value) { + bytes = concat(bytes, value); + return read(); + } + }; + await read(); + assertEquals(decode(bytes), "Hello World"); +}); diff --git a/cli/tests/unit/body_test.ts b/cli/tests/unit/body_test.ts new file mode 100644 index 000000000..c8f783e04 --- /dev/null +++ b/cli/tests/unit/body_test.ts @@ -0,0 +1,74 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals, assert } from "./test_util.ts"; + +// just a hack to get a body object +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function buildBody(body: any): Body { + const stub = new Request("", { + body: body, + }); + return stub as Body; +} + +const intArrays = [ + Int8Array, + Int16Array, + Int32Array, + Uint8Array, + Uint16Array, + Uint32Array, + Uint8ClampedArray, + Float32Array, + Float64Array, +]; +unitTest(async function arrayBufferFromByteArrays(): Promise<void> { + const buffer = new TextEncoder().encode("ahoyhoy8").buffer; + + for (const type of intArrays) { + const body = buildBody(new type(buffer)); + const text = new TextDecoder("utf-8").decode(await body.arrayBuffer()); + assertEquals(text, "ahoyhoy8"); + } +}); + +//FormData +unitTest( + { perms: { net: true } }, + async function bodyMultipartFormData(): Promise<void> { + const response = await fetch( + "http://localhost:4545/cli/tests/subdir/multipart_form_data.txt" + ); + const text = await response.text(); + + const body = buildBody(text); + + // @ts-ignore + body.contentType = "multipart/form-data;boundary=boundary"; + + const formData = await body.formData(); + assert(formData.has("field_1")); + assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n"); + assert(formData.has("field_2")); + } +); + +unitTest( + { perms: { net: true } }, + async function bodyURLEncodedFormData(): Promise<void> { + const response = await fetch( + "http://localhost:4545/cli/tests/subdir/form_urlencoded.txt" + ); + const text = await response.text(); + + const body = buildBody(text); + + // @ts-ignore + body.contentType = "application/x-www-form-urlencoded"; + + const formData = await body.formData(); + assert(formData.has("field_1")); + assertEquals(formData.get("field_1")!.toString(), "Hi"); + assert(formData.has("field_2")); + assertEquals(formData.get("field_2")!.toString(), "<Deno>"); + } +); diff --git a/cli/tests/unit/buffer_test.ts b/cli/tests/unit/buffer_test.ts new file mode 100644 index 000000000..ac2d7db7b --- /dev/null +++ b/cli/tests/unit/buffer_test.ts @@ -0,0 +1,294 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This code has been ported almost directly from Go's src/bytes/buffer_test.go +// Copyright 2009 The Go Authors. All rights reserved. BSD license. +// https://github.com/golang/go/blob/master/LICENSE +import { + assertEquals, + assert, + assertStrContains, + unitTest, +} from "./test_util.ts"; + +const { Buffer, readAll, readAllSync, writeAll, writeAllSync } = Deno; +type Buffer = Deno.Buffer; + +// N controls how many iterations of certain checks are performed. +const N = 100; +let testBytes: Uint8Array | null; +let testString: string | null; + +function init(): void { + if (testBytes == null) { + testBytes = new Uint8Array(N); + for (let i = 0; i < N; i++) { + testBytes[i] = "a".charCodeAt(0) + (i % 26); + } + const decoder = new TextDecoder(); + testString = decoder.decode(testBytes); + } +} + +function check(buf: Deno.Buffer, s: string): void { + const bytes = buf.bytes(); + assertEquals(buf.length, bytes.byteLength); + const decoder = new TextDecoder(); + const bytesStr = decoder.decode(bytes); + assertEquals(bytesStr, s); + assertEquals(buf.length, s.length); +} + +// Fill buf through n writes of byte slice fub. +// The initial contents of buf corresponds to the string s; +// the result is the final contents of buf returned as a string. +async function fillBytes( + buf: Buffer, + s: string, + n: number, + fub: Uint8Array +): Promise<string> { + check(buf, s); + for (; n > 0; n--) { + const m = await buf.write(fub); + assertEquals(m, fub.byteLength); + const decoder = new TextDecoder(); + s += decoder.decode(fub); + check(buf, s); + } + return s; +} + +// Empty buf through repeated reads into fub. +// The initial contents of buf corresponds to the string s. +async function empty(buf: Buffer, s: string, fub: Uint8Array): Promise<void> { + check(buf, s); + while (true) { + const r = await buf.read(fub); + if (r === null) { + break; + } + s = s.slice(r); + check(buf, s); + } + check(buf, ""); +} + +function repeat(c: string, bytes: number): Uint8Array { + assertEquals(c.length, 1); + const ui8 = new Uint8Array(bytes); + ui8.fill(c.charCodeAt(0)); + return ui8; +} + +unitTest(function bufferNewBuffer(): void { + init(); + assert(testBytes); + assert(testString); + const buf = new Buffer(testBytes.buffer as ArrayBuffer); + check(buf, testString); +}); + +unitTest(async function bufferBasicOperations(): Promise<void> { + init(); + assert(testBytes); + assert(testString); + const buf = new Buffer(); + for (let i = 0; i < 5; i++) { + check(buf, ""); + + buf.reset(); + check(buf, ""); + + buf.truncate(0); + check(buf, ""); + + let n = await buf.write(testBytes.subarray(0, 1)); + assertEquals(n, 1); + check(buf, "a"); + + n = await buf.write(testBytes.subarray(1, 2)); + assertEquals(n, 1); + check(buf, "ab"); + + n = await buf.write(testBytes.subarray(2, 26)); + assertEquals(n, 24); + check(buf, testString.slice(0, 26)); + + buf.truncate(26); + check(buf, testString.slice(0, 26)); + + buf.truncate(20); + check(buf, testString.slice(0, 20)); + + await empty(buf, testString.slice(0, 20), new Uint8Array(5)); + await empty(buf, "", new Uint8Array(100)); + + // TODO buf.writeByte() + // TODO buf.readByte() + } +}); + +unitTest(async function bufferReadEmptyAtEOF(): Promise<void> { + // check that EOF of 'buf' is not reached (even though it's empty) if + // results are written to buffer that has 0 length (ie. it can't store any data) + const buf = new Buffer(); + const zeroLengthTmp = new Uint8Array(0); + const result = await buf.read(zeroLengthTmp); + assertEquals(result, 0); +}); + +unitTest(async function bufferLargeByteWrites(): Promise<void> { + init(); + const buf = new Buffer(); + const limit = 9; + for (let i = 3; i < limit; i += 3) { + const s = await fillBytes(buf, "", 5, testBytes!); + await empty(buf, s, new Uint8Array(Math.floor(testString!.length / i))); + } + check(buf, ""); +}); + +unitTest(async function bufferTooLargeByteWrites(): Promise<void> { + init(); + const tmp = new Uint8Array(72); + const growLen = Number.MAX_VALUE; + const xBytes = repeat("x", 0); + const buf = new Buffer(xBytes.buffer as ArrayBuffer); + await buf.read(tmp); + + let err; + try { + buf.grow(growLen); + } catch (e) { + err = e; + } + + assert(err instanceof Error); + assertStrContains(err.message, "grown beyond the maximum size"); +}); + +unitTest(async function bufferLargeByteReads(): Promise<void> { + init(); + assert(testBytes); + assert(testString); + const buf = new Buffer(); + for (let i = 3; i < 30; i += 3) { + const n = Math.floor(testBytes.byteLength / i); + const s = await fillBytes(buf, "", 5, testBytes.subarray(0, n)); + await empty(buf, s, new Uint8Array(testString.length)); + } + check(buf, ""); +}); + +unitTest(function bufferCapWithPreallocatedSlice(): void { + const buf = new Buffer(new ArrayBuffer(10)); + assertEquals(buf.capacity, 10); +}); + +unitTest(async function bufferReadFrom(): Promise<void> { + init(); + assert(testBytes); + assert(testString); + const buf = new Buffer(); + for (let i = 3; i < 30; i += 3) { + const s = await fillBytes( + buf, + "", + 5, + testBytes.subarray(0, Math.floor(testBytes.byteLength / i)) + ); + const b = new Buffer(); + await b.readFrom(buf); + const fub = new Uint8Array(testString.length); + await empty(b, s, fub); + } +}); + +unitTest(async function bufferReadFromSync(): Promise<void> { + init(); + assert(testBytes); + assert(testString); + const buf = new Buffer(); + for (let i = 3; i < 30; i += 3) { + const s = await fillBytes( + buf, + "", + 5, + testBytes.subarray(0, Math.floor(testBytes.byteLength / i)) + ); + const b = new Buffer(); + b.readFromSync(buf); + const fub = new Uint8Array(testString.length); + await empty(b, s, fub); + } +}); + +unitTest(async function bufferTestGrow(): Promise<void> { + const tmp = new Uint8Array(72); + for (const startLen of [0, 100, 1000, 10000, 100000]) { + const xBytes = repeat("x", startLen); + for (const growLen of [0, 100, 1000, 10000, 100000]) { + const buf = new Buffer(xBytes.buffer as ArrayBuffer); + // If we read, this affects buf.off, which is good to test. + const nread = (await buf.read(tmp)) ?? 0; + buf.grow(growLen); + const yBytes = repeat("y", growLen); + await buf.write(yBytes); + // Check that buffer has correct data. + assertEquals( + buf.bytes().subarray(0, startLen - nread), + xBytes.subarray(nread) + ); + assertEquals( + buf.bytes().subarray(startLen - nread, startLen - nread + growLen), + yBytes + ); + } + } +}); + +unitTest(async function testReadAll(): Promise<void> { + init(); + assert(testBytes); + const reader = new Buffer(testBytes.buffer as ArrayBuffer); + const actualBytes = await readAll(reader); + assertEquals(testBytes.byteLength, actualBytes.byteLength); + for (let i = 0; i < testBytes.length; ++i) { + assertEquals(testBytes[i], actualBytes[i]); + } +}); + +unitTest(function testReadAllSync(): void { + init(); + assert(testBytes); + const reader = new Buffer(testBytes.buffer as ArrayBuffer); + const actualBytes = readAllSync(reader); + assertEquals(testBytes.byteLength, actualBytes.byteLength); + for (let i = 0; i < testBytes.length; ++i) { + assertEquals(testBytes[i], actualBytes[i]); + } +}); + +unitTest(async function testWriteAll(): Promise<void> { + init(); + assert(testBytes); + const writer = new Buffer(); + await writeAll(writer, testBytes); + const actualBytes = writer.bytes(); + assertEquals(testBytes.byteLength, actualBytes.byteLength); + for (let i = 0; i < testBytes.length; ++i) { + assertEquals(testBytes[i], actualBytes[i]); + } +}); + +unitTest(function testWriteAllSync(): void { + init(); + assert(testBytes); + const writer = new Buffer(); + writeAllSync(writer, testBytes); + const actualBytes = writer.bytes(); + assertEquals(testBytes.byteLength, actualBytes.byteLength); + for (let i = 0; i < testBytes.length; ++i) { + assertEquals(testBytes[i], actualBytes[i]); + } +}); diff --git a/cli/tests/unit/build_test.ts b/cli/tests/unit/build_test.ts new file mode 100644 index 000000000..987ed8d39 --- /dev/null +++ b/cli/tests/unit/build_test.ts @@ -0,0 +1,10 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +unitTest(function buildInfo(): void { + // Deno.build is injected by rollup at compile time. Here + // we check it has been properly transformed. + const { arch, os } = Deno.build; + assert(arch.length > 0); + assert(os === "darwin" || os === "windows" || os === "linux"); +}); diff --git a/cli/tests/unit/chmod_test.ts b/cli/tests/unit/chmod_test.ts new file mode 100644 index 000000000..d0d17629d --- /dev/null +++ b/cli/tests/unit/chmod_test.ts @@ -0,0 +1,149 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + function chmodSyncSuccess(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + + Deno.chmodSync(filename, 0o777); + + const fileInfo = Deno.statSync(filename); + assert(fileInfo.mode); + assertEquals(fileInfo.mode & 0o777, 0o777); + } +); + +// Check symlink when not on windows +unitTest( + { + ignore: Deno.build.os === "windows", + perms: { read: true, write: true }, + }, + function chmodSyncSymlinkSuccess(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const tempDir = Deno.makeTempDirSync(); + + const filename = tempDir + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const symlinkName = tempDir + "/test_symlink.txt"; + Deno.symlinkSync(filename, symlinkName); + + let symlinkInfo = Deno.lstatSync(symlinkName); + assert(symlinkInfo.mode); + const symlinkMode = symlinkInfo.mode & 0o777; // platform dependent + + Deno.chmodSync(symlinkName, 0o777); + + // Change actual file mode, not symlink + const fileInfo = Deno.statSync(filename); + assert(fileInfo.mode); + assertEquals(fileInfo.mode & 0o777, 0o777); + symlinkInfo = Deno.lstatSync(symlinkName); + assert(symlinkInfo.mode); + assertEquals(symlinkInfo.mode & 0o777, symlinkMode); + } +); + +unitTest({ perms: { write: true } }, function chmodSyncFailure(): void { + let err; + try { + const filename = "/badfile.txt"; + Deno.chmodSync(filename, 0o777); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); +}); + +unitTest({ perms: { write: false } }, function chmodSyncPerm(): void { + let err; + try { + Deno.chmodSync("/somefile.txt", 0o777); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + async function chmodSuccess(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + + await Deno.chmod(filename, 0o777); + + const fileInfo = Deno.statSync(filename); + assert(fileInfo.mode); + assertEquals(fileInfo.mode & 0o777, 0o777); + } +); + +// Check symlink when not on windows + +unitTest( + { + ignore: Deno.build.os === "windows", + perms: { read: true, write: true }, + }, + async function chmodSymlinkSuccess(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const tempDir = Deno.makeTempDirSync(); + + const filename = tempDir + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const symlinkName = tempDir + "/test_symlink.txt"; + Deno.symlinkSync(filename, symlinkName); + + let symlinkInfo = Deno.lstatSync(symlinkName); + assert(symlinkInfo.mode); + const symlinkMode = symlinkInfo.mode & 0o777; // platform dependent + + await Deno.chmod(symlinkName, 0o777); + + // Just change actual file mode, not symlink + const fileInfo = Deno.statSync(filename); + assert(fileInfo.mode); + assertEquals(fileInfo.mode & 0o777, 0o777); + symlinkInfo = Deno.lstatSync(symlinkName); + assert(symlinkInfo.mode); + assertEquals(symlinkInfo.mode & 0o777, symlinkMode); + } +); + +unitTest({ perms: { write: true } }, async function chmodFailure(): Promise< + void +> { + let err; + try { + const filename = "/badfile.txt"; + await Deno.chmod(filename, 0o777); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); +}); + +unitTest({ perms: { write: false } }, async function chmodPerm(): Promise< + void +> { + let err; + try { + await Deno.chmod("/somefile.txt", 0o777); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); diff --git a/cli/tests/unit/chown_test.ts b/cli/tests/unit/chown_test.ts new file mode 100644 index 000000000..724ea5a21 --- /dev/null +++ b/cli/tests/unit/chown_test.ts @@ -0,0 +1,147 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals, assert } from "./test_util.ts"; + +// chown on Windows is noop for now, so ignore its testing on Windows +if (Deno.build.os !== "windows") { + async function getUidAndGid(): Promise<{ uid: number; gid: number }> { + // get the user ID and group ID of the current process + const uidProc = Deno.run({ + stdout: "piped", + cmd: ["python", "-c", "import os; print(os.getuid())"], + }); + const gidProc = Deno.run({ + stdout: "piped", + cmd: ["python", "-c", "import os; print(os.getgid())"], + }); + + assertEquals((await uidProc.status()).code, 0); + assertEquals((await gidProc.status()).code, 0); + const uid = parseInt( + new TextDecoder("utf-8").decode(await uidProc.output()) + ); + uidProc.close(); + const gid = parseInt( + new TextDecoder("utf-8").decode(await gidProc.output()) + ); + gidProc.close(); + + return { uid, gid }; + } + + unitTest(async function chownNoWritePermission(): Promise<void> { + const filePath = "chown_test_file.txt"; + try { + await Deno.chown(filePath, 1000, 1000); + } catch (e) { + assert(e instanceof Deno.errors.PermissionDenied); + } + }); + + unitTest( + { perms: { run: true, write: true } }, + async function chownSyncFileNotExist(): Promise<void> { + const { uid, gid } = await getUidAndGid(); + const filePath = Deno.makeTempDirSync() + "/chown_test_file.txt"; + + try { + Deno.chownSync(filePath, uid, gid); + } catch (e) { + assert(e instanceof Deno.errors.NotFound); + } + } + ); + + unitTest( + { perms: { run: true, write: true } }, + async function chownFileNotExist(): Promise<void> { + const { uid, gid } = await getUidAndGid(); + const filePath = (await Deno.makeTempDir()) + "/chown_test_file.txt"; + + try { + await Deno.chown(filePath, uid, gid); + } catch (e) { + assert(e instanceof Deno.errors.NotFound); + } + } + ); + + unitTest( + { perms: { write: true } }, + function chownSyncPermissionDenied(): void { + const enc = new TextEncoder(); + const dirPath = Deno.makeTempDirSync(); + const filePath = dirPath + "/chown_test_file.txt"; + const fileData = enc.encode("Hello"); + Deno.writeFileSync(filePath, fileData); + + try { + // try changing the file's owner to root + Deno.chownSync(filePath, 0, 0); + } catch (e) { + assert(e instanceof Deno.errors.PermissionDenied); + } + Deno.removeSync(dirPath, { recursive: true }); + } + ); + + unitTest( + { perms: { write: true } }, + async function chownPermissionDenied(): Promise<void> { + const enc = new TextEncoder(); + const dirPath = await Deno.makeTempDir(); + const filePath = dirPath + "/chown_test_file.txt"; + const fileData = enc.encode("Hello"); + await Deno.writeFile(filePath, fileData); + + try { + // try changing the file's owner to root + await Deno.chown(filePath, 0, 0); + } catch (e) { + assert(e instanceof Deno.errors.PermissionDenied); + } + await Deno.remove(dirPath, { recursive: true }); + } + ); + + unitTest( + { perms: { run: true, write: true } }, + async function chownSyncSucceed(): Promise<void> { + // TODO: when a file's owner is actually being changed, + // chown only succeeds if run under priviledged user (root) + // The test script has no such privilege, so need to find a better way to test this case + const { uid, gid } = await getUidAndGid(); + + const enc = new TextEncoder(); + const dirPath = Deno.makeTempDirSync(); + const filePath = dirPath + "/chown_test_file.txt"; + const fileData = enc.encode("Hello"); + Deno.writeFileSync(filePath, fileData); + + // the test script creates this file with the same uid and gid, + // here chown is a noop so it succeeds under non-priviledged user + Deno.chownSync(filePath, uid, gid); + + Deno.removeSync(dirPath, { recursive: true }); + } + ); + + unitTest( + { perms: { run: true, write: true } }, + async function chownSucceed(): Promise<void> { + // TODO: same as chownSyncSucceed + const { uid, gid } = await getUidAndGid(); + + const enc = new TextEncoder(); + const dirPath = await Deno.makeTempDir(); + const filePath = dirPath + "/chown_test_file.txt"; + const fileData = enc.encode("Hello"); + await Deno.writeFile(filePath, fileData); + + // the test script creates this file with the same uid and gid, + // here chown is a noop so it succeeds under non-priviledged user + await Deno.chown(filePath, uid, gid); + + Deno.removeSync(dirPath, { recursive: true }); + } + ); +} diff --git a/cli/tests/unit/console_test.ts b/cli/tests/unit/console_test.ts new file mode 100644 index 000000000..02d71e4ca --- /dev/null +++ b/cli/tests/unit/console_test.ts @@ -0,0 +1,1139 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// TODO(ry) The unit test functions in this module are too coarse. They should +// be broken up into smaller bits. + +// TODO(ry) These tests currentl strip all the ANSI colors out. We don't have a +// good way to control whether we produce color output or not since +// std/fmt/colors auto determines whether to put colors in or not. We need +// better infrastructure here so we can properly test the colors. + +import { assert, assertEquals, unitTest } from "./test_util.ts"; +import { stripColor } from "../../../std/fmt/colors.ts"; + +// Some of these APIs aren't exposed in the types and so we have to cast to any +// in order to "trick" TypeScript. +const { + inspect, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} = Deno as any; + +const customInspect = Deno.customInspect; +const { + Console, + stringifyArgs, + // @ts-ignore TypeScript (as of 3.7) does not support indexing namespaces by symbol +} = Deno[Deno.internal]; + +function stringify(...args: unknown[]): string { + return stripColor(stringifyArgs(args).replace(/\n$/, "")); +} + +// test cases from web-platform-tests +// via https://github.com/web-platform-tests/wpt/blob/master/console/console-is-a-namespace.any.js +unitTest(function consoleShouldBeANamespace(): void { + const prototype1 = Object.getPrototypeOf(console); + const prototype2 = Object.getPrototypeOf(prototype1); + + assertEquals(Object.getOwnPropertyNames(prototype1).length, 0); + assertEquals(prototype2, Object.prototype); +}); + +unitTest(function consoleHasRightInstance(): void { + assert(console instanceof Console); + assertEquals({} instanceof Console, false); +}); + +unitTest(function consoleTestAssertShouldNotThrowError(): void { + mockConsole((console) => { + console.assert(true); + let hasThrown = undefined; + try { + console.assert(false); + hasThrown = false; + } catch { + hasThrown = true; + } + assertEquals(hasThrown, false); + }); +}); + +unitTest(function consoleTestStringifyComplexObjects(): void { + assertEquals(stringify("foo"), "foo"); + assertEquals(stringify(["foo", "bar"]), `[ "foo", "bar" ]`); + assertEquals(stringify({ foo: "bar" }), `{ foo: "bar" }`); +}); + +unitTest(function consoleTestStringifyLongStrings(): void { + const veryLongString = "a".repeat(200); + // If we stringify an object containing the long string, it gets abbreviated. + let actual = stringify({ veryLongString }); + assert(actual.includes("...")); + assert(actual.length < 200); + // However if we stringify the string itself, we get it exactly. + actual = stringify(veryLongString); + assertEquals(actual, veryLongString); +}); + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +unitTest(function consoleTestStringifyCircular(): void { + class Base { + a = 1; + m1() {} + } + + class Extended extends Base { + b = 2; + m2() {} + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nestedObj: any = { + num: 1, + bool: true, + str: "a", + method() {}, + async asyncMethod() {}, + *generatorMethod() {}, + un: undefined, + nu: null, + arrowFunc: () => {}, + extendedClass: new Extended(), + nFunc: new Function(), + extendedCstr: Extended, + }; + + const circularObj = { + num: 2, + bool: false, + str: "b", + method() {}, + un: undefined, + nu: null, + nested: nestedObj, + emptyObj: {}, + arr: [1, "s", false, null, nestedObj], + baseClass: new Base(), + }; + + nestedObj.o = circularObj; + const nestedObjExpected = `{ + num: 1, + bool: true, + str: "a", + method: [Function: method], + asyncMethod: [AsyncFunction: asyncMethod], + generatorMethod: [GeneratorFunction: generatorMethod], + un: undefined, + nu: null, + arrowFunc: [Function: arrowFunc], + extendedClass: Extended { a: 1, b: 2 }, + nFunc: [Function], + extendedCstr: [Function: Extended], + o: { + num: 2, + bool: false, + str: "b", + method: [Function: method], + un: undefined, + nu: null, + nested: [Circular], + emptyObj: {}, + arr: [ 1, "s", false, null, [Circular] ], + baseClass: Base { a: 1 } + } +}`; + + assertEquals(stringify(1), "1"); + assertEquals(stringify(-0), "-0"); + assertEquals(stringify(1n), "1n"); + assertEquals(stringify("s"), "s"); + assertEquals(stringify(false), "false"); + assertEquals(stringify(new Number(1)), "[Number: 1]"); + assertEquals(stringify(new Boolean(true)), "[Boolean: true]"); + assertEquals(stringify(new String("deno")), `[String: "deno"]`); + assertEquals(stringify(/[0-9]*/), "/[0-9]*/"); + assertEquals( + stringify(new Date("2018-12-10T02:26:59.002Z")), + "2018-12-10T02:26:59.002Z" + ); + assertEquals(stringify(new Set([1, 2, 3])), "Set { 1, 2, 3 }"); + assertEquals( + stringify( + new Map([ + [1, "one"], + [2, "two"], + ]) + ), + `Map { 1 => "one", 2 => "two" }` + ); + assertEquals(stringify(new WeakSet()), "WeakSet { [items unknown] }"); + assertEquals(stringify(new WeakMap()), "WeakMap { [items unknown] }"); + assertEquals(stringify(Symbol(1)), "Symbol(1)"); + assertEquals(stringify(null), "null"); + assertEquals(stringify(undefined), "undefined"); + assertEquals(stringify(new Extended()), "Extended { a: 1, b: 2 }"); + assertEquals( + stringify(function f(): void {}), + "[Function: f]" + ); + assertEquals( + stringify(async function af(): Promise<void> {}), + "[AsyncFunction: af]" + ); + assertEquals( + stringify(function* gf() {}), + "[GeneratorFunction: gf]" + ); + assertEquals( + stringify(async function* agf() {}), + "[AsyncGeneratorFunction: agf]" + ); + assertEquals( + stringify(new Uint8Array([1, 2, 3])), + "Uint8Array(3) [ 1, 2, 3 ]" + ); + assertEquals(stringify(Uint8Array.prototype), "TypedArray {}"); + assertEquals( + stringify({ a: { b: { c: { d: new Set([1]) } } } }), + "{ a: { b: { c: { d: [Set] } } } }" + ); + assertEquals(stringify(nestedObj), nestedObjExpected); + assertEquals(stringify(JSON), 'JSON { Symbol(Symbol.toStringTag): "JSON" }'); + assertEquals( + stringify(console), + `{ + log: [Function], + debug: [Function], + info: [Function], + dir: [Function], + dirxml: [Function], + warn: [Function], + error: [Function], + assert: [Function], + count: [Function], + countReset: [Function], + table: [Function], + time: [Function], + timeLog: [Function], + timeEnd: [Function], + group: [Function], + groupCollapsed: [Function], + groupEnd: [Function], + clear: [Function], + trace: [Function], + indentLevel: 0, + Symbol(isConsoleInstance): true +}` + ); + assertEquals( + stringify({ str: 1, [Symbol.for("sym")]: 2, [Symbol.toStringTag]: "TAG" }), + 'TAG { str: 1, Symbol(sym): 2, Symbol(Symbol.toStringTag): "TAG" }' + ); + // test inspect is working the same + assertEquals(stripColor(inspect(nestedObj)), nestedObjExpected); +}); +/* eslint-enable @typescript-eslint/explicit-function-return-type */ + +unitTest(function consoleTestStringifyWithDepth(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } }; + assertEquals( + stripColor(stringifyArgs([nestedObj], { depth: 3 })), + "{ a: { b: { c: [Object] } } }" + ); + assertEquals( + stripColor(stringifyArgs([nestedObj], { depth: 4 })), + "{ a: { b: { c: { d: [Object] } } } }" + ); + assertEquals( + stripColor(stringifyArgs([nestedObj], { depth: 0 })), + "[Object]" + ); + assertEquals( + stripColor(stringifyArgs([nestedObj])), + "{ a: { b: { c: { d: [Object] } } } }" + ); + // test inspect is working the same way + assertEquals( + stripColor(inspect(nestedObj, { depth: 4 })), + "{ a: { b: { c: { d: [Object] } } } }" + ); +}); + +unitTest(function consoleTestStringifyLargeObject(): void { + const obj = { + a: 2, + o: { + a: "1", + b: "2", + c: "3", + d: "4", + e: "5", + f: "6", + g: 10, + asd: 2, + asda: 3, + x: { a: "asd", x: 3 }, + }, + }; + assertEquals( + stringify(obj), + `{ + a: 2, + o: { + a: "1", + b: "2", + c: "3", + d: "4", + e: "5", + f: "6", + g: 10, + asd: 2, + asda: 3, + x: { a: "asd", x: 3 } + } +}` + ); +}); + +unitTest(function consoleTestStringifyIterable() { + const shortArray = [1, 2, 3, 4, 5]; + assertEquals(stringify(shortArray), "[ 1, 2, 3, 4, 5 ]"); + + const longArray = new Array(200).fill(0); + assertEquals( + stringify(longArray), + `[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ... 100 more items +]` + ); + + const obj = { a: "a", longArray }; + assertEquals( + stringify(obj), + `{ + a: "a", + longArray: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ... 100 more items + ] +}` + ); + + const shortMap = new Map([ + ["a", 0], + ["b", 1], + ]); + assertEquals(stringify(shortMap), `Map { "a" => 0, "b" => 1 }`); + + const longMap = new Map(); + for (const key of Array(200).keys()) { + longMap.set(`${key}`, key); + } + assertEquals( + stringify(longMap), + `Map { + "0" => 0, + "1" => 1, + "2" => 2, + "3" => 3, + "4" => 4, + "5" => 5, + "6" => 6, + "7" => 7, + "8" => 8, + "9" => 9, + "10" => 10, + "11" => 11, + "12" => 12, + "13" => 13, + "14" => 14, + "15" => 15, + "16" => 16, + "17" => 17, + "18" => 18, + "19" => 19, + "20" => 20, + "21" => 21, + "22" => 22, + "23" => 23, + "24" => 24, + "25" => 25, + "26" => 26, + "27" => 27, + "28" => 28, + "29" => 29, + "30" => 30, + "31" => 31, + "32" => 32, + "33" => 33, + "34" => 34, + "35" => 35, + "36" => 36, + "37" => 37, + "38" => 38, + "39" => 39, + "40" => 40, + "41" => 41, + "42" => 42, + "43" => 43, + "44" => 44, + "45" => 45, + "46" => 46, + "47" => 47, + "48" => 48, + "49" => 49, + "50" => 50, + "51" => 51, + "52" => 52, + "53" => 53, + "54" => 54, + "55" => 55, + "56" => 56, + "57" => 57, + "58" => 58, + "59" => 59, + "60" => 60, + "61" => 61, + "62" => 62, + "63" => 63, + "64" => 64, + "65" => 65, + "66" => 66, + "67" => 67, + "68" => 68, + "69" => 69, + "70" => 70, + "71" => 71, + "72" => 72, + "73" => 73, + "74" => 74, + "75" => 75, + "76" => 76, + "77" => 77, + "78" => 78, + "79" => 79, + "80" => 80, + "81" => 81, + "82" => 82, + "83" => 83, + "84" => 84, + "85" => 85, + "86" => 86, + "87" => 87, + "88" => 88, + "89" => 89, + "90" => 90, + "91" => 91, + "92" => 92, + "93" => 93, + "94" => 94, + "95" => 95, + "96" => 96, + "97" => 97, + "98" => 98, + "99" => 99, + ... 100 more items +}` + ); + + const shortSet = new Set([1, 2, 3]); + assertEquals(stringify(shortSet), `Set { 1, 2, 3 }`); + const longSet = new Set(); + for (const key of Array(200).keys()) { + longSet.add(key); + } + assertEquals( + stringify(longSet), + `Set { + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + ... 100 more items +}` + ); + + const withEmptyEl = Array(10); + withEmptyEl.fill(0, 4, 6); + assertEquals( + stringify(withEmptyEl), + `[ <4 empty items>, 0, 0, <4 empty items> ]` + ); + + /* TODO(ry) Fix this test + const lWithEmptyEl = Array(200); + lWithEmptyEl.fill(0, 50, 80); + assertEquals( + stringify(lWithEmptyEl), + `[ + <50 empty items>, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, <120 empty items> +]` + ); +*/ +}); + +unitTest(async function consoleTestStringifyPromises(): Promise<void> { + const pendingPromise = new Promise((_res, _rej) => {}); + assertEquals(stringify(pendingPromise), "Promise { <pending> }"); + + const resolvedPromise = new Promise((res, _rej) => { + res("Resolved!"); + }); + assertEquals(stringify(resolvedPromise), `Promise { "Resolved!" }`); + + let rejectedPromise; + try { + rejectedPromise = new Promise((_, rej) => { + rej(Error("Whoops")); + }); + await rejectedPromise; + } catch (err) {} + const strLines = stringify(rejectedPromise).split("\n"); + assertEquals(strLines[0], "Promise {"); + assertEquals(strLines[1], " <rejected> Error: Whoops"); +}); + +unitTest(function consoleTestWithCustomInspector(): void { + class A { + [customInspect](): string { + return "b"; + } + } + + assertEquals(stringify(new A()), "b"); +}); + +unitTest(function consoleTestWithCustomInspectorError(): void { + class A { + [customInspect](): string { + throw new Error("BOOM"); + return "b"; + } + } + + assertEquals(stringify(new A()), "A {}"); + + class B { + constructor(public field: { a: string }) {} + [customInspect](): string { + return this.field.a; + } + } + + assertEquals(stringify(new B({ a: "a" })), "a"); + assertEquals( + stringify(B.prototype), + "{ Symbol(Deno.symbols.customInspect): [Function: [Deno.symbols.customInspect]] }" + ); +}); + +unitTest(function consoleTestWithIntegerFormatSpecifier(): void { + assertEquals(stringify("%i"), "%i"); + assertEquals(stringify("%i", 42.0), "42"); + assertEquals(stringify("%i", 42), "42"); + assertEquals(stringify("%i", "42"), "42"); + assertEquals(stringify("%i", "42.0"), "42"); + assertEquals(stringify("%i", 1.5), "1"); + assertEquals(stringify("%i", -0.5), "0"); + assertEquals(stringify("%i", ""), "NaN"); + assertEquals(stringify("%i", Symbol()), "NaN"); + assertEquals(stringify("%i %d", 42, 43), "42 43"); + assertEquals(stringify("%d %i", 42), "42 %i"); + assertEquals(stringify("%d", 12345678901234567890123), "1"); + assertEquals( + stringify("%i", 12345678901234567890123n), + "12345678901234567890123n" + ); +}); + +unitTest(function consoleTestWithFloatFormatSpecifier(): void { + assertEquals(stringify("%f"), "%f"); + assertEquals(stringify("%f", 42.0), "42"); + assertEquals(stringify("%f", 42), "42"); + assertEquals(stringify("%f", "42"), "42"); + assertEquals(stringify("%f", "42.0"), "42"); + assertEquals(stringify("%f", 1.5), "1.5"); + assertEquals(stringify("%f", -0.5), "-0.5"); + assertEquals(stringify("%f", Math.PI), "3.141592653589793"); + assertEquals(stringify("%f", ""), "NaN"); + assertEquals(stringify("%f", Symbol("foo")), "NaN"); + assertEquals(stringify("%f", 5n), "5"); + assertEquals(stringify("%f %f", 42, 43), "42 43"); + assertEquals(stringify("%f %f", 42), "42 %f"); +}); + +unitTest(function consoleTestWithStringFormatSpecifier(): void { + assertEquals(stringify("%s"), "%s"); + assertEquals(stringify("%s", undefined), "undefined"); + assertEquals(stringify("%s", "foo"), "foo"); + assertEquals(stringify("%s", 42), "42"); + assertEquals(stringify("%s", "42"), "42"); + assertEquals(stringify("%s %s", 42, 43), "42 43"); + assertEquals(stringify("%s %s", 42), "42 %s"); + assertEquals(stringify("%s", Symbol("foo")), "Symbol(foo)"); +}); + +unitTest(function consoleTestWithObjectFormatSpecifier(): void { + assertEquals(stringify("%o"), "%o"); + assertEquals(stringify("%o", 42), "42"); + assertEquals(stringify("%o", "foo"), "foo"); + assertEquals(stringify("o: %o, a: %O", {}, []), "o: {}, a: []"); + assertEquals(stringify("%o", { a: 42 }), "{ a: 42 }"); + assertEquals( + stringify("%o", { a: { b: { c: { d: new Set([1]) } } } }), + "{ a: { b: { c: { d: [Set] } } } }" + ); +}); + +unitTest(function consoleTestWithVariousOrInvalidFormatSpecifier(): void { + assertEquals(stringify("%s:%s"), "%s:%s"); + assertEquals(stringify("%i:%i"), "%i:%i"); + assertEquals(stringify("%d:%d"), "%d:%d"); + assertEquals(stringify("%%s%s", "foo"), "%sfoo"); + assertEquals(stringify("%s:%s", undefined), "undefined:%s"); + assertEquals(stringify("%s:%s", "foo", "bar"), "foo:bar"); + assertEquals(stringify("%s:%s", "foo", "bar", "baz"), "foo:bar baz"); + assertEquals(stringify("%%%s%%", "hi"), "%hi%"); + assertEquals(stringify("%d:%d", 12), "12:%d"); + assertEquals(stringify("%i:%i", 12), "12:%i"); + assertEquals(stringify("%f:%f", 12), "12:%f"); + assertEquals(stringify("o: %o, a: %o", {}), "o: {}, a: %o"); + assertEquals(stringify("abc%", 1), "abc% 1"); +}); + +unitTest(function consoleTestCallToStringOnLabel(): void { + const methods = ["count", "countReset", "time", "timeLog", "timeEnd"]; + mockConsole((console) => { + for (const method of methods) { + let hasCalled = false; + // @ts-ignore + console[method]({ + toString(): void { + hasCalled = true; + }, + }); + assertEquals(hasCalled, true); + } + }); +}); + +unitTest(function consoleTestError(): void { + class MyError extends Error { + constructor(errStr: string) { + super(errStr); + this.name = "MyError"; + } + } + try { + throw new MyError("This is an error"); + } catch (e) { + assert( + stringify(e) + .split("\n")[0] // error has been caught + .includes("MyError: This is an error") + ); + } +}); + +unitTest(function consoleTestClear(): void { + mockConsole((console, out) => { + console.clear(); + assertEquals(out.toString(), "\x1b[1;1H" + "\x1b[0J"); + }); +}); + +// Test bound this issue +unitTest(function consoleDetachedLog(): void { + mockConsole((console) => { + const log = console.log; + const dir = console.dir; + const dirxml = console.dirxml; + const debug = console.debug; + const info = console.info; + const warn = console.warn; + const error = console.error; + const consoleAssert = console.assert; + const consoleCount = console.count; + const consoleCountReset = console.countReset; + const consoleTable = console.table; + const consoleTime = console.time; + const consoleTimeLog = console.timeLog; + const consoleTimeEnd = console.timeEnd; + const consoleGroup = console.group; + const consoleGroupEnd = console.groupEnd; + const consoleClear = console.clear; + log("Hello world"); + dir("Hello world"); + dirxml("Hello world"); + debug("Hello world"); + info("Hello world"); + warn("Hello world"); + error("Hello world"); + consoleAssert(true); + consoleCount("Hello world"); + consoleCountReset("Hello world"); + consoleTable({ test: "Hello world" }); + consoleTime("Hello world"); + consoleTimeLog("Hello world"); + consoleTimeEnd("Hello world"); + consoleGroup("Hello world"); + consoleGroupEnd(); + consoleClear(); + }); +}); + +class StringBuffer { + chunks: string[] = []; + add(x: string): void { + this.chunks.push(x); + } + toString(): string { + return this.chunks.join(""); + } +} + +type ConsoleExamineFunc = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + csl: any, + out: StringBuffer, + err?: StringBuffer, + both?: StringBuffer +) => void; + +function mockConsole(f: ConsoleExamineFunc): void { + const out = new StringBuffer(); + const err = new StringBuffer(); + const both = new StringBuffer(); + const csl = new Console( + (x: string, isErr: boolean, printsNewLine: boolean): void => { + const content = x + (printsNewLine ? "\n" : ""); + const buf = isErr ? err : out; + buf.add(content); + both.add(content); + } + ); + f(csl, out, err, both); +} + +// console.group test +unitTest(function consoleGroup(): void { + mockConsole((console, out): void => { + console.group("1"); + console.log("2"); + console.group("3"); + console.log("4"); + console.groupEnd(); + console.groupEnd(); + console.log("5"); + console.log("6"); + + assertEquals( + out.toString(), + `1 + 2 + 3 + 4 +5 +6 +` + ); + }); +}); + +// console.group with console.warn test +unitTest(function consoleGroupWarn(): void { + mockConsole((console, _out, _err, both): void => { + assert(both); + console.warn("1"); + console.group(); + console.warn("2"); + console.group(); + console.warn("3"); + console.groupEnd(); + console.warn("4"); + console.groupEnd(); + console.warn("5"); + + console.warn("6"); + console.warn("7"); + assertEquals( + both.toString(), + `1 + 2 + 3 + 4 +5 +6 +7 +` + ); + }); +}); + +// console.table test +unitTest(function consoleTable(): void { + mockConsole((console, out): void => { + console.table({ a: "test", b: 1 }); + assertEquals( + stripColor(out.toString()), + `┌───────┬────────┐ +│ (idx) │ Values │ +├───────┼────────┤ +│ a │ "test" │ +│ b │ 1 │ +└───────┴────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table({ a: { b: 10 }, b: { b: 20, c: 30 } }, ["c"]); + assertEquals( + stripColor(out.toString()), + `┌───────┬────┐ +│ (idx) │ c │ +├───────┼────┤ +│ a │ │ +│ b │ 30 │ +└───────┴────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table([1, 2, [3, [4]], [5, 6], [[7], [8]]]); + assertEquals( + stripColor(out.toString()), + `┌───────┬───────┬───────┬────────┐ +│ (idx) │ 0 │ 1 │ Values │ +├───────┼───────┼───────┼────────┤ +│ 0 │ │ │ 1 │ +│ 1 │ │ │ 2 │ +│ 2 │ 3 │ [ 4 ] │ │ +│ 3 │ 5 │ 6 │ │ +│ 4 │ [ 7 ] │ [ 8 ] │ │ +└───────┴───────┴───────┴────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table(new Set([1, 2, 3, "test"])); + assertEquals( + stripColor(out.toString()), + `┌────────────┬────────┐ +│ (iter idx) │ Values │ +├────────────┼────────┤ +│ 0 │ 1 │ +│ 1 │ 2 │ +│ 2 │ 3 │ +│ 3 │ "test" │ +└────────────┴────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table( + new Map([ + [1, "one"], + [2, "two"], + ]) + ); + assertEquals( + stripColor(out.toString()), + `┌────────────┬─────┬────────┐ +│ (iter idx) │ Key │ Values │ +├────────────┼─────┼────────┤ +│ 0 │ 1 │ "one" │ +│ 1 │ 2 │ "two" │ +└────────────┴─────┴────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table({ + a: true, + b: { c: { d: 10 }, e: [1, 2, [5, 6]] }, + f: "test", + g: new Set([1, 2, 3, "test"]), + h: new Map([[1, "one"]]), + }); + assertEquals( + stripColor(out.toString()), + `┌───────┬───────────┬───────────────────┬────────┐ +│ (idx) │ c │ e │ Values │ +├───────┼───────────┼───────────────────┼────────┤ +│ a │ │ │ true │ +│ b │ { d: 10 } │ [ 1, 2, [Array] ] │ │ +│ f │ │ │ "test" │ +│ g │ │ │ │ +│ h │ │ │ │ +└───────┴───────────┴───────────────────┴────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table([ + 1, + "test", + false, + { a: 10 }, + ["test", { b: 20, c: "test" }], + ]); + assertEquals( + stripColor(out.toString()), + `┌───────┬────────┬──────────────────────┬────┬────────┐ +│ (idx) │ 0 │ 1 │ a │ Values │ +├───────┼────────┼──────────────────────┼────┼────────┤ +│ 0 │ │ │ │ 1 │ +│ 1 │ │ │ │ "test" │ +│ 2 │ │ │ │ false │ +│ 3 │ │ │ 10 │ │ +│ 4 │ "test" │ { b: 20, c: "test" } │ │ │ +└───────┴────────┴──────────────────────┴────┴────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table([]); + assertEquals( + stripColor(out.toString()), + `┌───────┐ +│ (idx) │ +├───────┤ +└───────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table({}); + assertEquals( + stripColor(out.toString()), + `┌───────┐ +│ (idx) │ +├───────┤ +└───────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table(new Set()); + assertEquals( + stripColor(out.toString()), + `┌────────────┐ +│ (iter idx) │ +├────────────┤ +└────────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table(new Map()); + assertEquals( + stripColor(out.toString()), + `┌────────────┐ +│ (iter idx) │ +├────────────┤ +└────────────┘ +` + ); + }); + mockConsole((console, out): void => { + console.table("test"); + assertEquals(out.toString(), "test\n"); + }); + mockConsole((console, out): void => { + console.table(["Hello", "你好", "Amapá"]); + assertEquals( + stripColor(out.toString()), + `┌───────┬─────────┐ +│ (idx) │ Values │ +├───────┼─────────┤ +│ 0 │ "Hello" │ +│ 1 │ "你好" │ +│ 2 │ "Amapá" │ +└───────┴─────────┘ +` + ); + }); +}); + +// console.log(Error) test +unitTest(function consoleLogShouldNotThrowError(): void { + mockConsole((console) => { + let result = 0; + try { + console.log(new Error("foo")); + result = 1; + } catch (e) { + result = 2; + } + assertEquals(result, 1); + }); + + // output errors to the console should not include "Uncaught" + mockConsole((console, out): void => { + console.log(new Error("foo")); + assertEquals(out.toString().includes("Uncaught"), false); + }); +}); + +// console.log(Invalid Date) test +unitTest(function consoleLogShoultNotThrowErrorWhenInvalidDateIsPassed(): void { + mockConsole((console, out) => { + const invalidDate = new Date("test"); + console.log(invalidDate); + assertEquals(stripColor(out.toString()), "Invalid Date\n"); + }); +}); + +// console.dir test +unitTest(function consoleDir(): void { + mockConsole((console, out): void => { + console.dir("DIR"); + assertEquals(out.toString(), "DIR\n"); + }); + mockConsole((console, out): void => { + console.dir("DIR", { indentLevel: 2 }); + assertEquals(out.toString(), " DIR\n"); + }); +}); + +// console.dir test +unitTest(function consoleDirXml(): void { + mockConsole((console, out): void => { + console.dirxml("DIRXML"); + assertEquals(out.toString(), "DIRXML\n"); + }); + mockConsole((console, out): void => { + console.dirxml("DIRXML", { indentLevel: 2 }); + assertEquals(out.toString(), " DIRXML\n"); + }); +}); + +// console.trace test +unitTest(function consoleTrace(): void { + mockConsole((console, _out, err): void => { + console.trace("%s", "custom message"); + assert(err); + assert(err.toString().includes("Trace: custom message")); + }); +}); diff --git a/cli/tests/unit/copy_file_test.ts b/cli/tests/unit/copy_file_test.ts new file mode 100644 index 000000000..986bab53b --- /dev/null +++ b/cli/tests/unit/copy_file_test.ts @@ -0,0 +1,176 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +function readFileString(filename: string): string { + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + return dec.decode(dataRead); +} + +function writeFileString(filename: string, s: string): void { + const enc = new TextEncoder(); + const data = enc.encode(s); + Deno.writeFileSync(filename, data, { mode: 0o666 }); +} + +function assertSameContent(filename1: string, filename2: string): void { + const data1 = Deno.readFileSync(filename1); + const data2 = Deno.readFileSync(filename2); + assertEquals(data1, data2); +} + +unitTest( + { perms: { read: true, write: true } }, + function copyFileSyncSuccess(): void { + const tempDir = Deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + Deno.copyFileSync(fromFilename, toFilename); + // No change to original file + assertEquals(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); + } +); + +unitTest( + { perms: { write: true, read: true } }, + function copyFileSyncFailure(): void { + const tempDir = Deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + // We skip initial writing here, from.txt does not exist + let err; + try { + Deno.copyFileSync(fromFilename, toFilename); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: false } }, + function copyFileSyncPerm1(): void { + let caughtError = false; + try { + Deno.copyFileSync("/from.txt", "/to.txt"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); + +unitTest( + { perms: { write: false, read: true } }, + function copyFileSyncPerm2(): void { + let caughtError = false; + try { + Deno.copyFileSync("/from.txt", "/to.txt"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function copyFileSyncOverwrite(): void { + const tempDir = Deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + // Make Dest exist and have different content + writeFileString(toFilename, "Goodbye!"); + Deno.copyFileSync(fromFilename, toFilename); + // No change to original file + assertEquals(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function copyFileSuccess(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + await Deno.copyFile(fromFilename, toFilename); + // No change to original file + assertEquals(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function copyFileFailure(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + // We skip initial writing here, from.txt does not exist + let err; + try { + await Deno.copyFile(fromFilename, toFilename); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function copyFileOverwrite(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const fromFilename = tempDir + "/from.txt"; + const toFilename = tempDir + "/to.txt"; + writeFileString(fromFilename, "Hello world!"); + // Make Dest exist and have different content + writeFileString(toFilename, "Goodbye!"); + await Deno.copyFile(fromFilename, toFilename); + // No change to original file + assertEquals(readFileString(fromFilename), "Hello world!"); + // Original == Dest + assertSameContent(fromFilename, toFilename); + } +); + +unitTest( + { perms: { read: false, write: true } }, + async function copyFilePerm1(): Promise<void> { + let caughtError = false; + try { + await Deno.copyFile("/from.txt", "/to.txt"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: false } }, + async function copyFilePerm2(): Promise<void> { + let caughtError = false; + try { + await Deno.copyFile("/from.txt", "/to.txt"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); diff --git a/cli/tests/unit/custom_event_test.ts b/cli/tests/unit/custom_event_test.ts new file mode 100644 index 000000000..a8b2fcf88 --- /dev/null +++ b/cli/tests/unit/custom_event_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals } from "./test_util.ts"; + +unitTest(function customEventInitializedWithDetail(): void { + const type = "touchstart"; + const detail = { message: "hello" }; + const customEventInit = { + bubbles: true, + cancelable: true, + detail, + } as CustomEventInit; + const event = new CustomEvent(type, customEventInit); + + assertEquals(event.bubbles, true); + assertEquals(event.cancelable, true); + assertEquals(event.currentTarget, null); + assertEquals(event.detail, detail); + assertEquals(event.isTrusted, false); + assertEquals(event.target, null); + assertEquals(event.type, type); +}); + +unitTest(function toStringShouldBeWebCompatibility(): void { + const type = "touchstart"; + const event = new CustomEvent(type, {}); + assertEquals(event.toString(), "[object CustomEvent]"); +}); diff --git a/cli/tests/unit/dir_test.ts b/cli/tests/unit/dir_test.ts new file mode 100644 index 000000000..dc05d9564 --- /dev/null +++ b/cli/tests/unit/dir_test.ts @@ -0,0 +1,60 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest({ perms: { read: true } }, function dirCwdNotNull(): void { + assert(Deno.cwd() != null); +}); + +unitTest( + { perms: { read: true, write: true } }, + function dirCwdChdirSuccess(): void { + const initialdir = Deno.cwd(); + const path = Deno.makeTempDirSync(); + Deno.chdir(path); + const current = Deno.cwd(); + if (Deno.build.os === "darwin") { + assertEquals(current, "/private" + path); + } else { + assertEquals(current, path); + } + Deno.chdir(initialdir); + } +); + +unitTest({ perms: { read: true, write: true } }, function dirCwdError(): void { + // excluding windows since it throws resource busy, while removeSync + if (["linux", "darwin"].includes(Deno.build.os)) { + const initialdir = Deno.cwd(); + const path = Deno.makeTempDirSync(); + Deno.chdir(path); + Deno.removeSync(path); + try { + Deno.cwd(); + throw Error("current directory removed, should throw error"); + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + assert(err.name === "NotFound"); + } else { + throw Error("raised different exception"); + } + } + Deno.chdir(initialdir); + } +}); + +unitTest( + { perms: { read: true, write: true } }, + function dirChdirError(): void { + const path = Deno.makeTempDirSync() + "test"; + try { + Deno.chdir(path); + throw Error("directory not available, should throw error"); + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + assert(err.name === "NotFound"); + } else { + throw Error("raised different exception"); + } + } + } +); diff --git a/cli/tests/unit/dispatch_json_test.ts b/cli/tests/unit/dispatch_json_test.ts new file mode 100644 index 000000000..4e95b86a2 --- /dev/null +++ b/cli/tests/unit/dispatch_json_test.ts @@ -0,0 +1,32 @@ +import { assert, unitTest, assertMatch, unreachable } from "./test_util.ts"; + +const openErrorStackPattern = new RegExp( + `^.* + at unwrapResponse \\(.*dispatch_json\\.ts:.*\\) + at Object.sendAsync \\(.*dispatch_json\\.ts:.*\\) + at async Object\\.open \\(.*files\\.ts:.*\\).*$`, + "ms" +); + +unitTest( + { perms: { read: true } }, + async function sendAsyncStackTrace(): Promise<void> { + await Deno.open("nonexistent.txt") + .then(unreachable) + .catch((error): void => { + assertMatch(error.stack, openErrorStackPattern); + }); + } +); + +unitTest(function malformedJsonControlBuffer(): void { + // @ts-ignore + const opId = Deno.core.ops()["op_open"]; + // @ts-ignore + const res = Deno.core.send(opId, new Uint8Array([1, 2, 3, 4, 5])); + const resText = new TextDecoder().decode(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resJson = JSON.parse(resText) as any; + assert(!resJson.ok); + assert(resJson.err); +}); diff --git a/cli/tests/unit/dispatch_minimal_test.ts b/cli/tests/unit/dispatch_minimal_test.ts new file mode 100644 index 000000000..afc17f4fb --- /dev/null +++ b/cli/tests/unit/dispatch_minimal_test.ts @@ -0,0 +1,43 @@ +import { + assert, + assertEquals, + assertMatch, + unitTest, + unreachable, +} from "./test_util.ts"; + +const readErrorStackPattern = new RegExp( + `^.* + at unwrapResponse \\(.*dispatch_minimal\\.ts:.*\\) + at Object.sendAsyncMinimal \\(.*dispatch_minimal\\.ts:.*\\) + at async Object\\.read \\(.*io\\.ts:.*\\).*$`, + "ms" +); + +unitTest(async function sendAsyncStackTrace(): Promise<void> { + const buf = new Uint8Array(10); + const rid = 10; + try { + await Deno.read(rid, buf); + unreachable(); + } catch (error) { + assertMatch(error.stack, readErrorStackPattern); + } +}); + +unitTest(function malformedMinimalControlBuffer(): void { + // @ts-ignore + const readOpId = Deno.core.ops()["op_read"]; + // @ts-ignore + const res = Deno.core.send(readOpId, new Uint8Array([1, 2, 3, 4, 5])); + const header = res.slice(0, 12); + const buf32 = new Int32Array( + header.buffer, + header.byteOffset, + header.byteLength / 4 + ); + const arg = buf32[1]; + const message = new TextDecoder().decode(res.slice(12)).trim(); + assert(arg < 0); + assertEquals(message, "Unparsable control buffer"); +}); diff --git a/cli/tests/unit/dom_exception_test.ts b/cli/tests/unit/dom_exception_test.ts new file mode 100644 index 000000000..2eb7633e1 --- /dev/null +++ b/cli/tests/unit/dom_exception_test.ts @@ -0,0 +1,9 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals, assert } from "./test_util.ts"; + +unitTest(function testDomError() { + const de = new DOMException("foo", "bar"); + assert(de); + assertEquals(de.message, "foo"); + assertEquals(de.name, "bar"); +}); diff --git a/cli/tests/unit/dom_iterable_test.ts b/cli/tests/unit/dom_iterable_test.ts new file mode 100644 index 000000000..b9435b3bc --- /dev/null +++ b/cli/tests/unit/dom_iterable_test.ts @@ -0,0 +1,87 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function setup() { + const dataSymbol = Symbol("data symbol"); + class Base { + [dataSymbol] = new Map<string, number>(); + + constructor( + data: Array<[string, number]> | IterableIterator<[string, number]> + ) { + for (const [key, value] of data) { + this[dataSymbol].set(key, value); + } + } + } + + return { + Base, + // This is using an internal API we don't want published as types, so having + // to cast to any to "trick" TypeScript + // @ts-ignore TypeScript (as of 3.7) does not support indexing namespaces by symbol + DomIterable: Deno[Deno.internal].DomIterableMixin(Base, dataSymbol), + }; +} + +unitTest(function testDomIterable(): void { + const { DomIterable, Base } = setup(); + + const fixture: Array<[string, number]> = [ + ["foo", 1], + ["bar", 2], + ]; + + const domIterable = new DomIterable(fixture); + + assertEquals(Array.from(domIterable.entries()), fixture); + assertEquals(Array.from(domIterable.values()), [1, 2]); + assertEquals(Array.from(domIterable.keys()), ["foo", "bar"]); + + let result: Array<[string, number]> = []; + for (const [key, value] of domIterable) { + assert(key != null); + assert(value != null); + result.push([key, value]); + } + assertEquals(fixture, result); + + result = []; + const scope = {}; + function callback( + this: typeof scope, + value: number, + key: string, + parent: typeof domIterable + ): void { + assertEquals(parent, domIterable); + assert(key != null); + assert(value != null); + assert(this === scope); + result.push([key, value]); + } + domIterable.forEach(callback, scope); + assertEquals(fixture, result); + + assertEquals(DomIterable.name, Base.name); +}); + +unitTest(function testDomIterableScope(): void { + const { DomIterable } = setup(); + + const domIterable = new DomIterable([["foo", 1]]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function checkScope(thisArg: any, expected: any): void { + function callback(this: typeof thisArg): void { + assertEquals(this, expected); + } + domIterable.forEach(callback, thisArg); + } + + checkScope(0, Object(0)); + checkScope("", Object("")); + checkScope(null, window); + checkScope(undefined, window); +}); diff --git a/cli/tests/unit/error_stack_test.ts b/cli/tests/unit/error_stack_test.ts new file mode 100644 index 000000000..e5cedfcf5 --- /dev/null +++ b/cli/tests/unit/error_stack_test.ts @@ -0,0 +1,108 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +// @ts-ignore TypeScript (as of 3.7) does not support indexing namespaces by symbol +const { setPrepareStackTrace } = Deno[Deno.internal]; + +interface CallSite { + getThis(): unknown; + getTypeName(): string | null; + getFunction(): Function | null; + getFunctionName(): string | null; + getMethodName(): string | null; + getFileName(): string | null; + getLineNumber(): number | null; + getColumnNumber(): number | null; + getEvalOrigin(): string | null; + isToplevel(): boolean | null; + isEval(): boolean; + isNative(): boolean; + isConstructor(): boolean; + isAsync(): boolean; + isPromiseAll(): boolean; + getPromiseIndex(): number | null; +} + +function getMockCallSite( + fileName: string, + lineNumber: number | null, + columnNumber: number | null +): CallSite { + return { + getThis(): unknown { + return undefined; + }, + getTypeName(): string { + return ""; + }, + getFunction(): Function { + return (): void => {}; + }, + getFunctionName(): string { + return ""; + }, + getMethodName(): string { + return ""; + }, + getFileName(): string { + return fileName; + }, + getLineNumber(): number | null { + return lineNumber; + }, + getColumnNumber(): number | null { + return columnNumber; + }, + getEvalOrigin(): null { + return null; + }, + isToplevel(): false { + return false; + }, + isEval(): false { + return false; + }, + isNative(): false { + return false; + }, + isConstructor(): false { + return false; + }, + isAsync(): false { + return false; + }, + isPromiseAll(): false { + return false; + }, + getPromiseIndex(): null { + return null; + }, + }; +} + +unitTest(function prepareStackTrace(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const MockError = {} as any; + setPrepareStackTrace(MockError); + assert(typeof MockError.prepareStackTrace === "function"); + const prepareStackTrace: ( + error: Error, + structuredStackTrace: CallSite[] + ) => string = MockError.prepareStackTrace; + const result = prepareStackTrace(new Error("foo"), [ + getMockCallSite("CLI_SNAPSHOT.js", 23, 0), + ]); + assert(result.startsWith("Error: foo\n")); + assert(result.includes(".ts:"), "should remap to something in 'js/'"); +}); + +unitTest(function applySourceMap(): void { + const result = Deno.applySourceMap({ + fileName: "CLI_SNAPSHOT.js", + lineNumber: 23, + columnNumber: 0, + }); + assert(result.fileName.endsWith(".ts")); + assert(result.lineNumber != null); + assert(result.columnNumber != null); +}); diff --git a/cli/tests/unit/event_target_test.ts b/cli/tests/unit/event_target_test.ts new file mode 100644 index 000000000..0c4eb4d0d --- /dev/null +++ b/cli/tests/unit/event_target_test.ts @@ -0,0 +1,231 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals } from "./test_util.ts"; + +unitTest(function addEventListenerTest(): void { + const document = new EventTarget(); + + // @ts-ignore tests ignoring the type system for resilience + assertEquals(document.addEventListener("x", null, false), undefined); + // @ts-ignore + assertEquals(document.addEventListener("x", null, true), undefined); + // @ts-ignore + assertEquals(document.addEventListener("x", null), undefined); +}); + +unitTest(function constructedEventTargetCanBeUsedAsExpected(): void { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (e: Event): void => { + assertEquals(e, event); + ++callCount; + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assertEquals(callCount, 1); + + target.dispatchEvent(event); + assertEquals(callCount, 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assertEquals(callCount, 2); +}); + +unitTest(function anEventTargetCanBeSubclassed(): void { + class NicerEventTarget extends EventTarget { + on( + type: string, + callback: ((e: Event) => void) | null, + options?: AddEventListenerOptions + ): void { + this.addEventListener(type, callback, options); + } + + off( + type: string, + callback: ((e: Event) => void) | null, + options?: EventListenerOptions + ): void { + this.removeEventListener(type, callback, options); + } + } + + const target = new NicerEventTarget(); + new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (): void => { + ++callCount; + }; + + target.on("foo", listener); + assertEquals(callCount, 0); + + target.off("foo", listener); + assertEquals(callCount, 0); +}); + +unitTest(function removingNullEventListenerShouldSucceed(): void { + const document = new EventTarget(); + // @ts-ignore + assertEquals(document.removeEventListener("x", null, false), undefined); + // @ts-ignore + assertEquals(document.removeEventListener("x", null, true), undefined); + // @ts-ignore + assertEquals(document.removeEventListener("x", null), undefined); +}); + +unitTest(function constructedEventTargetUseObjectPrototype(): void { + const target = new EventTarget(); + const event = new Event("toString", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (e: Event): void => { + assertEquals(e, event); + ++callCount; + }; + + target.addEventListener("toString", listener); + + target.dispatchEvent(event); + assertEquals(callCount, 1); + + target.dispatchEvent(event); + assertEquals(callCount, 2); + + target.removeEventListener("toString", listener); + target.dispatchEvent(event); + assertEquals(callCount, 2); +}); + +unitTest(function toStringShouldBeWebCompatible(): void { + const target = new EventTarget(); + assertEquals(target.toString(), "[object EventTarget]"); +}); + +unitTest(function dispatchEventShouldNotThrowError(): void { + let hasThrown = false; + + try { + const target = new EventTarget(); + const event = new Event("hasOwnProperty", { + bubbles: true, + cancelable: false, + }); + const listener = (): void => {}; + target.addEventListener("hasOwnProperty", listener); + target.dispatchEvent(event); + } catch { + hasThrown = true; + } + + assertEquals(hasThrown, false); +}); + +unitTest(function eventTargetThisShouldDefaultToWindow(): void { + const { + addEventListener, + dispatchEvent, + removeEventListener, + } = EventTarget.prototype; + let n = 1; + const event = new Event("hello"); + const listener = (): void => { + n = 2; + }; + + addEventListener("hello", listener); + window.dispatchEvent(event); + assertEquals(n, 2); + n = 1; + removeEventListener("hello", listener); + window.dispatchEvent(event); + assertEquals(n, 1); + + window.addEventListener("hello", listener); + dispatchEvent(event); + assertEquals(n, 2); + n = 1; + window.removeEventListener("hello", listener); + dispatchEvent(event); + assertEquals(n, 1); +}); + +unitTest(function eventTargetShouldAcceptEventListenerObject(): void { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = { + handleEvent(e: Event): void { + assertEquals(e, event); + ++callCount; + }, + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assertEquals(callCount, 1); + + target.dispatchEvent(event); + assertEquals(callCount, 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assertEquals(callCount, 2); +}); + +unitTest(function eventTargetShouldAcceptAsyncFunction(): void { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (e: Event): void => { + assertEquals(e, event); + ++callCount; + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assertEquals(callCount, 1); + + target.dispatchEvent(event); + assertEquals(callCount, 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assertEquals(callCount, 2); +}); + +unitTest( + function eventTargetShouldAcceptAsyncFunctionForEventListenerObject(): void { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = { + handleEvent(e: Event): void { + assertEquals(e, event); + ++callCount; + }, + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assertEquals(callCount, 1); + + target.dispatchEvent(event); + assertEquals(callCount, 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assertEquals(callCount, 2); + } +); diff --git a/cli/tests/unit/event_test.ts b/cli/tests/unit/event_test.ts new file mode 100644 index 000000000..ce3076e58 --- /dev/null +++ b/cli/tests/unit/event_test.ts @@ -0,0 +1,94 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals, assert } from "./test_util.ts"; + +unitTest(function eventInitializedWithType(): void { + const type = "click"; + const event = new Event(type); + + assertEquals(event.isTrusted, false); + assertEquals(event.target, null); + assertEquals(event.currentTarget, null); + assertEquals(event.type, "click"); + assertEquals(event.bubbles, false); + assertEquals(event.cancelable, false); +}); + +unitTest(function eventInitializedWithTypeAndDict(): void { + const init = "submit"; + const eventInit = { bubbles: true, cancelable: true } as EventInit; + const event = new Event(init, eventInit); + + assertEquals(event.isTrusted, false); + assertEquals(event.target, null); + assertEquals(event.currentTarget, null); + assertEquals(event.type, "submit"); + assertEquals(event.bubbles, true); + assertEquals(event.cancelable, true); +}); + +unitTest(function eventComposedPathSuccess(): void { + const type = "click"; + const event = new Event(type); + const composedPath = event.composedPath(); + + assertEquals(composedPath, []); +}); + +unitTest(function eventStopPropagationSuccess(): void { + const type = "click"; + const event = new Event(type); + + assertEquals(event.cancelBubble, false); + event.stopPropagation(); + assertEquals(event.cancelBubble, true); +}); + +unitTest(function eventStopImmediatePropagationSuccess(): void { + const type = "click"; + const event = new Event(type); + + assertEquals(event.cancelBubble, false); + event.stopImmediatePropagation(); + assertEquals(event.cancelBubble, true); +}); + +unitTest(function eventPreventDefaultSuccess(): void { + const type = "click"; + const event = new Event(type); + + assertEquals(event.defaultPrevented, false); + event.preventDefault(); + assertEquals(event.defaultPrevented, false); + + const eventInit = { bubbles: true, cancelable: true } as EventInit; + const cancelableEvent = new Event(type, eventInit); + assertEquals(cancelableEvent.defaultPrevented, false); + cancelableEvent.preventDefault(); + assertEquals(cancelableEvent.defaultPrevented, true); +}); + +unitTest(function eventInitializedWithNonStringType(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const type: any = undefined; + const event = new Event(type); + + assertEquals(event.isTrusted, false); + assertEquals(event.target, null); + assertEquals(event.currentTarget, null); + assertEquals(event.type, "undefined"); + assertEquals(event.bubbles, false); + assertEquals(event.cancelable, false); +}); + +// ref https://github.com/web-platform-tests/wpt/blob/master/dom/events/Event-isTrusted.any.js +unitTest(function eventIsTrusted(): void { + const desc1 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted"); + assert(desc1); + assertEquals(typeof desc1.get, "function"); + + const desc2 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted"); + assert(desc2); + assertEquals(typeof desc2!.get, "function"); + + assertEquals(desc1!.get, desc2!.get); +}); diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts new file mode 100644 index 000000000..37fca2112 --- /dev/null +++ b/cli/tests/unit/fetch_test.ts @@ -0,0 +1,534 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertStrContains, + assertThrows, + fail, +} from "./test_util.ts"; + +unitTest({ perms: { net: true } }, async function fetchProtocolError(): Promise< + void +> { + let err; + try { + await fetch("file:///"); + } catch (err_) { + err = err_; + } + assert(err instanceof TypeError); + assertStrContains(err.message, "not supported"); +}); + +unitTest( + { perms: { net: true } }, + async function fetchConnectionError(): Promise<void> { + let err; + try { + await fetch("http://localhost:4000"); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.Http); + assertStrContains(err.message, "error trying to connect"); + } +); + +unitTest({ perms: { net: true } }, async function fetchJsonSuccess(): Promise< + void +> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + const json = await response.json(); + assertEquals(json.name, "deno"); +}); + +unitTest(async function fetchPerm(): Promise<void> { + let err; + try { + await fetch("http://localhost:4545/cli/tests/fixture.json"); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest({ perms: { net: true } }, async function fetchUrl(): Promise<void> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + assertEquals(response.url, "http://localhost:4545/cli/tests/fixture.json"); + const _json = await response.json(); +}); + +unitTest({ perms: { net: true } }, async function fetchURL(): Promise<void> { + const response = await fetch( + new URL("http://localhost:4545/cli/tests/fixture.json") + ); + assertEquals(response.url, "http://localhost:4545/cli/tests/fixture.json"); + const _json = await response.json(); +}); + +unitTest({ perms: { net: true } }, async function fetchHeaders(): Promise< + void +> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + const headers = response.headers; + assertEquals(headers.get("Content-Type"), "application/json"); + assert(headers.get("Server")!.startsWith("SimpleHTTP")); + const _json = await response.json(); +}); + +unitTest({ perms: { net: true } }, async function fetchBlob(): Promise<void> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + const headers = response.headers; + const blob = await response.blob(); + assertEquals(blob.type, headers.get("Content-Type")); + assertEquals(blob.size, Number(headers.get("Content-Length"))); +}); + +unitTest({ perms: { net: true } }, async function fetchBodyUsed(): Promise< + void +> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + assertEquals(response.bodyUsed, false); + assertThrows((): void => { + // Assigning to read-only property throws in the strict mode. + // @ts-ignore + response.bodyUsed = true; + }); + await response.blob(); + assertEquals(response.bodyUsed, true); +}); + +// TODO(ry) response.body shouldn't be iterable. Instead we should use +// response.body.getReader(). +/* +unitTest({ perms: { net: true } }, async function fetchAsyncIterator(): Promise< + void +> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + const headers = response.headers; + let total = 0; + for await (const chunk of response.body) { + total += chunk.length; + } + + assertEquals(total, Number(headers.get("Content-Length"))); + const _json = await response.json(); +}); +*/ + +unitTest({ perms: { net: true } }, async function responseClone(): Promise< + void +> { + const response = await fetch("http://localhost:4545/cli/tests/fixture.json"); + const response1 = response.clone(); + assert(response !== response1); + assertEquals(response.status, response1.status); + assertEquals(response.statusText, response1.statusText); + const u8a = new Uint8Array(await response.arrayBuffer()); + const u8a1 = new Uint8Array(await response1.arrayBuffer()); + for (let i = 0; i < u8a.byteLength; i++) { + assertEquals(u8a[i], u8a1[i]); + } +}); + +unitTest({ perms: { net: true } }, async function fetchEmptyInvalid(): Promise< + void +> { + let err; + try { + await fetch(""); + } catch (err_) { + err = err_; + } + assert(err instanceof URIError); +}); + +unitTest( + { perms: { net: true } }, + async function fetchMultipartFormDataSuccess(): Promise<void> { + const response = await fetch( + "http://localhost:4545/cli/tests/subdir/multipart_form_data.txt" + ); + const formData = await response.formData(); + assert(formData.has("field_1")); + assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n"); + assert(formData.has("field_2")); + /* TODO(ry) Re-enable this test once we bring back the global File type. + const file = formData.get("field_2") as File; + assertEquals(file.name, "file.js"); + */ + // Currently we cannot read from file... + } +); + +unitTest( + { perms: { net: true } }, + async function fetchURLEncodedFormDataSuccess(): Promise<void> { + const response = await fetch( + "http://localhost:4545/cli/tests/subdir/form_urlencoded.txt" + ); + const formData = await response.formData(); + assert(formData.has("field_1")); + assertEquals(formData.get("field_1")!.toString(), "Hi"); + assert(formData.has("field_2")); + assertEquals(formData.get("field_2")!.toString(), "<Deno>"); + } +); + +unitTest( + { + perms: { net: true }, + }, + async function fetchWithRedirection(): Promise<void> { + const response = await fetch("http://localhost:4546/"); // will redirect to http://localhost:4545/ + assertEquals(response.status, 200); + assertEquals(response.statusText, "OK"); + assertEquals(response.url, "http://localhost:4545/"); + const body = await response.text(); + assert(body.includes("<title>Directory listing for /</title>")); + } +); + +unitTest( + { + perms: { net: true }, + }, + async function fetchWithRelativeRedirection(): Promise<void> { + const response = await fetch("http://localhost:4545/cli/tests"); // will redirect to /cli/tests/ + assertEquals(response.status, 200); + assertEquals(response.statusText, "OK"); + const body = await response.text(); + assert(body.includes("<title>Directory listing for /cli/tests/</title>")); + } +); + +unitTest( + { + // FIXME(bartlomieju): + // The feature below is not implemented, but the test should work after implementation + ignore: true, + perms: { net: true }, + }, + async function fetchWithInfRedirection(): Promise<void> { + const response = await fetch("http://localhost:4549/cli/tests"); // will redirect to the same place + assertEquals(response.status, 0); // network error + } +); + +unitTest( + { perms: { net: true } }, + async function fetchInitStringBody(): Promise<void> { + const data = "Hello World"; + const response = await fetch("http://localhost:4545/echo_server", { + method: "POST", + body: data, + }); + const text = await response.text(); + assertEquals(text, data); + assert(response.headers.get("content-type")!.startsWith("text/plain")); + } +); + +unitTest( + { perms: { net: true } }, + async function fetchRequestInitStringBody(): Promise<void> { + const data = "Hello World"; + const req = new Request("http://localhost:4545/echo_server", { + method: "POST", + body: data, + }); + const response = await fetch(req); + const text = await response.text(); + assertEquals(text, data); + } +); + +unitTest( + { perms: { net: true } }, + async function fetchInitTypedArrayBody(): Promise<void> { + const data = "Hello World"; + const response = await fetch("http://localhost:4545/echo_server", { + method: "POST", + body: new TextEncoder().encode(data), + }); + const text = await response.text(); + assertEquals(text, data); + } +); + +unitTest( + { perms: { net: true } }, + async function fetchInitURLSearchParamsBody(): Promise<void> { + const data = "param1=value1¶m2=value2"; + const params = new URLSearchParams(data); + const response = await fetch("http://localhost:4545/echo_server", { + method: "POST", + body: params, + }); + const text = await response.text(); + assertEquals(text, data); + assert( + response.headers + .get("content-type")! + .startsWith("application/x-www-form-urlencoded") + ); + } +); + +unitTest({ perms: { net: true } }, async function fetchInitBlobBody(): Promise< + void +> { + const data = "const a = 1"; + const blob = new Blob([data], { + type: "text/javascript", + }); + const response = await fetch("http://localhost:4545/echo_server", { + method: "POST", + body: blob, + }); + const text = await response.text(); + assertEquals(text, data); + assert(response.headers.get("content-type")!.startsWith("text/javascript")); +}); + +unitTest( + { perms: { net: true } }, + async function fetchInitFormDataBody(): Promise<void> { + const form = new FormData(); + form.append("field", "value"); + const response = await fetch("http://localhost:4545/echo_server", { + method: "POST", + body: form, + }); + const resultForm = await response.formData(); + assertEquals(form.get("field"), resultForm.get("field")); + } +); + +unitTest({ perms: { net: true } }, async function fetchUserAgent(): Promise< + void +> { + const data = "Hello World"; + const response = await fetch("http://localhost:4545/echo_server", { + method: "POST", + body: new TextEncoder().encode(data), + }); + assertEquals(response.headers.get("user-agent"), `Deno/${Deno.version.deno}`); + await response.text(); +}); + +// TODO(ry) The following tests work but are flaky. There's a race condition +// somewhere. Here is what one of these flaky failures looks like: +// +// unitTest fetchPostBodyString_permW0N1E0R0 +// assertEquals failed. actual = expected = POST /blah HTTP/1.1 +// hello: World +// foo: Bar +// host: 127.0.0.1:4502 +// content-length: 11 +// hello world +// Error: actual: expected: POST /blah HTTP/1.1 +// hello: World +// foo: Bar +// host: 127.0.0.1:4502 +// content-length: 11 +// hello world +// at Object.assertEquals (file:///C:/deno/js/testing/util.ts:29:11) +// at fetchPostBodyString (file + +function bufferServer(addr: string): Deno.Buffer { + const [hostname, port] = addr.split(":"); + const listener = Deno.listen({ + hostname, + port: Number(port), + }) as Deno.Listener; + const buf = new Deno.Buffer(); + listener.accept().then(async (conn: Deno.Conn) => { + const p1 = buf.readFrom(conn); + const p2 = conn.write( + new TextEncoder().encode( + "HTTP/1.0 404 Not Found\r\nContent-Length: 2\r\n\r\nNF" + ) + ); + // Wait for both an EOF on the read side of the socket and for the write to + // complete before closing it. Due to keep-alive, the EOF won't be sent + // until the Connection close (HTTP/1.0) response, so readFrom() can't + // proceed write. Conversely, if readFrom() is async, waiting for the + // write() to complete is not a guarantee that we've read the incoming + // request. + await Promise.all([p1, p2]); + conn.close(); + listener.close(); + }); + return buf; +} + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + perms: { net: true }, + }, + async function fetchRequest(): Promise<void> { + const addr = "127.0.0.1:4501"; + const buf = bufferServer(addr); + const response = await fetch(`http://${addr}/blah`, { + method: "POST", + headers: [ + ["Hello", "World"], + ["Foo", "Bar"], + ], + }); + assertEquals(response.status, 404); + assertEquals(response.headers.get("Content-Length"), "2"); + + const actual = new TextDecoder().decode(buf.bytes()); + const expected = [ + "POST /blah HTTP/1.1\r\n", + "hello: World\r\n", + "foo: Bar\r\n", + `host: ${addr}\r\n\r\n`, + ].join(""); + assertEquals(actual, expected); + } +); + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + perms: { net: true }, + }, + async function fetchPostBodyString(): Promise<void> { + const addr = "127.0.0.1:4502"; + const buf = bufferServer(addr); + const body = "hello world"; + const response = await fetch(`http://${addr}/blah`, { + method: "POST", + headers: [ + ["Hello", "World"], + ["Foo", "Bar"], + ], + body, + }); + assertEquals(response.status, 404); + assertEquals(response.headers.get("Content-Length"), "2"); + + const actual = new TextDecoder().decode(buf.bytes()); + const expected = [ + "POST /blah HTTP/1.1\r\n", + "hello: World\r\n", + "foo: Bar\r\n", + `host: ${addr}\r\n`, + `content-length: ${body.length}\r\n\r\n`, + body, + ].join(""); + assertEquals(actual, expected); + } +); + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + perms: { net: true }, + }, + async function fetchPostBodyTypedArray(): Promise<void> { + const addr = "127.0.0.1:4503"; + const buf = bufferServer(addr); + const bodyStr = "hello world"; + const body = new TextEncoder().encode(bodyStr); + const response = await fetch(`http://${addr}/blah`, { + method: "POST", + headers: [ + ["Hello", "World"], + ["Foo", "Bar"], + ], + body, + }); + assertEquals(response.status, 404); + assertEquals(response.headers.get("Content-Length"), "2"); + + const actual = new TextDecoder().decode(buf.bytes()); + const expected = [ + "POST /blah HTTP/1.1\r\n", + "hello: World\r\n", + "foo: Bar\r\n", + `host: ${addr}\r\n`, + `content-length: ${body.byteLength}\r\n\r\n`, + bodyStr, + ].join(""); + assertEquals(actual, expected); + } +); + +unitTest( + { + perms: { net: true }, + }, + async function fetchWithManualRedirection(): Promise<void> { + const response = await fetch("http://localhost:4546/", { + redirect: "manual", + }); // will redirect to http://localhost:4545/ + assertEquals(response.status, 0); + assertEquals(response.statusText, ""); + assertEquals(response.url, ""); + assertEquals(response.type, "opaqueredirect"); + try { + await response.text(); + fail( + "Reponse.text() didn't throw on a filtered response without a body (type opaqueredirect)" + ); + } catch (e) { + return; + } + } +); + +unitTest( + { + perms: { net: true }, + }, + async function fetchWithErrorRedirection(): Promise<void> { + const response = await fetch("http://localhost:4546/", { + redirect: "error", + }); // will redirect to http://localhost:4545/ + assertEquals(response.status, 0); + assertEquals(response.statusText, ""); + assertEquals(response.url, ""); + assertEquals(response.type, "error"); + try { + await response.text(); + fail( + "Reponse.text() didn't throw on a filtered response without a body (type error)" + ); + } catch (e) { + return; + } + } +); + +unitTest(function responseRedirect(): void { + const redir = Response.redirect("example.com/newLocation", 301); + assertEquals(redir.status, 301); + assertEquals(redir.statusText, ""); + assertEquals(redir.url, ""); + assertEquals(redir.headers.get("Location"), "example.com/newLocation"); + assertEquals(redir.type, "default"); +}); + +unitTest(function responseConstructionHeaderRemoval(): void { + const res = new Response( + "example.com", + 200, + "OK", + [["Set-Cookie", "mysessionid"]], + -1, + false, + "basic", + null + ); + assert(res.headers.get("Set-Cookie") != "mysessionid"); +}); diff --git a/cli/tests/unit/file_test.ts b/cli/tests/unit/file_test.ts new file mode 100644 index 000000000..4941554ad --- /dev/null +++ b/cli/tests/unit/file_test.ts @@ -0,0 +1,105 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function testFirstArgument(arg1: any[], expectedSize: number): void { + const file = new File(arg1, "name"); + assert(file instanceof File); + assertEquals(file.name, "name"); + assertEquals(file.size, expectedSize); + assertEquals(file.type, ""); +} + +unitTest(function fileEmptyFileBits(): void { + testFirstArgument([], 0); +}); + +unitTest(function fileStringFileBits(): void { + testFirstArgument(["bits"], 4); +}); + +unitTest(function fileUnicodeStringFileBits(): void { + testFirstArgument(["𝓽𝓮𝔁𝓽"], 16); +}); + +unitTest(function fileStringObjectFileBits(): void { + testFirstArgument([new String("string object")], 13); +}); + +unitTest(function fileEmptyBlobFileBits(): void { + testFirstArgument([new Blob()], 0); +}); + +unitTest(function fileBlobFileBits(): void { + testFirstArgument([new Blob(["bits"])], 4); +}); + +unitTest(function fileEmptyFileFileBits(): void { + testFirstArgument([new File([], "world.txt")], 0); +}); + +unitTest(function fileFileFileBits(): void { + testFirstArgument([new File(["bits"], "world.txt")], 4); +}); + +unitTest(function fileArrayBufferFileBits(): void { + testFirstArgument([new ArrayBuffer(8)], 8); +}); + +unitTest(function fileTypedArrayFileBits(): void { + testFirstArgument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4); +}); + +unitTest(function fileVariousFileBits(): void { + testFirstArgument( + [ + "bits", + new Blob(["bits"]), + new Blob(), + new Uint8Array([0x50, 0x41]), + new Uint16Array([0x5353]), + new Uint32Array([0x53534150]), + ], + 16 + ); +}); + +unitTest(function fileNumberInFileBits(): void { + testFirstArgument([12], 2); +}); + +unitTest(function fileArrayInFileBits(): void { + testFirstArgument([[1, 2, 3]], 5); +}); + +unitTest(function fileObjectInFileBits(): void { + // "[object Object]" + testFirstArgument([{}], 15); +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function testSecondArgument(arg2: any, expectedFileName: string): void { + const file = new File(["bits"], arg2); + assert(file instanceof File); + assertEquals(file.name, expectedFileName); +} + +unitTest(function fileUsingFileName(): void { + testSecondArgument("dummy", "dummy"); +}); + +unitTest(function fileUsingSpecialCharacterInFileName(): void { + testSecondArgument("dummy/foo", "dummy:foo"); +}); + +unitTest(function fileUsingNullFileName(): void { + testSecondArgument(null, "null"); +}); + +unitTest(function fileUsingNumberFileName(): void { + testSecondArgument(1, "1"); +}); + +unitTest(function fileUsingEmptyStringFileName(): void { + testSecondArgument("", ""); +}); diff --git a/cli/tests/unit/files_test.ts b/cli/tests/unit/files_test.ts new file mode 100644 index 000000000..a035c7074 --- /dev/null +++ b/cli/tests/unit/files_test.ts @@ -0,0 +1,571 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertStrContains, +} from "./test_util.ts"; + +unitTest(function filesStdioFileDescriptors(): void { + assertEquals(Deno.stdin.rid, 0); + assertEquals(Deno.stdout.rid, 1); + assertEquals(Deno.stderr.rid, 2); +}); + +unitTest({ perms: { read: true } }, async function filesCopyToStdout(): Promise< + void +> { + const filename = "cli/tests/fixture.json"; + const file = await Deno.open(filename); + assert(file.rid > 2); + const bytesWritten = await Deno.copy(file, Deno.stdout); + const fileSize = Deno.statSync(filename).size; + assertEquals(bytesWritten, fileSize); + console.log("bytes written", bytesWritten); + file.close(); +}); + +unitTest({ perms: { read: true } }, async function filesIter(): Promise<void> { + const filename = "cli/tests/hello.txt"; + const file = await Deno.open(filename); + + let totalSize = 0; + for await (const buf of Deno.iter(file)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); + file.close(); +}); + +unitTest( + { perms: { read: true } }, + async function filesIterCustomBufSize(): Promise<void> { + const filename = "cli/tests/hello.txt"; + const file = await Deno.open(filename); + + let totalSize = 0; + let iterations = 0; + for await (const buf of Deno.iter(file, { bufSize: 6 })) { + totalSize += buf.byteLength; + iterations += 1; + } + + assertEquals(totalSize, 12); + assertEquals(iterations, 2); + file.close(); + } +); + +unitTest({ perms: { read: true } }, function filesIterSync(): void { + const filename = "cli/tests/hello.txt"; + const file = Deno.openSync(filename); + + let totalSize = 0; + for (const buf of Deno.iterSync(file)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); + file.close(); +}); + +unitTest( + { perms: { read: true } }, + function filesIterSyncCustomBufSize(): void { + const filename = "cli/tests/hello.txt"; + const file = Deno.openSync(filename); + + let totalSize = 0; + let iterations = 0; + for (const buf of Deno.iterSync(file, { bufSize: 6 })) { + totalSize += buf.byteLength; + iterations += 1; + } + + assertEquals(totalSize, 12); + assertEquals(iterations, 2); + file.close(); + } +); + +unitTest(async function readerIter(): Promise<void> { + // ref: https://github.com/denoland/deno/issues/2330 + const encoder = new TextEncoder(); + + class TestReader implements Deno.Reader { + #offset = 0; + #buf: Uint8Array; + + constructor(s: string) { + this.#buf = new Uint8Array(encoder.encode(s)); + } + + read(p: Uint8Array): Promise<number | null> { + const n = Math.min(p.byteLength, this.#buf.byteLength - this.#offset); + p.set(this.#buf.slice(this.#offset, this.#offset + n)); + this.#offset += n; + + if (n === 0) { + return Promise.resolve(null); + } + + return Promise.resolve(n); + } + } + + const reader = new TestReader("hello world!"); + + let totalSize = 0; + for await (const buf of Deno.iter(reader)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); +}); + +unitTest(async function readerIterSync(): Promise<void> { + // ref: https://github.com/denoland/deno/issues/2330 + const encoder = new TextEncoder(); + + class TestReader implements Deno.ReaderSync { + #offset = 0; + #buf: Uint8Array; + + constructor(s: string) { + this.#buf = new Uint8Array(encoder.encode(s)); + } + + readSync(p: Uint8Array): number | null { + const n = Math.min(p.byteLength, this.#buf.byteLength - this.#offset); + p.set(this.#buf.slice(this.#offset, this.#offset + n)); + this.#offset += n; + + if (n === 0) { + return null; + } + + return n; + } + } + + const reader = new TestReader("hello world!"); + + let totalSize = 0; + for await (const buf of Deno.iterSync(reader)) { + totalSize += buf.byteLength; + } + + assertEquals(totalSize, 12); +}); + +unitTest( + { + perms: { read: true, write: true }, + }, + function openSyncMode(): void { + const path = Deno.makeTempDirSync() + "/test_openSync.txt"; + const file = Deno.openSync(path, { + write: true, + createNew: true, + mode: 0o626, + }); + file.close(); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask()); + } + } +); + +unitTest( + { + perms: { read: true, write: true }, + }, + async function openMode(): Promise<void> { + const path = (await Deno.makeTempDir()) + "/test_open.txt"; + const file = await Deno.open(path, { + write: true, + createNew: true, + mode: 0o626, + }); + file.close(); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o626 & ~Deno.umask()); + } + } +); + +unitTest( + { perms: { write: false } }, + async function writePermFailure(): Promise<void> { + const filename = "tests/hello.txt"; + const openOptions: Deno.OpenOptions[] = [{ write: true }, { append: true }]; + for (const options of openOptions) { + let err; + try { + await Deno.open(filename, options); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } + } +); + +unitTest(async function openOptions(): Promise<void> { + const filename = "cli/tests/fixture.json"; + let err; + try { + await Deno.open(filename, { write: false }); + } catch (e) { + err = e; + } + assert(!!err); + assertStrContains( + err.message, + "OpenOptions requires at least one option to be true" + ); + + try { + await Deno.open(filename, { truncate: true, write: false }); + } catch (e) { + err = e; + } + assert(!!err); + assertStrContains(err.message, "'truncate' option requires 'write' option"); + + try { + await Deno.open(filename, { create: true, write: false }); + } catch (e) { + err = e; + } + assert(!!err); + assertStrContains( + err.message, + "'create' or 'createNew' options require 'write' or 'append' option" + ); + + try { + await Deno.open(filename, { createNew: true, append: false }); + } catch (e) { + err = e; + } + assert(!!err); + assertStrContains( + err.message, + "'create' or 'createNew' options require 'write' or 'append' option" + ); +}); + +unitTest({ perms: { read: false } }, async function readPermFailure(): Promise< + void +> { + let caughtError = false; + try { + await Deno.open("package.json", { read: true }); + await Deno.open("cli/tests/fixture.json", { read: true }); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest( + { perms: { write: true } }, + async function writeNullBufferFailure(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "hello.txt"; + const w = { + write: true, + truncate: true, + create: true, + }; + const file = await Deno.open(filename, w); + + // writing null should throw an error + let err; + try { + // @ts-ignore + await file.write(null); + } catch (e) { + err = e; + } + // TODO: Check error kind when dispatch_minimal pipes errors properly + assert(!!err); + + file.close(); + await Deno.remove(tempDir, { recursive: true }); + } +); + +unitTest( + { perms: { write: true, read: true } }, + async function readNullBufferFailure(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "hello.txt"; + const file = await Deno.open(filename, { + read: true, + write: true, + truncate: true, + create: true, + }); + + // reading into an empty buffer should return 0 immediately + const bytesRead = await file.read(new Uint8Array(0)); + assert(bytesRead === 0); + + // reading file into null buffer should throw an error + let err; + try { + // @ts-ignore + await file.read(null); + } catch (e) { + err = e; + } + // TODO: Check error kind when dispatch_minimal pipes errors properly + assert(!!err); + + file.close(); + await Deno.remove(tempDir, { recursive: true }); + } +); + +unitTest( + { perms: { write: false, read: false } }, + async function readWritePermFailure(): Promise<void> { + const filename = "tests/hello.txt"; + let err; + try { + await Deno.open(filename, { read: true }); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function createFile(): Promise<void> { + const tempDir = await Deno.makeTempDir(); + const filename = tempDir + "/test.txt"; + const f = await Deno.create(filename); + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assert(fileInfo.size === 0); + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + await f.write(data); + fileInfo = Deno.statSync(filename); + assert(fileInfo.size === 5); + f.close(); + + // TODO: test different modes + await Deno.remove(tempDir, { recursive: true }); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function openModeWrite(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const encoder = new TextEncoder(); + const filename = tempDir + "hello.txt"; + const data = encoder.encode("Hello world!\n"); + let file = await Deno.open(filename, { + create: true, + write: true, + truncate: true, + }); + // assert file was created + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assertEquals(fileInfo.size, 0); + // write some data + await file.write(data); + fileInfo = Deno.statSync(filename); + assertEquals(fileInfo.size, 13); + // assert we can't read from file + let thrown = false; + try { + const buf = new Uint8Array(20); + await file.read(buf); + } catch (e) { + thrown = true; + } finally { + assert(thrown, "'w' mode shouldn't allow to read file"); + } + file.close(); + // assert that existing file is truncated on open + file = await Deno.open(filename, { + write: true, + truncate: true, + }); + file.close(); + const fileSize = Deno.statSync(filename).size; + assertEquals(fileSize, 0); + await Deno.remove(tempDir, { recursive: true }); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function openModeWriteRead(): Promise<void> { + const tempDir = Deno.makeTempDirSync(); + const encoder = new TextEncoder(); + const filename = tempDir + "hello.txt"; + const data = encoder.encode("Hello world!\n"); + + const file = await Deno.open(filename, { + write: true, + truncate: true, + create: true, + read: true, + }); + const seekPosition = 0; + // assert file was created + let fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); + assertEquals(fileInfo.size, 0); + // write some data + await file.write(data); + fileInfo = Deno.statSync(filename); + assertEquals(fileInfo.size, 13); + + const buf = new Uint8Array(20); + // seeking from beginning of a file + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, cursorPosition); + const result = await file.read(buf); + assertEquals(result, 13); + file.close(); + + await Deno.remove(tempDir, { recursive: true }); + } +); + +unitTest({ perms: { read: true } }, async function seekStart(): Promise<void> { + const filename = "cli/tests/hello.txt"; + const file = await Deno.open(filename); + const seekPosition = 6; + // Deliberately move 1 step forward + await file.read(new Uint8Array(1)); // "H" + // Skipping "Hello " + // seeking from beginning of a file plus seekPosition + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, cursorPosition); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); + file.close(); +}); + +unitTest({ perms: { read: true } }, function seekSyncStart(): void { + const filename = "cli/tests/hello.txt"; + const file = Deno.openSync(filename); + const seekPosition = 6; + // Deliberately move 1 step forward + file.readSync(new Uint8Array(1)); // "H" + // Skipping "Hello " + // seeking from beginning of a file plus seekPosition + const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Start); + assertEquals(seekPosition, cursorPosition); + const buf = new Uint8Array(6); + file.readSync(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); + file.close(); +}); + +unitTest({ perms: { read: true } }, async function seekCurrent(): Promise< + void +> { + const filename = "cli/tests/hello.txt"; + const file = await Deno.open(filename); + // Deliberately move 1 step forward + await file.read(new Uint8Array(1)); // "H" + // Skipping "ello " + const seekPosition = 5; + // seekPosition is relative to current cursor position after read + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.Current); + assertEquals(seekPosition + 1, cursorPosition); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); + file.close(); +}); + +unitTest({ perms: { read: true } }, function seekSyncCurrent(): void { + const filename = "cli/tests/hello.txt"; + const file = Deno.openSync(filename); + // Deliberately move 1 step forward + file.readSync(new Uint8Array(1)); // "H" + // Skipping "ello " + const seekPosition = 5; + // seekPosition is relative to current cursor position after read + const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.Current); + assertEquals(seekPosition + 1, cursorPosition); + const buf = new Uint8Array(6); + file.readSync(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); + file.close(); +}); + +unitTest({ perms: { read: true } }, async function seekEnd(): Promise<void> { + const filename = "cli/tests/hello.txt"; + const file = await Deno.open(filename); + const seekPosition = -6; + // seek from end of file that has 12 chars, 12 - 6 = 6 + const cursorPosition = await file.seek(seekPosition, Deno.SeekMode.End); + assertEquals(6, cursorPosition); + const buf = new Uint8Array(6); + await file.read(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); + file.close(); +}); + +unitTest({ perms: { read: true } }, function seekSyncEnd(): void { + const filename = "cli/tests/hello.txt"; + const file = Deno.openSync(filename); + const seekPosition = -6; + // seek from end of file that has 12 chars, 12 - 6 = 6 + const cursorPosition = file.seekSync(seekPosition, Deno.SeekMode.End); + assertEquals(6, cursorPosition); + const buf = new Uint8Array(6); + file.readSync(buf); + const decoded = new TextDecoder().decode(buf); + assertEquals(decoded, "world!"); + file.close(); +}); + +unitTest({ perms: { read: true } }, async function seekMode(): Promise<void> { + const filename = "cli/tests/hello.txt"; + const file = await Deno.open(filename); + let err; + try { + await file.seek(1, -1); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof TypeError); + assertStrContains(err.message, "Invalid seek mode"); + + // We should still be able to read the file + // since it is still open. + const buf = new Uint8Array(1); + await file.read(buf); // "H" + assertEquals(new TextDecoder().decode(buf), "H"); + file.close(); +}); diff --git a/cli/tests/unit/form_data_test.ts b/cli/tests/unit/form_data_test.ts new file mode 100644 index 000000000..10cbd30a7 --- /dev/null +++ b/cli/tests/unit/form_data_test.ts @@ -0,0 +1,203 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertStrContains, +} from "./test_util.ts"; + +unitTest({ ignore: true }, function formDataHasCorrectNameProp(): void { + assertEquals(FormData.name, "FormData"); +}); + +unitTest(function formDataParamsAppendSuccess(): void { + const formData = new FormData(); + formData.append("a", "true"); + assertEquals(formData.get("a"), "true"); +}); + +unitTest(function formDataParamsDeleteSuccess(): void { + const formData = new FormData(); + formData.append("a", "true"); + formData.append("b", "false"); + assertEquals(formData.get("b"), "false"); + formData.delete("b"); + assertEquals(formData.get("a"), "true"); + assertEquals(formData.get("b"), null); +}); + +unitTest(function formDataParamsGetAllSuccess(): void { + const formData = new FormData(); + formData.append("a", "true"); + formData.append("b", "false"); + formData.append("a", "null"); + assertEquals(formData.getAll("a"), ["true", "null"]); + assertEquals(formData.getAll("b"), ["false"]); + assertEquals(formData.getAll("c"), []); +}); + +unitTest(function formDataParamsGetSuccess(): void { + const formData = new FormData(); + formData.append("a", "true"); + formData.append("b", "false"); + formData.append("a", "null"); + // @ts-ignore + formData.append("d", undefined); + // @ts-ignore + formData.append("e", null); + assertEquals(formData.get("a"), "true"); + assertEquals(formData.get("b"), "false"); + assertEquals(formData.get("c"), null); + assertEquals(formData.get("d"), "undefined"); + assertEquals(formData.get("e"), "null"); +}); + +unitTest(function formDataParamsHasSuccess(): void { + const formData = new FormData(); + formData.append("a", "true"); + formData.append("b", "false"); + assert(formData.has("a")); + assert(formData.has("b")); + assert(!formData.has("c")); +}); + +unitTest(function formDataParamsSetSuccess(): void { + const formData = new FormData(); + formData.append("a", "true"); + formData.append("b", "false"); + formData.append("a", "null"); + assertEquals(formData.getAll("a"), ["true", "null"]); + assertEquals(formData.getAll("b"), ["false"]); + formData.set("a", "false"); + assertEquals(formData.getAll("a"), ["false"]); + // @ts-ignore + formData.set("d", undefined); + assertEquals(formData.get("d"), "undefined"); + // @ts-ignore + formData.set("e", null); + assertEquals(formData.get("e"), "null"); +}); + +unitTest(function fromDataUseDomFile(): void { + const formData = new FormData(); + const file = new File(["foo"], "bar", { + type: "text/plain", + }); + formData.append("file", file); + assertEquals(formData.get("file"), file); +}); + +unitTest(function formDataSetEmptyBlobSuccess(): void { + const formData = new FormData(); + formData.set("a", new Blob([]), "blank.txt"); + formData.get("a"); + /* TODO Fix this test. + assert(file instanceof File); + if (typeof file !== "string") { + assertEquals(file.name, "blank.txt"); + } + */ +}); + +unitTest(function formDataParamsForEachSuccess(): void { + const init = [ + ["a", "54"], + ["b", "true"], + ]; + const formData = new FormData(); + for (const [name, value] of init) { + formData.append(name, value); + } + let callNum = 0; + formData.forEach((value, key, parent): void => { + assertEquals(formData, parent); + assertEquals(value, init[callNum][1]); + assertEquals(key, init[callNum][0]); + callNum++; + }); + assertEquals(callNum, init.length); +}); + +unitTest(function formDataParamsArgumentsCheck(): void { + const methodRequireOneParam = [ + "delete", + "getAll", + "get", + "has", + "forEach", + ] as const; + + const methodRequireTwoParams = ["append", "set"] as const; + + methodRequireOneParam.forEach((method): void => { + const formData = new FormData(); + let hasThrown = 0; + let errMsg = ""; + try { + // @ts-ignore + formData[method](); + hasThrown = 1; + } catch (err) { + errMsg = err.message; + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + assertStrContains( + errMsg, + `${method} requires at least 1 argument, but only 0 present` + ); + }); + + methodRequireTwoParams.forEach((method: string): void => { + const formData = new FormData(); + let hasThrown = 0; + let errMsg = ""; + + try { + // @ts-ignore + formData[method](); + hasThrown = 1; + } catch (err) { + errMsg = err.message; + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + assertStrContains( + errMsg, + `${method} requires at least 2 arguments, but only 0 present` + ); + + hasThrown = 0; + errMsg = ""; + try { + // @ts-ignore + formData[method]("foo"); + hasThrown = 1; + } catch (err) { + errMsg = err.message; + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + assertStrContains( + errMsg, + `${method} requires at least 2 arguments, but only 1 present` + ); + }); +}); + +unitTest(function toStringShouldBeWebCompatibility(): void { + const formData = new FormData(); + assertEquals(formData.toString(), "[object FormData]"); +}); diff --git a/cli/tests/unit/format_error_test.ts b/cli/tests/unit/format_error_test.ts new file mode 100644 index 000000000..ae7559c82 --- /dev/null +++ b/cli/tests/unit/format_error_test.ts @@ -0,0 +1,33 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { assert, unitTest } from "./test_util.ts"; + +unitTest(function formatDiagnosticBasic() { + const fixture: Deno.DiagnosticItem[] = [ + { + message: "Example error", + category: Deno.DiagnosticCategory.Error, + sourceLine: "abcdefghijklmnopqrstuv", + lineNumber: 1000, + scriptResourceName: "foo.ts", + startColumn: 1, + endColumn: 2, + code: 4000, + }, + ]; + const out = Deno.formatDiagnostics(fixture); + assert(out.includes("Example error")); + assert(out.includes("foo.ts")); +}); + +unitTest(function formatDiagnosticError() { + let thrown = false; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const bad = ([{ hello: 123 }] as any) as Deno.DiagnosticItem[]; + try { + Deno.formatDiagnostics(bad); + } catch (e) { + assert(e instanceof Deno.errors.InvalidData); + thrown = true; + } + assert(thrown); +}); diff --git a/cli/tests/unit/fs_events_test.ts b/cli/tests/unit/fs_events_test.ts new file mode 100644 index 000000000..ad8ba8502 --- /dev/null +++ b/cli/tests/unit/fs_events_test.ts @@ -0,0 +1,71 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +// TODO(ry) Add more tests to specify format. + +unitTest({ perms: { read: false } }, function watchFsPermissions() { + let thrown = false; + try { + Deno.watchFs("."); + } catch (err) { + assert(err instanceof Deno.errors.PermissionDenied); + thrown = true; + } + assert(thrown); +}); + +unitTest({ perms: { read: true } }, function watchFsInvalidPath() { + let thrown = false; + try { + Deno.watchFs("non-existant.file"); + } catch (err) { + console.error(err); + if (Deno.build.os === "windows") { + assert( + err.message.includes( + "Input watch path is neither a file nor a directory" + ) + ); + } else { + assert(err instanceof Deno.errors.NotFound); + } + thrown = true; + } + assert(thrown); +}); + +async function getTwoEvents( + iter: AsyncIterableIterator<Deno.FsEvent> +): Promise<Deno.FsEvent[]> { + const events = []; + for await (const event of iter) { + events.push(event); + if (events.length > 2) break; + } + return events; +} + +unitTest( + { perms: { read: true, write: true } }, + async function watchFsBasic(): Promise<void> { + const testDir = await Deno.makeTempDir(); + const iter = Deno.watchFs(testDir); + + // Asynchornously capture two fs events. + const eventsPromise = getTwoEvents(iter); + + // Make some random file system activity. + const file1 = testDir + "/file1.txt"; + const file2 = testDir + "/file2.txt"; + Deno.writeFileSync(file1, new Uint8Array([0, 1, 2])); + Deno.writeFileSync(file2, new Uint8Array([0, 1, 2])); + + // We should have gotten two fs events. + const events = await eventsPromise; + assert(events.length >= 2); + assert(events[0].kind == "create"); + assert(events[0].paths[0].includes(testDir)); + assert(events[1].kind == "create" || events[1].kind == "modify"); + assert(events[1].paths[0].includes(testDir)); + } +); diff --git a/cli/tests/unit/get_random_values_test.ts b/cli/tests/unit/get_random_values_test.ts new file mode 100644 index 000000000..76fa732ea --- /dev/null +++ b/cli/tests/unit/get_random_values_test.ts @@ -0,0 +1,51 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertNotEquals, assertStrictEq } from "./test_util.ts"; + +unitTest(function getRandomValuesInt8Array(): void { + const arr = new Int8Array(32); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Int8Array(32)); +}); + +unitTest(function getRandomValuesUint8Array(): void { + const arr = new Uint8Array(32); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Uint8Array(32)); +}); + +unitTest(function getRandomValuesUint8ClampedArray(): void { + const arr = new Uint8ClampedArray(32); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Uint8ClampedArray(32)); +}); + +unitTest(function getRandomValuesInt16Array(): void { + const arr = new Int16Array(4); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Int16Array(4)); +}); + +unitTest(function getRandomValuesUint16Array(): void { + const arr = new Uint16Array(4); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Uint16Array(4)); +}); + +unitTest(function getRandomValuesInt32Array(): void { + const arr = new Int32Array(8); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Int32Array(8)); +}); + +unitTest(function getRandomValuesUint32Array(): void { + const arr = new Uint32Array(8); + crypto.getRandomValues(arr); + assertNotEquals(arr, new Uint32Array(8)); +}); + +unitTest(function getRandomValuesReturnValue(): void { + const arr = new Uint32Array(8); + const rtn = crypto.getRandomValues(arr); + assertNotEquals(arr, new Uint32Array(8)); + assertStrictEq(rtn, arr); +}); diff --git a/cli/tests/unit/globals_test.ts b/cli/tests/unit/globals_test.ts new file mode 100644 index 000000000..aa8b4f46e --- /dev/null +++ b/cli/tests/unit/globals_test.ts @@ -0,0 +1,112 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +unitTest(function globalThisExists(): void { + assert(globalThis != null); +}); + +unitTest(function windowExists(): void { + assert(window != null); +}); + +unitTest(function selfExists(): void { + assert(self != null); +}); + +unitTest(function windowWindowExists(): void { + assert(window.window === window); +}); + +unitTest(function windowSelfExists(): void { + assert(window.self === window); +}); + +unitTest(function globalThisEqualsWindow(): void { + assert(globalThis === window); +}); + +unitTest(function globalThisEqualsSelf(): void { + assert(globalThis === self); +}); + +unitTest(function DenoNamespaceExists(): void { + assert(Deno != null); +}); + +unitTest(function DenoNamespaceEqualsWindowDeno(): void { + assert(Deno === window.Deno); +}); + +unitTest(function DenoNamespaceIsFrozen(): void { + assert(Object.isFrozen(Deno)); +}); + +unitTest(function webAssemblyExists(): void { + assert(typeof WebAssembly.compile === "function"); +}); + +unitTest(function DenoNamespaceImmutable(): void { + const denoCopy = window.Deno; + try { + // @ts-ignore + Deno = 1; + } catch {} + assert(denoCopy === Deno); + try { + // @ts-ignore + window.Deno = 1; + } catch {} + assert(denoCopy === Deno); + try { + delete window.Deno; + } catch {} + assert(denoCopy === Deno); + + const { readFile } = Deno; + try { + // @ts-ignore + Deno.readFile = 1; + } catch {} + assert(readFile === Deno.readFile); + try { + delete window.Deno.readFile; + } catch {} + assert(readFile === Deno.readFile); + + // @ts-ignore + const { print } = Deno.core; + try { + // @ts-ignore + Deno.core.print = 1; + } catch {} + // @ts-ignore + assert(print === Deno.core.print); + try { + // @ts-ignore + delete Deno.core.print; + } catch {} + // @ts-ignore + assert(print === Deno.core.print); +}); + +unitTest(async function windowQueueMicrotask(): Promise<void> { + let resolve1: () => void | undefined; + let resolve2: () => void | undefined; + let microtaskDone = false; + const p1 = new Promise((res): void => { + resolve1 = (): void => { + microtaskDone = true; + res(); + }; + }); + const p2 = new Promise((res): void => { + resolve2 = (): void => { + assert(microtaskDone); + res(); + }; + }); + window.queueMicrotask(resolve1!); + setTimeout(resolve2!, 0); + await p1; + await p2; +}); diff --git a/cli/tests/unit/headers_test.ts b/cli/tests/unit/headers_test.ts new file mode 100644 index 000000000..aaa829837 --- /dev/null +++ b/cli/tests/unit/headers_test.ts @@ -0,0 +1,420 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertStrContains, +} from "./test_util.ts"; +const { + stringifyArgs, + // @ts-ignore TypeScript (as of 3.7) does not support indexing namespaces by symbol +} = Deno[Deno.internal]; + +// Logic heavily copied from web-platform-tests, make +// sure pass mostly header basic test +// ref: https://github.com/web-platform-tests/wpt/blob/7c50c216081d6ea3c9afe553ee7b64534020a1b2/fetch/api/headers/headers-basic.html +unitTest(function newHeaderTest(): void { + new Headers(); + new Headers(undefined); + new Headers({}); + try { + // @ts-ignore + new Headers(null); + } catch (e) { + assertEquals( + e.message, + "Failed to construct 'Headers'; The provided value was not valid" + ); + } +}); + +const headerDict: Record<string, string> = { + name1: "value1", + name2: "value2", + name3: "value3", + // @ts-ignore + name4: undefined, + "Content-Type": "value4", +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const headerSeq: any[] = []; +for (const name in headerDict) { + headerSeq.push([name, headerDict[name]]); +} + +unitTest(function newHeaderWithSequence(): void { + const headers = new Headers(headerSeq); + for (const name in headerDict) { + assertEquals(headers.get(name), String(headerDict[name])); + } + assertEquals(headers.get("length"), null); +}); + +unitTest(function newHeaderWithRecord(): void { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assertEquals(headers.get(name), String(headerDict[name])); + } +}); + +unitTest(function newHeaderWithHeadersInstance(): void { + const headers = new Headers(headerDict); + const headers2 = new Headers(headers); + for (const name in headerDict) { + assertEquals(headers2.get(name), String(headerDict[name])); + } +}); + +unitTest(function headerAppendSuccess(): void { + const headers = new Headers(); + for (const name in headerDict) { + headers.append(name, headerDict[name]); + assertEquals(headers.get(name), String(headerDict[name])); + } +}); + +unitTest(function headerSetSuccess(): void { + const headers = new Headers(); + for (const name in headerDict) { + headers.set(name, headerDict[name]); + assertEquals(headers.get(name), String(headerDict[name])); + } +}); + +unitTest(function headerHasSuccess(): void { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assert(headers.has(name), "headers has name " + name); + assert( + !headers.has("nameNotInHeaders"), + "headers do not have header: nameNotInHeaders" + ); + } +}); + +unitTest(function headerDeleteSuccess(): void { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assert(headers.has(name), "headers have a header: " + name); + headers.delete(name); + assert(!headers.has(name), "headers do not have anymore a header: " + name); + } +}); + +unitTest(function headerGetSuccess(): void { + const headers = new Headers(headerDict); + for (const name in headerDict) { + assertEquals(headers.get(name), String(headerDict[name])); + assertEquals(headers.get("nameNotInHeaders"), null); + } +}); + +unitTest(function headerEntriesSuccess(): void { + const headers = new Headers(headerDict); + const iterators = headers.entries(); + for (const it of iterators) { + const key = it[0]; + const value = it[1]; + assert(headers.has(key)); + assertEquals(value, headers.get(key)); + } +}); + +unitTest(function headerKeysSuccess(): void { + const headers = new Headers(headerDict); + const iterators = headers.keys(); + for (const it of iterators) { + assert(headers.has(it)); + } +}); + +unitTest(function headerValuesSuccess(): void { + const headers = new Headers(headerDict); + const iterators = headers.values(); + const entries = headers.entries(); + const values = []; + for (const pair of entries) { + values.push(pair[1]); + } + for (const it of iterators) { + assert(values.includes(it)); + } +}); + +const headerEntriesDict: Record<string, string> = { + name1: "value1", + Name2: "value2", + name: "value3", + "content-Type": "value4", + "Content-Typ": "value5", + "Content-Types": "value6", +}; + +unitTest(function headerForEachSuccess(): void { + const headers = new Headers(headerEntriesDict); + const keys = Object.keys(headerEntriesDict); + keys.forEach((key): void => { + const value = headerEntriesDict[key]; + const newkey = key.toLowerCase(); + headerEntriesDict[newkey] = value; + }); + let callNum = 0; + headers.forEach((value, key, container): void => { + assertEquals(headers, container); + assertEquals(value, headerEntriesDict[key]); + callNum++; + }); + assertEquals(callNum, keys.length); +}); + +unitTest(function headerSymbolIteratorSuccess(): void { + assert(Symbol.iterator in Headers.prototype); + const headers = new Headers(headerEntriesDict); + for (const header of headers) { + const key = header[0]; + const value = header[1]; + assert(headers.has(key)); + assertEquals(value, headers.get(key)); + } +}); + +unitTest(function headerTypesAvailable(): void { + function newHeaders(): Headers { + return new Headers(); + } + const headers = newHeaders(); + assert(headers instanceof Headers); +}); + +// Modified from https://github.com/bitinn/node-fetch/blob/7d3293200a91ad52b5ca7962f9d6fd1c04983edb/test/test.js#L2001-L2014 +// Copyright (c) 2016 David Frank. MIT License. +unitTest(function headerIllegalReject(): void { + let errorCount = 0; + try { + new Headers({ "He y": "ok" }); + } catch (e) { + errorCount++; + } + try { + new Headers({ "Hé-y": "ok" }); + } catch (e) { + errorCount++; + } + try { + new Headers({ "He-y": "ăk" }); + } catch (e) { + errorCount++; + } + const headers = new Headers(); + try { + headers.append("Hé-y", "ok"); + } catch (e) { + errorCount++; + } + try { + headers.delete("Hé-y"); + } catch (e) { + errorCount++; + } + try { + headers.get("Hé-y"); + } catch (e) { + errorCount++; + } + try { + headers.has("Hé-y"); + } catch (e) { + errorCount++; + } + try { + headers.set("Hé-y", "ok"); + } catch (e) { + errorCount++; + } + try { + headers.set("", "ok"); + } catch (e) { + errorCount++; + } + assertEquals(errorCount, 9); + // 'o k' is valid value but invalid name + new Headers({ "He-y": "o k" }); +}); + +// If pair does not contain exactly two items,then throw a TypeError. +unitTest(function headerParamsShouldThrowTypeError(): void { + let hasThrown = 0; + + try { + new Headers(([["1"]] as unknown) as Array<[string, string]>); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + + assertEquals(hasThrown, 2); +}); + +unitTest(function headerParamsArgumentsCheck(): void { + const methodRequireOneParam = ["delete", "get", "has", "forEach"]; + + const methodRequireTwoParams = ["append", "set"]; + + methodRequireOneParam.forEach((method): void => { + const headers = new Headers(); + let hasThrown = 0; + let errMsg = ""; + try { + // @ts-ignore + headers[method](); + hasThrown = 1; + } catch (err) { + errMsg = err.message; + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + assertStrContains( + errMsg, + `${method} requires at least 1 argument, but only 0 present` + ); + }); + + methodRequireTwoParams.forEach((method): void => { + const headers = new Headers(); + let hasThrown = 0; + let errMsg = ""; + + try { + // @ts-ignore + headers[method](); + hasThrown = 1; + } catch (err) { + errMsg = err.message; + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + assertStrContains( + errMsg, + `${method} requires at least 2 arguments, but only 0 present` + ); + + hasThrown = 0; + errMsg = ""; + try { + // @ts-ignore + headers[method]("foo"); + hasThrown = 1; + } catch (err) { + errMsg = err.message; + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + assertStrContains( + errMsg, + `${method} requires at least 2 arguments, but only 1 present` + ); + }); +}); + +unitTest(function headersInitMultiple(): void { + const headers = new Headers([ + ["Set-Cookie", "foo=bar"], + ["Set-Cookie", "bar=baz"], + ["X-Deno", "foo"], + ["X-Deno", "bar"], + ]); + const actual = [...headers]; + assertEquals(actual, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "bar=baz"], + ["x-deno", "foo, bar"], + ]); +}); + +unitTest(function headersAppendMultiple(): void { + const headers = new Headers([ + ["Set-Cookie", "foo=bar"], + ["X-Deno", "foo"], + ]); + headers.append("set-Cookie", "bar=baz"); + headers.append("x-Deno", "bar"); + const actual = [...headers]; + assertEquals(actual, [ + ["set-cookie", "foo=bar"], + ["x-deno", "foo, bar"], + ["set-cookie", "bar=baz"], + ]); +}); + +unitTest(function headersAppendDuplicateSetCookieKey(): void { + const headers = new Headers([["Set-Cookie", "foo=bar"]]); + headers.append("set-Cookie", "foo=baz"); + headers.append("Set-cookie", "baz=bar"); + const actual = [...headers]; + assertEquals(actual, [ + ["set-cookie", "foo=baz"], + ["set-cookie", "baz=bar"], + ]); +}); + +unitTest(function headersSetDuplicateCookieKey(): void { + const headers = new Headers([["Set-Cookie", "foo=bar"]]); + headers.set("set-Cookie", "foo=baz"); + headers.set("set-cookie", "bar=qat"); + const actual = [...headers]; + assertEquals(actual, [ + ["set-cookie", "foo=baz"], + ["set-cookie", "bar=qat"], + ]); +}); + +unitTest(function headersGetSetCookie(): void { + const headers = new Headers([ + ["Set-Cookie", "foo=bar"], + ["set-Cookie", "bar=qat"], + ]); + assertEquals(headers.get("SET-COOKIE"), "foo=bar, bar=qat"); +}); + +unitTest(function toStringShouldBeWebCompatibility(): void { + const headers = new Headers(); + assertEquals(headers.toString(), "[object Headers]"); +}); + +function stringify(...args: unknown[]): string { + return stringifyArgs(args).replace(/\n$/, ""); +} + +unitTest(function customInspectReturnsCorrectHeadersFormat(): void { + const blankHeaders = new Headers(); + assertEquals(stringify(blankHeaders), "Headers {}"); + const singleHeader = new Headers([["Content-Type", "application/json"]]); + assertEquals( + stringify(singleHeader), + "Headers { content-type: application/json }" + ); + const multiParamHeader = new Headers([ + ["Content-Type", "application/json"], + ["Content-Length", "1337"], + ]); + assertEquals( + stringify(multiParamHeader), + "Headers { content-type: application/json, content-length: 1337 }" + ); +}); diff --git a/cli/tests/unit/internals_test.ts b/cli/tests/unit/internals_test.ts new file mode 100644 index 000000000..abd4c94c3 --- /dev/null +++ b/cli/tests/unit/internals_test.ts @@ -0,0 +1,10 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +unitTest(function internalsExists(): void { + const { + stringifyArgs, + // @ts-ignore TypeScript (as of 3.7) does not support indexing namespaces by symbol + } = Deno[Deno.internal]; + assert(!!stringifyArgs); +}); diff --git a/cli/tests/unit/io_test.ts b/cli/tests/unit/io_test.ts new file mode 100644 index 000000000..0ccd83ea2 --- /dev/null +++ b/cli/tests/unit/io_test.ts @@ -0,0 +1,73 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals } from "./test_util.ts"; + +const DEFAULT_BUF_SIZE = 32 * 1024; + +type Spy = { calls: number }; + +function repeat(c: string, bytes: number): Uint8Array { + assertEquals(c.length, 1); + const ui8 = new Uint8Array(bytes); + ui8.fill(c.charCodeAt(0)); + return ui8; +} + +function spyRead(obj: Deno.Buffer): Spy { + const spy: Spy = { + calls: 0, + }; + + const orig = obj.read.bind(obj); + + obj.read = (p: Uint8Array): Promise<number | null> => { + spy.calls++; + return orig(p); + }; + + return spy; +} + +unitTest(async function copyWithDefaultBufferSize() { + const xBytes = repeat("b", DEFAULT_BUF_SIZE); + const reader = new Deno.Buffer(xBytes.buffer as ArrayBuffer); + const write = new Deno.Buffer(); + + const readSpy = spyRead(reader); + + const n = await Deno.copy(reader, write); + + assertEquals(n, xBytes.length); + assertEquals(write.length, xBytes.length); + assertEquals(readSpy.calls, 2); // read with DEFAULT_BUF_SIZE bytes + read with 0 bytes +}); + +unitTest(async function copyWithCustomBufferSize() { + const bufSize = 1024; + const xBytes = repeat("b", DEFAULT_BUF_SIZE); + const reader = new Deno.Buffer(xBytes.buffer as ArrayBuffer); + const write = new Deno.Buffer(); + + const readSpy = spyRead(reader); + + const n = await Deno.copy(reader, write, { bufSize }); + + assertEquals(n, xBytes.length); + assertEquals(write.length, xBytes.length); + assertEquals(readSpy.calls, DEFAULT_BUF_SIZE / bufSize + 1); +}); + +unitTest({ perms: { write: true } }, async function copyBufferToFile() { + const filePath = "test-file.txt"; + // bigger than max File possible buffer 16kb + const bufSize = 32 * 1024; + const xBytes = repeat("b", bufSize); + const reader = new Deno.Buffer(xBytes.buffer as ArrayBuffer); + const write = await Deno.open(filePath, { write: true, create: true }); + + const n = await Deno.copy(reader, write, { bufSize }); + + assertEquals(n, xBytes.length); + + write.close(); + await Deno.remove(filePath); +}); diff --git a/cli/tests/unit/link_test.ts b/cli/tests/unit/link_test.ts new file mode 100644 index 000000000..c6ea4901e --- /dev/null +++ b/cli/tests/unit/link_test.ts @@ -0,0 +1,147 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { perms: { read: true, write: true } }, + function linkSyncSuccess(): void { + const testDir = Deno.makeTempDirSync(); + const oldData = "Hardlink"; + const oldName = testDir + "/oldname"; + const newName = testDir + "/newname"; + Deno.writeFileSync(oldName, new TextEncoder().encode(oldData)); + // Create the hard link. + Deno.linkSync(oldName, newName); + // We should expect reading the same content. + const newData = new TextDecoder().decode(Deno.readFileSync(newName)); + assertEquals(oldData, newData); + // Writing to newname also affects oldname. + const newData2 = "Modified"; + Deno.writeFileSync(newName, new TextEncoder().encode(newData2)); + assertEquals( + newData2, + new TextDecoder().decode(Deno.readFileSync(oldName)) + ); + // Writing to oldname also affects newname. + const newData3 = "ModifiedAgain"; + Deno.writeFileSync(oldName, new TextEncoder().encode(newData3)); + assertEquals( + newData3, + new TextDecoder().decode(Deno.readFileSync(newName)) + ); + // Remove oldname. File still accessible through newname. + Deno.removeSync(oldName); + const newNameStat = Deno.statSync(newName); + assert(newNameStat.isFile); + assert(!newNameStat.isSymlink); // Not a symlink. + assertEquals( + newData3, + new TextDecoder().decode(Deno.readFileSync(newName)) + ); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function linkSyncExists(): void { + const testDir = Deno.makeTempDirSync(); + const oldName = testDir + "/oldname"; + const newName = testDir + "/newname"; + Deno.writeFileSync(oldName, new TextEncoder().encode("oldName")); + // newname is already created. + Deno.writeFileSync(newName, new TextEncoder().encode("newName")); + + let err; + try { + Deno.linkSync(oldName, newName); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.AlreadyExists); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function linkSyncNotFound(): void { + const testDir = Deno.makeTempDirSync(); + const oldName = testDir + "/oldname"; + const newName = testDir + "/newname"; + + let err; + try { + Deno.linkSync(oldName, newName); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { read: false, write: true } }, + function linkSyncReadPerm(): void { + let err; + try { + Deno.linkSync("oldbaddir", "newbaddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } +); + +unitTest( + { perms: { read: true, write: false } }, + function linkSyncWritePerm(): void { + let err; + try { + Deno.linkSync("oldbaddir", "newbaddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function linkSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const oldData = "Hardlink"; + const oldName = testDir + "/oldname"; + const newName = testDir + "/newname"; + Deno.writeFileSync(oldName, new TextEncoder().encode(oldData)); + // Create the hard link. + await Deno.link(oldName, newName); + // We should expect reading the same content. + const newData = new TextDecoder().decode(Deno.readFileSync(newName)); + assertEquals(oldData, newData); + // Writing to newname also affects oldname. + const newData2 = "Modified"; + Deno.writeFileSync(newName, new TextEncoder().encode(newData2)); + assertEquals( + newData2, + new TextDecoder().decode(Deno.readFileSync(oldName)) + ); + // Writing to oldname also affects newname. + const newData3 = "ModifiedAgain"; + Deno.writeFileSync(oldName, new TextEncoder().encode(newData3)); + assertEquals( + newData3, + new TextDecoder().decode(Deno.readFileSync(newName)) + ); + // Remove oldname. File still accessible through newname. + Deno.removeSync(oldName); + const newNameStat = Deno.statSync(newName); + assert(newNameStat.isFile); + assert(!newNameStat.isSymlink); // Not a symlink. + assertEquals( + newData3, + new TextDecoder().decode(Deno.readFileSync(newName)) + ); + } +); diff --git a/cli/tests/unit/make_temp_test.ts b/cli/tests/unit/make_temp_test.ts new file mode 100644 index 000000000..59fe8c5f5 --- /dev/null +++ b/cli/tests/unit/make_temp_test.ts @@ -0,0 +1,178 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest({ perms: { write: true } }, function makeTempDirSyncSuccess(): void { + const dir1 = Deno.makeTempDirSync({ prefix: "hello", suffix: "world" }); + const dir2 = Deno.makeTempDirSync({ prefix: "hello", suffix: "world" }); + // Check that both dirs are different. + assert(dir1 !== dir2); + for (const dir of [dir1, dir2]) { + // Check that the prefix and suffix are applied. + const lastPart = dir.replace(/^.*[\\\/]/, ""); + assert(lastPart.startsWith("hello")); + assert(lastPart.endsWith("world")); + } + // Check that the `dir` option works. + const dir3 = Deno.makeTempDirSync({ dir: dir1 }); + assert(dir3.startsWith(dir1)); + assert(/^[\\\/]/.test(dir3.slice(dir1.length))); + // Check that creating a temp dir inside a nonexisting directory fails. + let err; + try { + Deno.makeTempDirSync({ dir: "/baddir" }); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.NotFound); +}); + +unitTest( + { perms: { read: true, write: true } }, + function makeTempDirSyncMode(): void { + const path = Deno.makeTempDirSync(); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o700 & ~Deno.umask()); + } + } +); + +unitTest(function makeTempDirSyncPerm(): void { + // makeTempDirSync should require write permissions (for now). + let err; + try { + Deno.makeTempDirSync({ dir: "/baddir" }); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { write: true } }, + async function makeTempDirSuccess(): Promise<void> { + const dir1 = await Deno.makeTempDir({ prefix: "hello", suffix: "world" }); + const dir2 = await Deno.makeTempDir({ prefix: "hello", suffix: "world" }); + // Check that both dirs are different. + assert(dir1 !== dir2); + for (const dir of [dir1, dir2]) { + // Check that the prefix and suffix are applied. + const lastPart = dir.replace(/^.*[\\\/]/, ""); + assert(lastPart.startsWith("hello")); + assert(lastPart.endsWith("world")); + } + // Check that the `dir` option works. + const dir3 = await Deno.makeTempDir({ dir: dir1 }); + assert(dir3.startsWith(dir1)); + assert(/^[\\\/]/.test(dir3.slice(dir1.length))); + // Check that creating a temp dir inside a nonexisting directory fails. + let err; + try { + await Deno.makeTempDir({ dir: "/baddir" }); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function makeTempDirMode(): Promise<void> { + const path = await Deno.makeTempDir(); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o700 & ~Deno.umask()); + } + } +); + +unitTest({ perms: { write: true } }, function makeTempFileSyncSuccess(): void { + const file1 = Deno.makeTempFileSync({ prefix: "hello", suffix: "world" }); + const file2 = Deno.makeTempFileSync({ prefix: "hello", suffix: "world" }); + // Check that both dirs are different. + assert(file1 !== file2); + for (const dir of [file1, file2]) { + // Check that the prefix and suffix are applied. + const lastPart = dir.replace(/^.*[\\\/]/, ""); + assert(lastPart.startsWith("hello")); + assert(lastPart.endsWith("world")); + } + // Check that the `dir` option works. + const dir = Deno.makeTempDirSync({ prefix: "tempdir" }); + const file3 = Deno.makeTempFileSync({ dir }); + assert(file3.startsWith(dir)); + assert(/^[\\\/]/.test(file3.slice(dir.length))); + // Check that creating a temp file inside a nonexisting directory fails. + let err; + try { + Deno.makeTempFileSync({ dir: "/baddir" }); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.NotFound); +}); + +unitTest( + { perms: { read: true, write: true } }, + function makeTempFileSyncMode(): void { + const path = Deno.makeTempFileSync(); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o600 & ~Deno.umask()); + } + } +); + +unitTest(function makeTempFileSyncPerm(): void { + // makeTempFileSync should require write permissions (for now). + let err; + try { + Deno.makeTempFileSync({ dir: "/baddir" }); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { write: true } }, + async function makeTempFileSuccess(): Promise<void> { + const file1 = await Deno.makeTempFile({ prefix: "hello", suffix: "world" }); + const file2 = await Deno.makeTempFile({ prefix: "hello", suffix: "world" }); + // Check that both dirs are different. + assert(file1 !== file2); + for (const dir of [file1, file2]) { + // Check that the prefix and suffix are applied. + const lastPart = dir.replace(/^.*[\\\/]/, ""); + assert(lastPart.startsWith("hello")); + assert(lastPart.endsWith("world")); + } + // Check that the `dir` option works. + const dir = Deno.makeTempDirSync({ prefix: "tempdir" }); + const file3 = await Deno.makeTempFile({ dir }); + assert(file3.startsWith(dir)); + assert(/^[\\\/]/.test(file3.slice(dir.length))); + // Check that creating a temp file inside a nonexisting directory fails. + let err; + try { + await Deno.makeTempFile({ dir: "/baddir" }); + } catch (err_) { + err = err_; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function makeTempFileMode(): Promise<void> { + const path = await Deno.makeTempFile(); + const pathInfo = Deno.statSync(path); + if (Deno.build.os !== "windows") { + assertEquals(pathInfo.mode! & 0o777, 0o600 & ~Deno.umask()); + } + } +); diff --git a/cli/tests/unit/metrics_test.ts b/cli/tests/unit/metrics_test.ts new file mode 100644 index 000000000..9b7d83887 --- /dev/null +++ b/cli/tests/unit/metrics_test.ts @@ -0,0 +1,58 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +unitTest(async function metrics(): Promise<void> { + const m1 = Deno.metrics(); + assert(m1.opsDispatched > 0); + assert(m1.opsDispatchedSync > 0); + assert(m1.opsCompleted > 0); + assert(m1.opsCompletedSync > 0); + assert(m1.bytesSentControl > 0); + assert(m1.bytesSentData >= 0); + assert(m1.bytesReceived > 0); + + // Write to stdout to ensure a "data" message gets sent instead of just + // control messages. + const dataMsg = new Uint8Array([13, 13, 13]); // "\r\r\r", + await Deno.stdout.write(dataMsg); + + const m2 = Deno.metrics(); + assert(m2.opsDispatched > m1.opsDispatched); + assert(m2.opsDispatchedSync > m1.opsDispatchedSync); + assert(m2.opsDispatchedAsync > m1.opsDispatchedAsync); + assert(m2.opsCompleted > m1.opsCompleted); + assert(m2.opsCompletedSync > m1.opsCompletedSync); + assert(m2.opsCompletedAsync > m1.opsCompletedAsync); + assert(m2.bytesSentControl > m1.bytesSentControl); + assert(m2.bytesSentData >= m1.bytesSentData + dataMsg.byteLength); + assert(m2.bytesReceived > m1.bytesReceived); +}); + +unitTest( + { perms: { write: true } }, + function metricsUpdatedIfNoResponseSync(): void { + const filename = Deno.makeTempDirSync() + "/test.txt"; + + const data = new Uint8Array([41, 42, 43]); + Deno.writeFileSync(filename, data, { mode: 0o666 }); + + const metrics = Deno.metrics(); + assert(metrics.opsDispatched === metrics.opsCompleted); + assert(metrics.opsDispatchedSync === metrics.opsCompletedSync); + } +); + +unitTest( + { perms: { write: true } }, + async function metricsUpdatedIfNoResponseAsync(): Promise<void> { + const filename = Deno.makeTempDirSync() + "/test.txt"; + + const data = new Uint8Array([41, 42, 43]); + await Deno.writeFile(filename, data, { mode: 0o666 }); + + const metrics = Deno.metrics(); + assert(metrics.opsDispatched === metrics.opsCompleted); + assert(metrics.opsDispatchedSync === metrics.opsCompletedSync); + assert(metrics.opsDispatchedAsync === metrics.opsCompletedAsync); + } +); diff --git a/cli/tests/unit/mkdir_test.ts b/cli/tests/unit/mkdir_test.ts new file mode 100644 index 000000000..68755ef4d --- /dev/null +++ b/cli/tests/unit/mkdir_test.ts @@ -0,0 +1,206 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals, assertThrows } from "./test_util.ts"; + +function assertDirectory(path: string, mode?: number): void { + const info = Deno.lstatSync(path); + assert(info.isDirectory); + if (Deno.build.os !== "windows" && mode !== undefined) { + assertEquals(info.mode! & 0o777, mode & ~Deno.umask()); + } +} + +unitTest( + { perms: { read: true, write: true } }, + function mkdirSyncSuccess(): void { + const path = Deno.makeTempDirSync() + "/dir"; + Deno.mkdirSync(path); + assertDirectory(path); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function mkdirSyncMode(): void { + const path = Deno.makeTempDirSync() + "/dir"; + Deno.mkdirSync(path, { mode: 0o737 }); + assertDirectory(path, 0o737); + } +); + +unitTest({ perms: { write: false } }, function mkdirSyncPerm(): void { + let err; + try { + Deno.mkdirSync("/baddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { read: true, write: true } }, + async function mkdirSuccess(): Promise<void> { + const path = Deno.makeTempDirSync() + "/dir"; + await Deno.mkdir(path); + assertDirectory(path); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function mkdirMode(): Promise<void> { + const path = Deno.makeTempDirSync() + "/dir"; + await Deno.mkdir(path, { mode: 0o737 }); + assertDirectory(path, 0o737); + } +); + +unitTest({ perms: { write: true } }, function mkdirErrSyncIfExists(): void { + let err; + try { + Deno.mkdirSync("."); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.AlreadyExists); +}); + +unitTest({ perms: { write: true } }, async function mkdirErrIfExists(): Promise< + void +> { + let err; + try { + await Deno.mkdir("."); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.AlreadyExists); +}); + +unitTest( + { perms: { read: true, write: true } }, + function mkdirSyncRecursive(): void { + const path = Deno.makeTempDirSync() + "/nested/directory"; + Deno.mkdirSync(path, { recursive: true }); + assertDirectory(path); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function mkdirRecursive(): Promise<void> { + const path = Deno.makeTempDirSync() + "/nested/directory"; + await Deno.mkdir(path, { recursive: true }); + assertDirectory(path); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function mkdirSyncRecursiveMode(): void { + const nested = Deno.makeTempDirSync() + "/nested"; + const path = nested + "/dir"; + Deno.mkdirSync(path, { mode: 0o737, recursive: true }); + assertDirectory(path, 0o737); + assertDirectory(nested, 0o737); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function mkdirRecursiveMode(): Promise<void> { + const nested = Deno.makeTempDirSync() + "/nested"; + const path = nested + "/dir"; + await Deno.mkdir(path, { mode: 0o737, recursive: true }); + assertDirectory(path, 0o737); + assertDirectory(nested, 0o737); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function mkdirSyncRecursiveIfExists(): void { + const path = Deno.makeTempDirSync() + "/dir"; + Deno.mkdirSync(path, { mode: 0o737 }); + Deno.mkdirSync(path, { recursive: true }); + Deno.mkdirSync(path, { recursive: true, mode: 0o731 }); + assertDirectory(path, 0o737); + if (Deno.build.os !== "windows") { + const pathLink = path + "Link"; + Deno.symlinkSync(path, pathLink); + Deno.mkdirSync(pathLink, { recursive: true }); + Deno.mkdirSync(pathLink, { recursive: true, mode: 0o731 }); + assertDirectory(path, 0o737); + } + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function mkdirRecursiveIfExists(): Promise<void> { + const path = Deno.makeTempDirSync() + "/dir"; + await Deno.mkdir(path, { mode: 0o737 }); + await Deno.mkdir(path, { recursive: true }); + await Deno.mkdir(path, { recursive: true, mode: 0o731 }); + assertDirectory(path, 0o737); + if (Deno.build.os !== "windows") { + const pathLink = path + "Link"; + Deno.symlinkSync(path, pathLink); + await Deno.mkdir(pathLink, { recursive: true }); + await Deno.mkdir(pathLink, { recursive: true, mode: 0o731 }); + assertDirectory(path, 0o737); + } + } +); + +unitTest( + { perms: { read: true, write: true } }, + function mkdirSyncErrors(): void { + const testDir = Deno.makeTempDirSync(); + const emptydir = testDir + "/empty"; + const fulldir = testDir + "/dir"; + const file = fulldir + "/file"; + Deno.mkdirSync(emptydir); + Deno.mkdirSync(fulldir); + Deno.createSync(file).close(); + + assertThrows((): void => { + Deno.mkdirSync(emptydir, { recursive: false }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(fulldir, { recursive: false }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(file, { recursive: false }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(file, { recursive: true }); + }, Deno.errors.AlreadyExists); + + if (Deno.build.os !== "windows") { + const fileLink = testDir + "/fileLink"; + const dirLink = testDir + "/dirLink"; + const danglingLink = testDir + "/danglingLink"; + Deno.symlinkSync(file, fileLink); + Deno.symlinkSync(emptydir, dirLink); + Deno.symlinkSync(testDir + "/nonexistent", danglingLink); + + assertThrows((): void => { + Deno.mkdirSync(dirLink, { recursive: false }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(fileLink, { recursive: false }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(fileLink, { recursive: true }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(danglingLink, { recursive: false }); + }, Deno.errors.AlreadyExists); + assertThrows((): void => { + Deno.mkdirSync(danglingLink, { recursive: true }); + }, Deno.errors.AlreadyExists); + } + } +); diff --git a/cli/tests/unit/net_test.ts b/cli/tests/unit/net_test.ts new file mode 100644 index 000000000..9e9a1e5e8 --- /dev/null +++ b/cli/tests/unit/net_test.ts @@ -0,0 +1,527 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + createResolvable, +} from "./test_util.ts"; + +unitTest({ perms: { net: true } }, function netTcpListenClose(): void { + const listener = Deno.listen({ hostname: "127.0.0.1", port: 3500 }); + assert(listener.addr.transport === "tcp"); + assertEquals(listener.addr.hostname, "127.0.0.1"); + assertEquals(listener.addr.port, 3500); + listener.close(); +}); + +unitTest( + { + perms: { net: true }, + // TODO: + ignore: Deno.build.os === "windows", + }, + function netUdpListenClose(): void { + const socket = Deno.listenDatagram({ + hostname: "127.0.0.1", + port: 3500, + transport: "udp", + }); + assert(socket.addr.transport === "udp"); + assertEquals(socket.addr.hostname, "127.0.0.1"); + assertEquals(socket.addr.port, 3500); + socket.close(); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + function netUnixListenClose(): void { + const filePath = Deno.makeTempFileSync(); + const socket = Deno.listen({ + path: filePath, + transport: "unix", + }); + assert(socket.addr.transport === "unix"); + assertEquals(socket.addr.path, filePath); + socket.close(); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + function netUnixPacketListenClose(): void { + const filePath = Deno.makeTempFileSync(); + const socket = Deno.listenDatagram({ + path: filePath, + transport: "unixpacket", + }); + assert(socket.addr.transport === "unixpacket"); + assertEquals(socket.addr.path, filePath); + socket.close(); + } +); + +unitTest( + { + perms: { net: true }, + }, + async function netTcpCloseWhileAccept(): Promise<void> { + const listener = Deno.listen({ port: 4501 }); + const p = listener.accept(); + listener.close(); + let err; + try { + await p; + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Error); + assertEquals(err.message, "Listener has been closed"); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + async function netUnixCloseWhileAccept(): Promise<void> { + const filePath = await Deno.makeTempFile(); + const listener = Deno.listen({ + path: filePath, + transport: "unix", + }); + const p = listener.accept(); + listener.close(); + let err; + try { + await p; + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Error); + assertEquals(err.message, "Listener has been closed"); + } +); + +unitTest( + { perms: { net: true } }, + async function netTcpConcurrentAccept(): Promise<void> { + const listener = Deno.listen({ port: 4502 }); + let acceptErrCount = 0; + const checkErr = (e: Error): void => { + if (e.message === "Listener has been closed") { + assertEquals(acceptErrCount, 1); + } else if (e.message === "Another accept task is ongoing") { + acceptErrCount++; + } else { + throw new Error("Unexpected error message"); + } + }; + const p = listener.accept().catch(checkErr); + const p1 = listener.accept().catch(checkErr); + await Promise.race([p, p1]); + listener.close(); + await Promise.all([p, p1]); + assertEquals(acceptErrCount, 1); + } +); + +// TODO(jsouto): Enable when tokio updates mio to v0.7! +unitTest( + { ignore: true, perms: { read: true, write: true } }, + async function netUnixConcurrentAccept(): Promise<void> { + const filePath = await Deno.makeTempFile(); + const listener = Deno.listen({ transport: "unix", path: filePath }); + let acceptErrCount = 0; + const checkErr = (e: Error): void => { + if (e.message === "Listener has been closed") { + assertEquals(acceptErrCount, 1); + } else if (e.message === "Another accept task is ongoing") { + acceptErrCount++; + } else { + throw new Error("Unexpected error message"); + } + }; + const p = listener.accept().catch(checkErr); + const p1 = listener.accept().catch(checkErr); + await Promise.race([p, p1]); + listener.close(); + await [p, p1]; + assertEquals(acceptErrCount, 1); + } +); + +unitTest({ perms: { net: true } }, async function netTcpDialListen(): Promise< + void +> { + const listener = Deno.listen({ port: 3500 }); + listener.accept().then( + async (conn): Promise<void> => { + assert(conn.remoteAddr != null); + assert(conn.localAddr.transport === "tcp"); + assertEquals(conn.localAddr.hostname, "127.0.0.1"); + assertEquals(conn.localAddr.port, 3500); + await conn.write(new Uint8Array([1, 2, 3])); + conn.close(); + } + ); + + const conn = await Deno.connect({ hostname: "127.0.0.1", port: 3500 }); + assert(conn.remoteAddr.transport === "tcp"); + assertEquals(conn.remoteAddr.hostname, "127.0.0.1"); + assertEquals(conn.remoteAddr.port, 3500); + assert(conn.localAddr != null); + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assertEquals(3, readResult); + assertEquals(1, buf[0]); + assertEquals(2, buf[1]); + assertEquals(3, buf[2]); + assert(conn.rid > 0); + + assert(readResult !== null); + + const readResult2 = await conn.read(buf); + assertEquals(readResult2, null); + + listener.close(); + conn.close(); +}); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + async function netUnixDialListen(): Promise<void> { + const filePath = await Deno.makeTempFile(); + const listener = Deno.listen({ path: filePath, transport: "unix" }); + listener.accept().then( + async (conn): Promise<void> => { + assert(conn.remoteAddr != null); + assert(conn.localAddr.transport === "unix"); + assertEquals(conn.localAddr.path, filePath); + await conn.write(new Uint8Array([1, 2, 3])); + conn.close(); + } + ); + const conn = await Deno.connect({ path: filePath, transport: "unix" }); + assert(conn.remoteAddr.transport === "unix"); + assertEquals(conn.remoteAddr.path, filePath); + assert(conn.remoteAddr != null); + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assertEquals(3, readResult); + assertEquals(1, buf[0]); + assertEquals(2, buf[1]); + assertEquals(3, buf[2]); + assert(conn.rid > 0); + + assert(readResult !== null); + + const readResult2 = await conn.read(buf); + assertEquals(readResult2, null); + + listener.close(); + conn.close(); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { net: true } }, + async function netUdpSendReceive(): Promise<void> { + const alice = Deno.listenDatagram({ port: 3500, transport: "udp" }); + assert(alice.addr.transport === "udp"); + assertEquals(alice.addr.port, 3500); + assertEquals(alice.addr.hostname, "127.0.0.1"); + + const bob = Deno.listenDatagram({ port: 4501, transport: "udp" }); + assert(bob.addr.transport === "udp"); + assertEquals(bob.addr.port, 4501); + assertEquals(bob.addr.hostname, "127.0.0.1"); + + const sent = new Uint8Array([1, 2, 3]); + await alice.send(sent, bob.addr); + + const [recvd, remote] = await bob.receive(); + assert(remote.transport === "udp"); + assertEquals(remote.port, 3500); + assertEquals(recvd.length, 3); + assertEquals(1, recvd[0]); + assertEquals(2, recvd[1]); + assertEquals(3, recvd[2]); + alice.close(); + bob.close(); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + async function netUnixPacketSendReceive(): Promise<void> { + const filePath = await Deno.makeTempFile(); + const alice = Deno.listenDatagram({ + path: filePath, + transport: "unixpacket", + }); + assert(alice.addr.transport === "unixpacket"); + assertEquals(alice.addr.path, filePath); + + const bob = Deno.listenDatagram({ + path: filePath, + transport: "unixpacket", + }); + assert(bob.addr.transport === "unixpacket"); + assertEquals(bob.addr.path, filePath); + + const sent = new Uint8Array([1, 2, 3]); + await alice.send(sent, bob.addr); + + const [recvd, remote] = await bob.receive(); + assert(remote.transport === "unixpacket"); + assertEquals(remote.path, filePath); + assertEquals(recvd.length, 3); + assertEquals(1, recvd[0]); + assertEquals(2, recvd[1]); + assertEquals(3, recvd[2]); + alice.close(); + bob.close(); + } +); + +unitTest( + { perms: { net: true } }, + async function netTcpListenIteratorBreakClosesResource(): Promise<void> { + const promise = createResolvable(); + + async function iterate(listener: Deno.Listener): Promise<void> { + let i = 0; + + for await (const conn of listener) { + conn.close(); + i++; + + if (i > 1) { + break; + } + } + + promise.resolve(); + } + + const addr = { hostname: "127.0.0.1", port: 8888 }; + const listener = Deno.listen(addr); + iterate(listener); + + await new Promise((resolve: () => void, _) => { + setTimeout(resolve, 100); + }); + const conn1 = await Deno.connect(addr); + conn1.close(); + const conn2 = await Deno.connect(addr); + conn2.close(); + + await promise; + } +); + +unitTest( + { perms: { net: true } }, + async function netTcpListenCloseWhileIterating(): Promise<void> { + const listener = Deno.listen({ port: 8000 }); + const nextWhileClosing = listener[Symbol.asyncIterator]().next(); + listener.close(); + assertEquals(await nextWhileClosing, { value: undefined, done: true }); + + const nextAfterClosing = listener[Symbol.asyncIterator]().next(); + assertEquals(await nextAfterClosing, { value: undefined, done: true }); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { net: true } }, + async function netUdpListenCloseWhileIterating(): Promise<void> { + const socket = Deno.listenDatagram({ port: 8000, transport: "udp" }); + const nextWhileClosing = socket[Symbol.asyncIterator]().next(); + socket.close(); + assertEquals(await nextWhileClosing, { value: undefined, done: true }); + + const nextAfterClosing = socket[Symbol.asyncIterator]().next(); + assertEquals(await nextAfterClosing, { value: undefined, done: true }); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + async function netUnixListenCloseWhileIterating(): Promise<void> { + const filePath = Deno.makeTempFileSync(); + const socket = Deno.listen({ path: filePath, transport: "unix" }); + const nextWhileClosing = socket[Symbol.asyncIterator]().next(); + socket.close(); + assertEquals(await nextWhileClosing, { value: undefined, done: true }); + + const nextAfterClosing = socket[Symbol.asyncIterator]().next(); + assertEquals(await nextAfterClosing, { value: undefined, done: true }); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + async function netUnixPacketListenCloseWhileIterating(): Promise<void> { + const filePath = Deno.makeTempFileSync(); + const socket = Deno.listenDatagram({ + path: filePath, + transport: "unixpacket", + }); + const nextWhileClosing = socket[Symbol.asyncIterator]().next(); + socket.close(); + assertEquals(await nextWhileClosing, { value: undefined, done: true }); + + const nextAfterClosing = socket[Symbol.asyncIterator]().next(); + assertEquals(await nextAfterClosing, { value: undefined, done: true }); + } +); + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + perms: { net: true }, + }, + async function netListenAsyncIterator(): Promise<void> { + const addr = { hostname: "127.0.0.1", port: 3500 }; + const listener = Deno.listen(addr); + const runAsyncIterator = async (): Promise<void> => { + for await (const conn of listener) { + await conn.write(new Uint8Array([1, 2, 3])); + conn.close(); + } + }; + runAsyncIterator(); + const conn = await Deno.connect(addr); + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assertEquals(3, readResult); + assertEquals(1, buf[0]); + assertEquals(2, buf[1]); + assertEquals(3, buf[2]); + assert(conn.rid > 0); + + assert(readResult !== null); + + const readResult2 = await conn.read(buf); + assertEquals(readResult2, null); + + listener.close(); + conn.close(); + } +); + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + perms: { net: true }, + }, + async function netCloseWriteSuccess() { + const addr = { hostname: "127.0.0.1", port: 3500 }; + const listener = Deno.listen(addr); + const closeDeferred = createResolvable(); + listener.accept().then(async (conn) => { + await conn.write(new Uint8Array([1, 2, 3])); + await closeDeferred; + conn.close(); + }); + const conn = await Deno.connect(addr); + conn.closeWrite(); // closing write + const buf = new Uint8Array(1024); + // Check read not impacted + const readResult = await conn.read(buf); + assertEquals(3, readResult); + assertEquals(1, buf[0]); + assertEquals(2, buf[1]); + assertEquals(3, buf[2]); + // Check write should be closed + let err; + try { + await conn.write(new Uint8Array([1, 2, 3])); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.BrokenPipe); + closeDeferred.resolve(); + listener.close(); + conn.close(); + } +); + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + perms: { net: true }, + }, + async function netDoubleCloseWrite() { + const addr = { hostname: "127.0.0.1", port: 3500 }; + const listener = Deno.listen(addr); + const closeDeferred = createResolvable(); + listener.accept().then(async (conn) => { + await closeDeferred; + conn.close(); + }); + const conn = await Deno.connect(addr); + conn.closeWrite(); // closing write + let err; + try { + // Duplicated close should throw error + conn.closeWrite(); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof Deno.errors.NotConnected); + closeDeferred.resolve(); + listener.close(); + conn.close(); + } +); + +unitTest( + { + perms: { net: true }, + }, + async function netHangsOnClose() { + let acceptedConn: Deno.Conn; + const resolvable = createResolvable(); + + async function iteratorReq(listener: Deno.Listener): Promise<void> { + const p = new Uint8Array(10); + const conn = await listener.accept(); + acceptedConn = conn; + + try { + while (true) { + const nread = await conn.read(p); + if (nread === null) { + break; + } + await conn.write(new Uint8Array([1, 2, 3])); + } + } catch (err) { + assert(!!err); + assert(err instanceof Deno.errors.BadResource); + } + + resolvable.resolve(); + } + + const addr = { hostname: "127.0.0.1", port: 3500 }; + const listener = Deno.listen(addr); + iteratorReq(listener); + const conn = await Deno.connect(addr); + await conn.write(new Uint8Array([1, 2, 3, 4])); + const buf = new Uint8Array(10); + await conn.read(buf); + conn!.close(); + acceptedConn!.close(); + listener.close(); + await resolvable; + } +); diff --git a/cli/tests/unit/os_test.ts b/cli/tests/unit/os_test.ts new file mode 100644 index 000000000..e99002534 --- /dev/null +++ b/cli/tests/unit/os_test.ts @@ -0,0 +1,338 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + assertNotEquals, + assertThrows, + unitTest, +} from "./test_util.ts"; + +unitTest({ perms: { env: true } }, function envSuccess(): void { + Deno.env.set("TEST_VAR", "A"); + const env = Deno.env.toObject(); + Deno.env.set("TEST_VAR", "B"); + assertEquals(env["TEST_VAR"], "A"); + assertNotEquals(Deno.env.get("TEST_VAR"), env["TEST_VAR"]); +}); + +unitTest({ perms: { env: true } }, function envNotFound(): void { + const r = Deno.env.get("env_var_does_not_exist!"); + assertEquals(r, undefined); +}); + +unitTest(function envPermissionDenied1(): void { + let err; + try { + Deno.env.toObject(); + } catch (e) { + err = e; + } + assertNotEquals(err, undefined); + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest(function envPermissionDenied2(): void { + let err; + try { + Deno.env.get("PATH"); + } catch (e) { + err = e; + } + assertNotEquals(err, undefined); + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +// This test verifies that on Windows, environment variables are +// case-insensitive. Case normalization needs be done using the collation +// that Windows uses, rather than naively using String.toLowerCase(). +unitTest( + { + ignore: Deno.build.os !== "windows", + perms: { read: true, env: true, run: true }, + }, + async function envCaseInsensitive() { + // Utility function that runs a Deno subprocess with the environment + // specified in `inputEnv`. The subprocess reads the environment variables + // which are in the keys of `expectedEnv` and writes them to stdout as JSON. + // It is then verified that these match with the values of `expectedEnv`. + const checkChildEnv = async ( + inputEnv: Record<string, string>, + expectedEnv: Record<string, string> + ): Promise<void> => { + const src = ` + console.log( + ${JSON.stringify(Object.keys(expectedEnv))}.map(k => Deno.env.get(k)) + )`; + const proc = Deno.run({ + cmd: [Deno.execPath(), "eval", src], + env: inputEnv, + stdout: "piped", + }); + const status = await proc.status(); + assertEquals(status.success, true); + const expectedValues = Object.values(expectedEnv); + const actualValues = JSON.parse( + new TextDecoder().decode(await proc.output()) + ); + assertEquals(actualValues, expectedValues); + proc.close(); + }; + + assertEquals(Deno.env.get("path"), Deno.env.get("PATH")); + assertEquals(Deno.env.get("Path"), Deno.env.get("PATH")); + + // Check 'foo', 'Foo' and 'Foo' are case folded. + await checkChildEnv({ foo: "X" }, { foo: "X", Foo: "X", FOO: "X" }); + + // Check that 'µ' and 'Μ' are not case folded. + const lc1 = "µ"; + const uc1 = lc1.toUpperCase(); + assertNotEquals(lc1, uc1); + await checkChildEnv( + { [lc1]: "mu", [uc1]: "MU" }, + { [lc1]: "mu", [uc1]: "MU" } + ); + + // Check that 'dž' and 'DŽ' are folded, but 'Dž' is preserved. + const c2 = "Dž"; + const lc2 = c2.toLowerCase(); + const uc2 = c2.toUpperCase(); + assertNotEquals(c2, lc2); + assertNotEquals(c2, uc2); + await checkChildEnv( + { [c2]: "Dz", [lc2]: "dz" }, + { [c2]: "Dz", [lc2]: "dz", [uc2]: "dz" } + ); + await checkChildEnv( + { [c2]: "Dz", [uc2]: "DZ" }, + { [c2]: "Dz", [uc2]: "DZ", [lc2]: "DZ" } + ); + } +); + +unitTest(function osPid(): void { + assert(Deno.pid > 0); +}); + +unitTest({ perms: { env: true } }, function getDir(): void { + type supportOS = "darwin" | "windows" | "linux"; + + interface Runtime { + os: supportOS; + shouldHaveValue: boolean; + } + + interface Scenes { + kind: Deno.DirKind; + runtime: Runtime[]; + } + + const scenes: Scenes[] = [ + { + kind: "config", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "cache", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "executable", + runtime: [ + { os: "darwin", shouldHaveValue: false }, + { os: "windows", shouldHaveValue: false }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "data", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "data_local", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "audio", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "desktop", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "document", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "download", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "font", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: false }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "picture", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "public", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "template", + runtime: [ + { os: "darwin", shouldHaveValue: false }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + { + kind: "tmp", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: true }, + ], + }, + { + kind: "video", + runtime: [ + { os: "darwin", shouldHaveValue: true }, + { os: "windows", shouldHaveValue: true }, + { os: "linux", shouldHaveValue: false }, + ], + }, + ]; + + for (const s of scenes) { + for (const r of s.runtime) { + if (Deno.build.os !== r.os) continue; + if (r.shouldHaveValue) { + const d = Deno.dir(s.kind); + assert(d); + assert(d.length > 0); + } + } + } +}); + +unitTest(function getDirWithoutPermission(): void { + assertThrows( + () => Deno.dir("home"), + Deno.errors.PermissionDenied, + `run again with the --allow-env flag` + ); +}); + +unitTest({ perms: { read: true } }, function execPath(): void { + assertNotEquals(Deno.execPath(), ""); +}); + +unitTest({ perms: { read: false } }, function execPathPerm(): void { + let caughtError = false; + try { + Deno.execPath(); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } + assert(caughtError); +}); + +unitTest({ perms: { env: true } }, function loadavgSuccess(): void { + const load = Deno.loadavg(); + assertEquals(load.length, 3); +}); + +unitTest({ perms: { env: false } }, function loadavgPerm(): void { + let caughtError = false; + try { + Deno.loadavg(); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } + assert(caughtError); +}); + +unitTest({ perms: { env: true } }, function hostnameDir(): void { + assertNotEquals(Deno.hostname(), ""); +}); + +unitTest({ perms: { env: false } }, function hostnamePerm(): void { + let caughtError = false; + try { + Deno.hostname(); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } + assert(caughtError); +}); + +unitTest({ perms: { env: true } }, function releaseDir(): void { + assertNotEquals(Deno.osRelease(), ""); +}); + +unitTest({ perms: { env: false } }, function releasePerm(): void { + let caughtError = false; + try { + Deno.osRelease(); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } + assert(caughtError); +}); diff --git a/cli/tests/unit/performance_test.ts b/cli/tests/unit/performance_test.ts new file mode 100644 index 000000000..89b7cad8b --- /dev/null +++ b/cli/tests/unit/performance_test.ts @@ -0,0 +1,15 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, createResolvable } from "./test_util.ts"; + +unitTest({ perms: { hrtime: false } }, async function performanceNow(): Promise< + void +> { + const resolvable = createResolvable(); + const start = performance.now(); + setTimeout((): void => { + const end = performance.now(); + assert(end - start >= 10); + resolvable.resolve(); + }, 10); + await resolvable; +}); diff --git a/cli/tests/unit/permissions_test.ts b/cli/tests/unit/permissions_test.ts new file mode 100644 index 000000000..7528768a1 --- /dev/null +++ b/cli/tests/unit/permissions_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +unitTest(async function permissionInvalidName(): Promise<void> { + let thrown = false; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await Deno.permissions.query({ name: "foo" as any }); + } catch (e) { + thrown = true; + assert(e instanceof Error); + } finally { + assert(thrown); + } +}); + +unitTest(async function permissionNetInvalidUrl(): Promise<void> { + let thrown = false; + try { + await Deno.permissions.query({ name: "net", url: ":" }); + } catch (e) { + thrown = true; + assert(e instanceof URIError); + } finally { + assert(thrown); + } +}); diff --git a/cli/tests/unit/process_test.ts b/cli/tests/unit/process_test.ts new file mode 100644 index 000000000..1ea6f95b7 --- /dev/null +++ b/cli/tests/unit/process_test.ts @@ -0,0 +1,386 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + assertStrContains, + unitTest, +} from "./test_util.ts"; +const { + kill, + run, + readFile, + open, + makeTempDir, + writeFile, + writeFileSync, +} = Deno; + +unitTest(function runPermissions(): void { + let caughtError = false; + try { + run({ cmd: ["python", "-c", "print('hello world')"] }); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { run: true } }, async function runSuccess(): Promise<void> { + const p = run({ + cmd: ["python", "-c", "print('hello world')"], + stdout: "piped", + stderr: "null", + }); + const status = await p.status(); + assertEquals(status.success, true); + assertEquals(status.code, 0); + assertEquals(status.signal, undefined); + p.stdout!.close(); + p.close(); +}); + +unitTest( + { perms: { run: true } }, + async function runCommandFailedWithCode(): Promise<void> { + const p = run({ + cmd: ["python", "-c", "import sys;sys.exit(41 + 1)"], + }); + const status = await p.status(); + assertEquals(status.success, false); + assertEquals(status.code, 42); + assertEquals(status.signal, undefined); + p.close(); + } +); + +unitTest( + { + // No signals on windows. + ignore: Deno.build.os === "windows", + perms: { run: true }, + }, + async function runCommandFailedWithSignal(): Promise<void> { + const p = run({ + cmd: ["python", "-c", "import os;os.kill(os.getpid(), 9)"], + }); + const status = await p.status(); + assertEquals(status.success, false); + assertEquals(status.code, undefined); + assertEquals(status.signal, 9); + p.close(); + } +); + +unitTest({ perms: { run: true } }, function runNotFound(): void { + let error; + try { + run({ cmd: ["this file hopefully doesn't exist"] }); + } catch (e) { + error = e; + } + assert(error !== undefined); + assert(error instanceof Deno.errors.NotFound); +}); + +unitTest( + { perms: { write: true, run: true } }, + async function runWithCwdIsAsync(): Promise<void> { + const enc = new TextEncoder(); + const cwd = await makeTempDir({ prefix: "deno_command_test" }); + + const exitCodeFile = "deno_was_here"; + const pyProgramFile = "poll_exit.py"; + const pyProgram = ` +from sys import exit +from time import sleep + +while True: + try: + with open("${exitCodeFile}", "r") as f: + line = f.readline() + code = int(line) + exit(code) + except IOError: + # Retry if we got here before deno wrote the file. + sleep(0.01) + pass +`; + + writeFileSync(`${cwd}/${pyProgramFile}.py`, enc.encode(pyProgram)); + const p = run({ + cwd, + cmd: ["python", `${pyProgramFile}.py`], + }); + + // Write the expected exit code *after* starting python. + // This is how we verify that `run()` is actually asynchronous. + const code = 84; + writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`)); + + const status = await p.status(); + assertEquals(status.success, false); + assertEquals(status.code, code); + assertEquals(status.signal, undefined); + p.close(); + } +); + +unitTest({ perms: { run: true } }, async function runStdinPiped(): Promise< + void +> { + const p = run({ + cmd: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"], + stdin: "piped", + }); + assert(p.stdin); + assert(!p.stdout); + assert(!p.stderr); + + const msg = new TextEncoder().encode("hello"); + const n = await p.stdin.write(msg); + assertEquals(n, msg.byteLength); + + p.stdin!.close(); + + const status = await p.status(); + assertEquals(status.success, true); + assertEquals(status.code, 0); + assertEquals(status.signal, undefined); + p.close(); +}); + +unitTest({ perms: { run: true } }, async function runStdoutPiped(): Promise< + void +> { + const p = run({ + cmd: ["python", "-c", "import sys; sys.stdout.write('hello')"], + stdout: "piped", + }); + assert(!p.stdin); + assert(!p.stderr); + + const data = new Uint8Array(10); + let r = await p.stdout!.read(data); + if (r === null) { + throw new Error("p.stdout.read(...) should not be null"); + } + assertEquals(r, 5); + const s = new TextDecoder().decode(data.subarray(0, r)); + assertEquals(s, "hello"); + r = await p.stdout!.read(data); + assertEquals(r, null); + p.stdout!.close(); + + const status = await p.status(); + assertEquals(status.success, true); + assertEquals(status.code, 0); + assertEquals(status.signal, undefined); + p.close(); +}); + +unitTest({ perms: { run: true } }, async function runStderrPiped(): Promise< + void +> { + const p = run({ + cmd: ["python", "-c", "import sys; sys.stderr.write('hello')"], + stderr: "piped", + }); + assert(!p.stdin); + assert(!p.stdout); + + const data = new Uint8Array(10); + let r = await p.stderr!.read(data); + if (r === null) { + throw new Error("p.stderr.read should not return null here"); + } + assertEquals(r, 5); + const s = new TextDecoder().decode(data.subarray(0, r)); + assertEquals(s, "hello"); + r = await p.stderr!.read(data); + assertEquals(r, null); + p.stderr!.close(); + + const status = await p.status(); + assertEquals(status.success, true); + assertEquals(status.code, 0); + assertEquals(status.signal, undefined); + p.close(); +}); + +unitTest({ perms: { run: true } }, async function runOutput(): Promise<void> { + const p = run({ + cmd: ["python", "-c", "import sys; sys.stdout.write('hello')"], + stdout: "piped", + }); + const output = await p.output(); + const s = new TextDecoder().decode(output); + assertEquals(s, "hello"); + p.close(); +}); + +unitTest({ perms: { run: true } }, async function runStderrOutput(): Promise< + void +> { + const p = run({ + cmd: ["python", "-c", "import sys; sys.stderr.write('error')"], + stderr: "piped", + }); + const error = await p.stderrOutput(); + const s = new TextDecoder().decode(error); + assertEquals(s, "error"); + p.close(); +}); + +unitTest( + { perms: { run: true, write: true, read: true } }, + async function runRedirectStdoutStderr(): Promise<void> { + const tempDir = await makeTempDir(); + const fileName = tempDir + "/redirected_stdio.txt"; + const file = await open(fileName, { + create: true, + write: true, + }); + + const p = run({ + cmd: [ + "python", + "-c", + "import sys; sys.stderr.write('error\\n'); sys.stdout.write('output\\n');", + ], + stdout: file.rid, + stderr: file.rid, + }); + + await p.status(); + p.close(); + file.close(); + + const fileContents = await readFile(fileName); + const decoder = new TextDecoder(); + const text = decoder.decode(fileContents); + + assertStrContains(text, "error"); + assertStrContains(text, "output"); + } +); + +unitTest( + { perms: { run: true, write: true, read: true } }, + async function runRedirectStdin(): Promise<void> { + const tempDir = await makeTempDir(); + const fileName = tempDir + "/redirected_stdio.txt"; + const encoder = new TextEncoder(); + await writeFile(fileName, encoder.encode("hello")); + const file = await open(fileName); + + const p = run({ + cmd: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"], + stdin: file.rid, + }); + + const status = await p.status(); + assertEquals(status.code, 0); + p.close(); + file.close(); + } +); + +unitTest({ perms: { run: true } }, async function runEnv(): Promise<void> { + const p = run({ + cmd: [ + "python", + "-c", + "import os, sys; sys.stdout.write(os.environ.get('FOO', '') + os.environ.get('BAR', ''))", + ], + env: { + FOO: "0123", + BAR: "4567", + }, + stdout: "piped", + }); + const output = await p.output(); + const s = new TextDecoder().decode(output); + assertEquals(s, "01234567"); + p.close(); +}); + +unitTest({ perms: { run: true } }, async function runClose(): Promise<void> { + const p = run({ + cmd: [ + "python", + "-c", + "from time import sleep; import sys; sleep(10000); sys.stderr.write('error')", + ], + stderr: "piped", + }); + assert(!p.stdin); + assert(!p.stdout); + + p.close(); + + const data = new Uint8Array(10); + const r = await p.stderr!.read(data); + assertEquals(r, null); + p.stderr!.close(); +}); + +unitTest(function signalNumbers(): void { + if (Deno.build.os === "darwin") { + assertEquals(Deno.Signal.SIGSTOP, 17); + } else if (Deno.build.os === "linux") { + assertEquals(Deno.Signal.SIGSTOP, 19); + } +}); + +unitTest(function killPermissions(): void { + let caughtError = false; + try { + // Unlike the other test cases, we don't have permission to spawn a + // subprocess we can safely kill. Instead we send SIGCONT to the current + // process - assuming that Deno does not have a special handler set for it + // and will just continue even if a signal is erroneously sent. + kill(Deno.pid, Deno.Signal.SIGCONT); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { run: true } }, async function killSuccess(): Promise<void> { + const p = run({ + cmd: ["python", "-c", "from time import sleep; sleep(10000)"], + }); + + assertEquals(Deno.Signal.SIGINT, 2); + kill(p.pid, Deno.Signal.SIGINT); + const status = await p.status(); + + assertEquals(status.success, false); + // TODO(ry) On Linux, status.code is sometimes undefined and sometimes 1. + // The following assert is causing this test to be flaky. Investigate and + // re-enable when it can be made deterministic. + // assertEquals(status.code, 1); + // assertEquals(status.signal, Deno.Signal.SIGINT); + p.close(); +}); + +unitTest({ perms: { run: true } }, function killFailed(): void { + const p = run({ + cmd: ["python", "-c", "from time import sleep; sleep(10000)"], + }); + assert(!p.stdin); + assert(!p.stdout); + + let err; + try { + kill(p.pid, 12345); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof TypeError); + + p.close(); +}); diff --git a/cli/tests/unit/read_dir_test.ts b/cli/tests/unit/read_dir_test.ts new file mode 100644 index 000000000..79e2a1507 --- /dev/null +++ b/cli/tests/unit/read_dir_test.ts @@ -0,0 +1,82 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +function assertSameContent(files: Deno.DirEntry[]): void { + let counter = 0; + + for (const entry of files) { + if (entry.name === "subdir") { + assert(entry.isDirectory); + counter++; + } + } + + assertEquals(counter, 1); +} + +unitTest({ perms: { read: true } }, function readDirSyncSuccess(): void { + const files = [...Deno.readDirSync("cli/tests/")]; + assertSameContent(files); +}); + +unitTest({ perms: { read: false } }, function readDirSyncPerm(): void { + let caughtError = false; + try { + Deno.readDirSync("tests/"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function readDirSyncNotDir(): void { + let caughtError = false; + let src; + + try { + src = Deno.readDirSync("cli/tests/fixture.json"); + } catch (err) { + caughtError = true; + assert(err instanceof Error); + } + assert(caughtError); + assertEquals(src, undefined); +}); + +unitTest({ perms: { read: true } }, function readDirSyncNotFound(): void { + let caughtError = false; + let src; + + try { + src = Deno.readDirSync("bad_dir_name"); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.NotFound); + } + assert(caughtError); + assertEquals(src, undefined); +}); + +unitTest({ perms: { read: true } }, async function readDirSuccess(): Promise< + void +> { + const files = []; + for await (const dirEntry of Deno.readDir("cli/tests/")) { + files.push(dirEntry); + } + assertSameContent(files); +}); + +unitTest({ perms: { read: false } }, async function readDirPerm(): Promise< + void +> { + let caughtError = false; + try { + await Deno.readDir("tests/")[Symbol.asyncIterator]().next(); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); diff --git a/cli/tests/unit/read_file_test.ts b/cli/tests/unit/read_file_test.ts new file mode 100644 index 000000000..0d3cdf422 --- /dev/null +++ b/cli/tests/unit/read_file_test.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest({ perms: { read: true } }, function readFileSyncSuccess(): void { + const data = Deno.readFileSync("cli/tests/fixture.json"); + assert(data.byteLength > 0); + const decoder = new TextDecoder("utf-8"); + const json = decoder.decode(data); + const pkg = JSON.parse(json); + assertEquals(pkg.name, "deno"); +}); + +unitTest({ perms: { read: false } }, function readFileSyncPerm(): void { + let caughtError = false; + try { + Deno.readFileSync("cli/tests/fixture.json"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function readFileSyncNotFound(): void { + let caughtError = false; + let data; + try { + data = Deno.readFileSync("bad_filename"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + assert(data === undefined); +}); + +unitTest({ perms: { read: true } }, async function readFileSuccess(): Promise< + void +> { + const data = await Deno.readFile("cli/tests/fixture.json"); + assert(data.byteLength > 0); + const decoder = new TextDecoder("utf-8"); + const json = decoder.decode(data); + const pkg = JSON.parse(json); + assertEquals(pkg.name, "deno"); +}); + +unitTest({ perms: { read: false } }, async function readFilePerm(): Promise< + void +> { + let caughtError = false; + try { + await Deno.readFile("cli/tests/fixture.json"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function readFileSyncLoop(): void { + for (let i = 0; i < 256; i++) { + Deno.readFileSync("cli/tests/fixture.json"); + } +}); diff --git a/cli/tests/unit/read_link_test.ts b/cli/tests/unit/read_link_test.ts new file mode 100644 index 000000000..6821d8dc8 --- /dev/null +++ b/cli/tests/unit/read_link_test.ts @@ -0,0 +1,73 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { perms: { write: true, read: true } }, + function readLinkSyncSuccess(): void { + const testDir = Deno.makeTempDirSync(); + const target = testDir + "/target"; + const symlink = testDir + "/symln"; + Deno.mkdirSync(target); + // TODO Add test for Windows once symlink is implemented for Windows. + // See https://github.com/denoland/deno/issues/815. + if (Deno.build.os !== "windows") { + Deno.symlinkSync(target, symlink); + const targetPath = Deno.readLinkSync(symlink); + assertEquals(targetPath, target); + } + } +); + +unitTest({ perms: { read: false } }, function readLinkSyncPerm(): void { + let caughtError = false; + try { + Deno.readLinkSync("/symlink"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function readLinkSyncNotFound(): void { + let caughtError = false; + let data; + try { + data = Deno.readLinkSync("bad_filename"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + assertEquals(data, undefined); +}); + +unitTest( + { perms: { write: true, read: true } }, + async function readLinkSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const target = testDir + "/target"; + const symlink = testDir + "/symln"; + Deno.mkdirSync(target); + // TODO Add test for Windows once symlink is implemented for Windows. + // See https://github.com/denoland/deno/issues/815. + if (Deno.build.os !== "windows") { + Deno.symlinkSync(target, symlink); + const targetPath = await Deno.readLink(symlink); + assertEquals(targetPath, target); + } + } +); + +unitTest({ perms: { read: false } }, async function readLinkPerm(): Promise< + void +> { + let caughtError = false; + try { + await Deno.readLink("/symlink"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); diff --git a/cli/tests/unit/read_text_file_test.ts b/cli/tests/unit/read_text_file_test.ts new file mode 100644 index 000000000..3e7493e4a --- /dev/null +++ b/cli/tests/unit/read_text_file_test.ts @@ -0,0 +1,61 @@ +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest({ perms: { read: true } }, function readTextFileSyncSuccess(): void { + const data = Deno.readTextFileSync("cli/tests/fixture.json"); + assert(data.length > 0); + const pkg = JSON.parse(data); + assertEquals(pkg.name, "deno"); +}); + +unitTest({ perms: { read: false } }, function readTextFileSyncPerm(): void { + let caughtError = false; + try { + Deno.readTextFileSync("cli/tests/fixture.json"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function readTextFileSyncNotFound(): void { + let caughtError = false; + let data; + try { + data = Deno.readTextFileSync("bad_filename"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + assert(data === undefined); +}); + +unitTest( + { perms: { read: true } }, + async function readTextFileSuccess(): Promise<void> { + const data = await Deno.readTextFile("cli/tests/fixture.json"); + assert(data.length > 0); + const pkg = JSON.parse(data); + assertEquals(pkg.name, "deno"); + } +); + +unitTest({ perms: { read: false } }, async function readTextFilePerm(): Promise< + void +> { + let caughtError = false; + try { + await Deno.readTextFile("cli/tests/fixture.json"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function readTextFileSyncLoop(): void { + for (let i = 0; i < 256; i++) { + Deno.readTextFileSync("cli/tests/fixture.json"); + } +}); diff --git a/cli/tests/unit/real_path_test.ts b/cli/tests/unit/real_path_test.ts new file mode 100644 index 000000000..c88955270 --- /dev/null +++ b/cli/tests/unit/real_path_test.ts @@ -0,0 +1,108 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +unitTest({ perms: { read: true } }, function realPathSyncSuccess(): void { + const incompletePath = "cli/tests/fixture.json"; + const realPath = Deno.realPathSync(incompletePath); + if (Deno.build.os !== "windows") { + assert(realPath.startsWith("/")); + } else { + assert(/^[A-Z]/.test(realPath)); + } + assert(realPath.endsWith(incompletePath)); +}); + +unitTest( + { + ignore: Deno.build.os === "windows", + perms: { read: true, write: true }, + }, + function realPathSyncSymlink(): void { + const testDir = Deno.makeTempDirSync(); + const target = testDir + "/target"; + const symlink = testDir + "/symln"; + Deno.mkdirSync(target); + Deno.symlinkSync(target, symlink); + const targetPath = Deno.realPathSync(symlink); + assert(targetPath.startsWith("/")); + assert(targetPath.endsWith("/target")); + } +); + +unitTest({ perms: { read: false } }, function realPathSyncPerm(): void { + let caughtError = false; + try { + Deno.realPathSync("some_file"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function realPathSyncNotFound(): void { + let caughtError = false; + try { + Deno.realPathSync("bad_filename"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, async function realPathSuccess(): Promise< + void +> { + const incompletePath = "cli/tests/fixture.json"; + const realPath = await Deno.realPath(incompletePath); + if (Deno.build.os !== "windows") { + assert(realPath.startsWith("/")); + } else { + assert(/^[A-Z]/.test(realPath)); + } + assert(realPath.endsWith(incompletePath)); +}); + +unitTest( + { + ignore: Deno.build.os === "windows", + perms: { read: true, write: true }, + }, + async function realPathSymlink(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const target = testDir + "/target"; + const symlink = testDir + "/symln"; + Deno.mkdirSync(target); + Deno.symlinkSync(target, symlink); + const targetPath = await Deno.realPath(symlink); + assert(targetPath.startsWith("/")); + assert(targetPath.endsWith("/target")); + } +); + +unitTest({ perms: { read: false } }, async function realPathPerm(): Promise< + void +> { + let caughtError = false; + try { + await Deno.realPath("some_file"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, async function realPathNotFound(): Promise< + void +> { + let caughtError = false; + try { + await Deno.realPath("bad_filename"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); +}); diff --git a/cli/tests/unit/remove_test.ts b/cli/tests/unit/remove_test.ts new file mode 100644 index 000000000..35e5c821e --- /dev/null +++ b/cli/tests/unit/remove_test.ts @@ -0,0 +1,506 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +// SYNC + +unitTest( + { perms: { write: true, read: true } }, + function removeSyncDirSuccess(): void { + // REMOVE EMPTY DIRECTORY + const path = Deno.makeTempDirSync() + "/subdir"; + Deno.mkdirSync(path); + const pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + Deno.removeSync(path); // remove + // We then check again after remove + let err; + try { + Deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + function removeSyncFileSuccess(): void { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); // check exist first + Deno.removeSync(filename); // remove + // We then check again after remove + let err; + try { + Deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + function removeSyncFail(): void { + // NON-EMPTY DIRECTORY + const path = Deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + Deno.mkdirSync(path, { recursive: true }); + Deno.mkdirSync(subPath); + const pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + const subPathInfo = Deno.statSync(subPath); + assert(subPathInfo.isDirectory); // check exist first + let err; + try { + // Should not be able to recursively remove + Deno.removeSync(path); + } catch (e) { + err = e; + } + // TODO(ry) Is Other really the error we should get here? What would Go do? + assert(err instanceof Error); + // NON-EXISTENT DIRECTORY/FILE + try { + // Non-existent + Deno.removeSync("/baddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + function removeSyncDanglingSymlinkSuccess(): void { + const danglingSymlinkPath = Deno.makeTempDirSync() + "/dangling_symlink"; + if (Deno.build.os === "windows") { + Deno.symlinkSync("unexistent_file", danglingSymlinkPath, { + type: "file", + }); + } else { + Deno.symlinkSync("unexistent_file", danglingSymlinkPath); + } + const pathInfo = Deno.lstatSync(danglingSymlinkPath); + assert(pathInfo.isSymlink); + Deno.removeSync(danglingSymlinkPath); + let err; + try { + Deno.lstatSync(danglingSymlinkPath); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + function removeSyncValidSymlinkSuccess(): void { + const encoder = new TextEncoder(); + const data = encoder.encode("Test"); + const tempDir = Deno.makeTempDirSync(); + const filePath = tempDir + "/test.txt"; + const validSymlinkPath = tempDir + "/valid_symlink"; + Deno.writeFileSync(filePath, data, { mode: 0o666 }); + if (Deno.build.os === "windows") { + Deno.symlinkSync(filePath, validSymlinkPath, { type: "file" }); + } else { + Deno.symlinkSync(filePath, validSymlinkPath); + } + const symlinkPathInfo = Deno.statSync(validSymlinkPath); + assert(symlinkPathInfo.isFile); + Deno.removeSync(validSymlinkPath); + let err; + try { + Deno.statSync(validSymlinkPath); + } catch (e) { + err = e; + } + Deno.removeSync(filePath); + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest({ perms: { write: false } }, function removeSyncPerm(): void { + let err; + try { + Deno.removeSync("/baddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { write: true, read: true } }, + function removeAllSyncDirSuccess(): void { + // REMOVE EMPTY DIRECTORY + let path = Deno.makeTempDirSync() + "/dir/subdir"; + Deno.mkdirSync(path, { recursive: true }); + let pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + Deno.removeSync(path, { recursive: true }); // remove + // We then check again after remove + let err; + try { + Deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assert(err instanceof Deno.errors.NotFound); + + // REMOVE NON-EMPTY DIRECTORY + path = Deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + Deno.mkdirSync(path, { recursive: true }); + Deno.mkdirSync(subPath); + pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + const subPathInfo = Deno.statSync(subPath); + assert(subPathInfo.isDirectory); // check exist first + Deno.removeSync(path, { recursive: true }); // remove + // We then check parent directory again after remove + try { + Deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + function removeAllSyncFileSuccess(): void { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); // check exist first + Deno.removeSync(filename, { recursive: true }); // remove + // We then check again after remove + let err; + try { + Deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest({ perms: { write: true } }, function removeAllSyncFail(): void { + // NON-EXISTENT DIRECTORY/FILE + let err; + try { + // Non-existent + Deno.removeSync("/baddir", { recursive: true }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); +}); + +unitTest({ perms: { write: false } }, function removeAllSyncPerm(): void { + let err; + try { + Deno.removeSync("/baddir", { recursive: true }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +// ASYNC + +unitTest( + { perms: { write: true, read: true } }, + async function removeDirSuccess(): Promise<void> { + // REMOVE EMPTY DIRECTORY + const path = Deno.makeTempDirSync() + "/dir/subdir"; + Deno.mkdirSync(path, { recursive: true }); + const pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + await Deno.remove(path); // remove + // We then check again after remove + let err; + try { + Deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + async function removeFileSuccess(): Promise<void> { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); // check exist first + await Deno.remove(filename); // remove + // We then check again after remove + let err; + try { + Deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + async function removeFail(): Promise<void> { + // NON-EMPTY DIRECTORY + const path = Deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + Deno.mkdirSync(path, { recursive: true }); + Deno.mkdirSync(subPath); + const pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + const subPathInfo = Deno.statSync(subPath); + assert(subPathInfo.isDirectory); // check exist first + let err; + try { + // Should not be able to recursively remove + await Deno.remove(path); + } catch (e) { + err = e; + } + assert(err instanceof Error); + // NON-EXISTENT DIRECTORY/FILE + try { + // Non-existent + await Deno.remove("/baddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + async function removeDanglingSymlinkSuccess(): Promise<void> { + const danglingSymlinkPath = Deno.makeTempDirSync() + "/dangling_symlink"; + if (Deno.build.os === "windows") { + Deno.symlinkSync("unexistent_file", danglingSymlinkPath, { + type: "file", + }); + } else { + Deno.symlinkSync("unexistent_file", danglingSymlinkPath); + } + const pathInfo = Deno.lstatSync(danglingSymlinkPath); + assert(pathInfo.isSymlink); + await Deno.remove(danglingSymlinkPath); + let err; + try { + Deno.lstatSync(danglingSymlinkPath); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + async function removeValidSymlinkSuccess(): Promise<void> { + const encoder = new TextEncoder(); + const data = encoder.encode("Test"); + const tempDir = Deno.makeTempDirSync(); + const filePath = tempDir + "/test.txt"; + const validSymlinkPath = tempDir + "/valid_symlink"; + Deno.writeFileSync(filePath, data, { mode: 0o666 }); + if (Deno.build.os === "windows") { + Deno.symlinkSync(filePath, validSymlinkPath, { type: "file" }); + } else { + Deno.symlinkSync(filePath, validSymlinkPath); + } + const symlinkPathInfo = Deno.statSync(validSymlinkPath); + assert(symlinkPathInfo.isFile); + await Deno.remove(validSymlinkPath); + let err; + try { + Deno.statSync(validSymlinkPath); + } catch (e) { + err = e; + } + Deno.removeSync(filePath); + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest({ perms: { write: false } }, async function removePerm(): Promise< + void +> { + let err; + try { + await Deno.remove("/baddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { write: true, read: true } }, + async function removeAllDirSuccess(): Promise<void> { + // REMOVE EMPTY DIRECTORY + let path = Deno.makeTempDirSync() + "/dir/subdir"; + Deno.mkdirSync(path, { recursive: true }); + let pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + await Deno.remove(path, { recursive: true }); // remove + // We then check again after remove + let err; + try { + Deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assert(err instanceof Deno.errors.NotFound); + + // REMOVE NON-EMPTY DIRECTORY + path = Deno.makeTempDirSync() + "/dir/subdir"; + const subPath = path + "/subsubdir"; + Deno.mkdirSync(path, { recursive: true }); + Deno.mkdirSync(subPath); + pathInfo = Deno.statSync(path); + assert(pathInfo.isDirectory); // check exist first + const subPathInfo = Deno.statSync(subPath); + assert(subPathInfo.isDirectory); // check exist first + await Deno.remove(path, { recursive: true }); // remove + // We then check parent directory again after remove + try { + Deno.statSync(path); + } catch (e) { + err = e; + } + // Directory is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest( + { perms: { write: true, read: true } }, + async function removeAllFileSuccess(): Promise<void> { + // REMOVE FILE + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const fileInfo = Deno.statSync(filename); + assert(fileInfo.isFile); // check exist first + await Deno.remove(filename, { recursive: true }); // remove + // We then check again after remove + let err; + try { + Deno.statSync(filename); + } catch (e) { + err = e; + } + // File is gone + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest({ perms: { write: true } }, async function removeAllFail(): Promise< + void +> { + // NON-EXISTENT DIRECTORY/FILE + let err; + try { + // Non-existent + await Deno.remove("/baddir", { recursive: true }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); +}); + +unitTest({ perms: { write: false } }, async function removeAllPerm(): Promise< + void +> { + let err; + try { + await Deno.remove("/baddir", { recursive: true }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +if (Deno.build.os === "windows") { + unitTest( + { perms: { run: true, write: true, read: true } }, + async function removeFileSymlink(): Promise<void> { + const symlink = Deno.run({ + cmd: ["cmd", "/c", "mklink", "file_link", "bar"], + stdout: "null", + }); + + assert(await symlink.status()); + symlink.close(); + await Deno.remove("file_link"); + let err; + try { + await Deno.lstat("file_link"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } + ); + + unitTest( + { perms: { run: true, write: true, read: true } }, + async function removeDirSymlink(): Promise<void> { + const symlink = Deno.run({ + cmd: ["cmd", "/c", "mklink", "/d", "dir_link", "bar"], + stdout: "null", + }); + + assert(await symlink.status()); + symlink.close(); + + await Deno.remove("dir_link"); + let err; + try { + await Deno.lstat("dir_link"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } + ); +} diff --git a/cli/tests/unit/rename_test.ts b/cli/tests/unit/rename_test.ts new file mode 100644 index 000000000..b4047f906 --- /dev/null +++ b/cli/tests/unit/rename_test.ts @@ -0,0 +1,216 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals, assertThrows } from "./test_util.ts"; + +function assertMissing(path: string): void { + let caughtErr = false; + let info; + try { + info = Deno.lstatSync(path); + } catch (e) { + caughtErr = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtErr); + assertEquals(info, undefined); +} + +function assertFile(path: string): void { + const info = Deno.lstatSync(path); + assert(info.isFile); +} + +function assertDirectory(path: string, mode?: number): void { + const info = Deno.lstatSync(path); + assert(info.isDirectory); + if (Deno.build.os !== "windows" && mode !== undefined) { + assertEquals(info.mode! & 0o777, mode & ~Deno.umask()); + } +} + +unitTest( + { perms: { read: true, write: true } }, + function renameSyncSuccess(): void { + const testDir = Deno.makeTempDirSync(); + const oldpath = testDir + "/oldpath"; + const newpath = testDir + "/newpath"; + Deno.mkdirSync(oldpath); + Deno.renameSync(oldpath, newpath); + assertDirectory(newpath); + assertMissing(oldpath); + } +); + +unitTest( + { perms: { read: false, write: true } }, + function renameSyncReadPerm(): void { + let err; + try { + const oldpath = "/oldbaddir"; + const newpath = "/newbaddir"; + Deno.renameSync(oldpath, newpath); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } +); + +unitTest( + { perms: { read: true, write: false } }, + function renameSyncWritePerm(): void { + let err; + try { + const oldpath = "/oldbaddir"; + const newpath = "/newbaddir"; + Deno.renameSync(oldpath, newpath); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function renameSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const oldpath = testDir + "/oldpath"; + const newpath = testDir + "/newpath"; + Deno.mkdirSync(oldpath); + await Deno.rename(oldpath, newpath); + assertDirectory(newpath); + assertMissing(oldpath); + } +); + +function readFileString(filename: string): string { + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + return dec.decode(dataRead); +} + +function writeFileString(filename: string, s: string): void { + const enc = new TextEncoder(); + const data = enc.encode(s); + Deno.writeFileSync(filename, data, { mode: 0o666 }); +} + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + function renameSyncErrorsUnix(): void { + const testDir = Deno.makeTempDirSync(); + const oldfile = testDir + "/oldfile"; + const olddir = testDir + "/olddir"; + const emptydir = testDir + "/empty"; + const fulldir = testDir + "/dir"; + const file = fulldir + "/file"; + writeFileString(oldfile, "Hello"); + Deno.mkdirSync(olddir); + Deno.mkdirSync(emptydir); + Deno.mkdirSync(fulldir); + writeFileString(file, "world"); + + assertThrows( + (): void => { + Deno.renameSync(oldfile, emptydir); + }, + Error, + "Is a directory" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, fulldir); + }, + Error, + "Directory not empty" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, file); + }, + Error, + "Not a directory" + ); + + const fileLink = testDir + "/fileLink"; + const dirLink = testDir + "/dirLink"; + const danglingLink = testDir + "/danglingLink"; + Deno.symlinkSync(file, fileLink); + Deno.symlinkSync(emptydir, dirLink); + Deno.symlinkSync(testDir + "/nonexistent", danglingLink); + + assertThrows( + (): void => { + Deno.renameSync(olddir, fileLink); + }, + Error, + "Not a directory" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, dirLink); + }, + Error, + "Not a directory" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, danglingLink); + }, + Error, + "Not a directory" + ); + + // should succeed on Unix + Deno.renameSync(olddir, emptydir); + Deno.renameSync(oldfile, dirLink); + Deno.renameSync(dirLink, danglingLink); + assertFile(danglingLink); + assertEquals("Hello", readFileString(danglingLink)); + } +); + +unitTest( + { ignore: Deno.build.os !== "windows", perms: { read: true, write: true } }, + function renameSyncErrorsWin(): void { + const testDir = Deno.makeTempDirSync(); + const oldfile = testDir + "/oldfile"; + const olddir = testDir + "/olddir"; + const emptydir = testDir + "/empty"; + const fulldir = testDir + "/dir"; + const file = fulldir + "/file"; + writeFileString(oldfile, "Hello"); + Deno.mkdirSync(olddir); + Deno.mkdirSync(emptydir); + Deno.mkdirSync(fulldir); + writeFileString(file, "world"); + + assertThrows( + (): void => { + Deno.renameSync(oldfile, emptydir); + }, + Deno.errors.PermissionDenied, + "Access is denied" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, fulldir); + }, + Deno.errors.PermissionDenied, + "Access is denied" + ); + assertThrows( + (): void => { + Deno.renameSync(olddir, emptydir); + }, + Deno.errors.PermissionDenied, + "Access is denied" + ); + + // should succeed on Windows + Deno.renameSync(olddir, file); + assertDirectory(file); + } +); diff --git a/cli/tests/unit/request_test.ts b/cli/tests/unit/request_test.ts new file mode 100644 index 000000000..8a276c5e7 --- /dev/null +++ b/cli/tests/unit/request_test.ts @@ -0,0 +1,49 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest(function fromInit(): void { + const req = new Request("https://example.com", { + body: "ahoyhoy", + method: "POST", + headers: { + "test-header": "value", + }, + }); + + // @ts-ignore + assertEquals("ahoyhoy", req._bodySource); + assertEquals(req.url, "https://example.com"); + assertEquals(req.headers.get("test-header"), "value"); +}); + +unitTest(function fromRequest(): void { + const r = new Request("https://example.com"); + // @ts-ignore + r._bodySource = "ahoyhoy"; + r.headers.set("test-header", "value"); + + const req = new Request(r); + + // @ts-ignore + assertEquals(req._bodySource, r._bodySource); + assertEquals(req.url, r.url); + assertEquals(req.headers.get("test-header"), r.headers.get("test-header")); +}); + +unitTest(async function cloneRequestBodyStream(): Promise<void> { + // hack to get a stream + const stream = new Request("", { body: "a test body" }).body; + const r1 = new Request("https://example.com", { + body: stream, + }); + + const r2 = r1.clone(); + + const b1 = await r1.text(); + const b2 = await r2.text(); + + assertEquals(b1, b2); + + // @ts-ignore + assert(r1._bodySource !== r2._bodySource); +}); diff --git a/cli/tests/unit/resources_test.ts b/cli/tests/unit/resources_test.ts new file mode 100644 index 000000000..385905a6e --- /dev/null +++ b/cli/tests/unit/resources_test.ts @@ -0,0 +1,61 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals, assert } from "./test_util.ts"; + +unitTest(function resourcesCloseBadArgs(): void { + let err; + try { + Deno.close((null as unknown) as number); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.InvalidData); +}); + +unitTest(function resourcesStdio(): void { + const res = Deno.resources(); + + assertEquals(res[0], "stdin"); + assertEquals(res[1], "stdout"); + assertEquals(res[2], "stderr"); +}); + +unitTest({ perms: { net: true } }, async function resourcesNet(): Promise< + void +> { + const listener = Deno.listen({ port: 4501 }); + const dialerConn = await Deno.connect({ port: 4501 }); + const listenerConn = await listener.accept(); + + const res = Deno.resources(); + assertEquals( + Object.values(res).filter((r): boolean => r === "tcpListener").length, + 1 + ); + const tcpStreams = Object.values(res).filter( + (r): boolean => r === "tcpStream" + ); + assert(tcpStreams.length >= 2); + + listenerConn.close(); + dialerConn.close(); + listener.close(); +}); + +unitTest({ perms: { read: true } }, async function resourcesFile(): Promise< + void +> { + const resourcesBefore = Deno.resources(); + const f = await Deno.open("cli/tests/hello.txt"); + const resourcesAfter = Deno.resources(); + f.close(); + + // check that exactly one new resource (file) was added + assertEquals( + Object.keys(resourcesAfter).length, + Object.keys(resourcesBefore).length + 1 + ); + const newRid = +Object.keys(resourcesAfter).find((rid): boolean => { + return !resourcesBefore.hasOwnProperty(rid); + })!; + assertEquals(resourcesAfter[newRid], "fsFile"); +}); diff --git a/cli/tests/unit/signal_test.ts b/cli/tests/unit/signal_test.ts new file mode 100644 index 000000000..2f117f8d1 --- /dev/null +++ b/cli/tests/unit/signal_test.ts @@ -0,0 +1,195 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertThrows, + createResolvable, +} from "./test_util.ts"; + +function defer(n: number): Promise<void> { + return new Promise((resolve: () => void, _) => { + setTimeout(resolve, n); + }); +} + +unitTest( + { ignore: Deno.build.os !== "windows" }, + function signalsNotImplemented(): void { + assertThrows( + () => { + Deno.signal(1); + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.alarm(); // for SIGALRM + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.child(); // for SIGCHLD + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.hungup(); // for SIGHUP + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.interrupt(); // for SIGINT + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.io(); // for SIGIO + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.pipe(); // for SIGPIPE + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.quit(); // for SIGQUIT + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.terminate(); // for SIGTERM + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.userDefined1(); // for SIGUSR1 + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.userDefined2(); // for SIGURS2 + }, + Error, + "not implemented" + ); + assertThrows( + () => { + Deno.signals.windowChange(); // for SIGWINCH + }, + Error, + "not implemented" + ); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { run: true, net: true } }, + async function signalStreamTest(): Promise<void> { + const resolvable = createResolvable(); + // This prevents the program from exiting. + const t = setInterval(() => {}, 1000); + + let c = 0; + const sig = Deno.signal(Deno.Signal.SIGUSR1); + setTimeout(async () => { + await defer(20); + for (const _ of Array(3)) { + // Sends SIGUSR1 3 times. + Deno.kill(Deno.pid, Deno.Signal.SIGUSR1); + await defer(20); + } + sig.dispose(); + resolvable.resolve(); + }); + + for await (const _ of sig) { + c += 1; + } + + assertEquals(c, 3); + + clearInterval(t); + await resolvable; + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { run: true } }, + async function signalPromiseTest(): Promise<void> { + const resolvable = createResolvable(); + // This prevents the program from exiting. + const t = setInterval(() => {}, 1000); + + const sig = Deno.signal(Deno.Signal.SIGUSR1); + setTimeout(() => { + Deno.kill(Deno.pid, Deno.Signal.SIGUSR1); + resolvable.resolve(); + }, 20); + await sig; + sig.dispose(); + + clearInterval(t); + await resolvable; + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { run: true } }, + function signalShorthandsTest(): void { + let s: Deno.SignalStream; + s = Deno.signals.alarm(); // for SIGALRM + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.child(); // for SIGCHLD + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.hungup(); // for SIGHUP + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.interrupt(); // for SIGINT + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.io(); // for SIGIO + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.pipe(); // for SIGPIPE + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.quit(); // for SIGQUIT + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.terminate(); // for SIGTERM + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.userDefined1(); // for SIGUSR1 + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.userDefined2(); // for SIGURS2 + assert(s instanceof Deno.SignalStream); + s.dispose(); + s = Deno.signals.windowChange(); // for SIGWINCH + assert(s instanceof Deno.SignalStream); + s.dispose(); + } +); diff --git a/cli/tests/unit/stat_test.ts b/cli/tests/unit/stat_test.ts new file mode 100644 index 000000000..7eaf73d58 --- /dev/null +++ b/cli/tests/unit/stat_test.ts @@ -0,0 +1,238 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { perms: { read: true, write: true } }, + function statSyncSuccess(): void { + const packageInfo = Deno.statSync("README.md"); + assert(packageInfo.isFile); + assert(!packageInfo.isSymlink); + + const modulesInfo = Deno.statSync("cli/tests/symlink_to_subdir"); + assert(modulesInfo.isDirectory); + assert(!modulesInfo.isSymlink); + + const testsInfo = Deno.statSync("cli/tests"); + assert(testsInfo.isDirectory); + assert(!testsInfo.isSymlink); + + const tempFile = Deno.makeTempFileSync(); + const tempInfo = Deno.statSync(tempFile); + const now = Date.now(); + assert(tempInfo.atime !== null && now - tempInfo.atime.valueOf() < 1000); + assert(tempInfo.mtime !== null && now - tempInfo.mtime.valueOf() < 1000); + assert( + tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000 + ); + } +); + +unitTest({ perms: { read: false } }, function statSyncPerm(): void { + let caughtError = false; + try { + Deno.statSync("README.md"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function statSyncNotFound(): void { + let caughtError = false; + let badInfo; + + try { + badInfo = Deno.statSync("bad_file_name"); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.NotFound); + } + + assert(caughtError); + assertEquals(badInfo, undefined); +}); + +unitTest({ perms: { read: true } }, function lstatSyncSuccess(): void { + const packageInfo = Deno.lstatSync("README.md"); + assert(packageInfo.isFile); + assert(!packageInfo.isSymlink); + + const modulesInfo = Deno.lstatSync("cli/tests/symlink_to_subdir"); + assert(!modulesInfo.isDirectory); + assert(modulesInfo.isSymlink); + + const coreInfo = Deno.lstatSync("core"); + assert(coreInfo.isDirectory); + assert(!coreInfo.isSymlink); +}); + +unitTest({ perms: { read: false } }, function lstatSyncPerm(): void { + let caughtError = false; + try { + Deno.lstatSync("README.md"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, function lstatSyncNotFound(): void { + let caughtError = false; + let badInfo; + + try { + badInfo = Deno.lstatSync("bad_file_name"); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.NotFound); + } + + assert(caughtError); + assertEquals(badInfo, undefined); +}); + +unitTest( + { perms: { read: true, write: true } }, + async function statSuccess(): Promise<void> { + const packageInfo = await Deno.stat("README.md"); + assert(packageInfo.isFile); + assert(!packageInfo.isSymlink); + + const modulesInfo = await Deno.stat("cli/tests/symlink_to_subdir"); + assert(modulesInfo.isDirectory); + assert(!modulesInfo.isSymlink); + + const testsInfo = await Deno.stat("cli/tests"); + assert(testsInfo.isDirectory); + assert(!testsInfo.isSymlink); + + const tempFile = await Deno.makeTempFile(); + const tempInfo = await Deno.stat(tempFile); + const now = Date.now(); + assert(tempInfo.atime !== null && now - tempInfo.atime.valueOf() < 1000); + assert(tempInfo.mtime !== null && now - tempInfo.mtime.valueOf() < 1000); + + assert( + tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000 + ); + } +); + +unitTest({ perms: { read: false } }, async function statPerm(): Promise<void> { + let caughtError = false; + try { + await Deno.stat("README.md"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, async function statNotFound(): Promise< + void +> { + let caughtError = false; + let badInfo; + + try { + badInfo = await Deno.stat("bad_file_name"); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.NotFound); + } + + assert(caughtError); + assertEquals(badInfo, undefined); +}); + +unitTest({ perms: { read: true } }, async function lstatSuccess(): Promise< + void +> { + const packageInfo = await Deno.lstat("README.md"); + assert(packageInfo.isFile); + assert(!packageInfo.isSymlink); + + const modulesInfo = await Deno.lstat("cli/tests/symlink_to_subdir"); + assert(!modulesInfo.isDirectory); + assert(modulesInfo.isSymlink); + + const coreInfo = await Deno.lstat("core"); + assert(coreInfo.isDirectory); + assert(!coreInfo.isSymlink); +}); + +unitTest({ perms: { read: false } }, async function lstatPerm(): Promise<void> { + let caughtError = false; + try { + await Deno.lstat("README.md"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest({ perms: { read: true } }, async function lstatNotFound(): Promise< + void +> { + let caughtError = false; + let badInfo; + + try { + badInfo = await Deno.lstat("bad_file_name"); + } catch (err) { + caughtError = true; + assert(err instanceof Deno.errors.NotFound); + } + + assert(caughtError); + assertEquals(badInfo, undefined); +}); + +unitTest( + { ignore: Deno.build.os !== "windows", perms: { read: true, write: true } }, + function statNoUnixFields(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + const s = Deno.statSync(filename); + assert(s.dev === null); + assert(s.ino === null); + assert(s.mode === null); + assert(s.nlink === null); + assert(s.uid === null); + assert(s.gid === null); + assert(s.rdev === null); + assert(s.blksize === null); + assert(s.blocks === null); + } +); + +unitTest( + { ignore: Deno.build.os === "windows", perms: { read: true, write: true } }, + function statUnixFields(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const tempDir = Deno.makeTempDirSync(); + const filename = tempDir + "/test.txt"; + const filename2 = tempDir + "/test2.txt"; + Deno.writeFileSync(filename, data, { mode: 0o666 }); + // Create a link + Deno.linkSync(filename, filename2); + const s = Deno.statSync(filename); + assert(s.dev !== null); + assert(s.ino !== null); + assertEquals(s.mode! & 0o666, 0o666); + assertEquals(s.nlink, 2); + assert(s.uid !== null); + assert(s.gid !== null); + assert(s.rdev !== null); + assert(s.blksize !== null); + assert(s.blocks !== null); + } +); diff --git a/cli/tests/unit/streams_piping_test.ts b/cli/tests/unit/streams_piping_test.ts new file mode 100644 index 000000000..a947b3821 --- /dev/null +++ b/cli/tests/unit/streams_piping_test.ts @@ -0,0 +1,131 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; +import { assertThrowsAsync } from "../../../std/testing/asserts.ts"; + +unitTest(function streamPipeLocks() { + const rs = new ReadableStream(); + const ws = new WritableStream(); + + assertEquals(rs.locked, false); + assertEquals(ws.locked, false); + + rs.pipeTo(ws); + + assert(rs.locked); + assert(ws.locked); +}); + +unitTest(async function streamPipeFinishUnlocks() { + const rs = new ReadableStream({ + start(controller: ReadableStreamDefaultController): void { + controller.close(); + }, + }); + const ws = new WritableStream(); + + await rs.pipeTo(ws); + assertEquals(rs.locked, false); + assertEquals(ws.locked, false); +}); + +unitTest(async function streamPipeReadableStreamLocked() { + const rs = new ReadableStream(); + const ws = new WritableStream(); + + rs.getReader(); + + await assertThrowsAsync(async () => { + await rs.pipeTo(ws); + }, TypeError); +}); + +unitTest(async function streamPipeReadableStreamLocked() { + const rs = new ReadableStream(); + const ws = new WritableStream(); + + ws.getWriter(); + + await assertThrowsAsync(async () => { + await rs.pipeTo(ws); + }, TypeError); +}); + +unitTest(async function streamPipeLotsOfChunks() { + const CHUNKS = 10; + + const rs = new ReadableStream<number>({ + start(c: ReadableStreamDefaultController): void { + for (let i = 0; i < CHUNKS; ++i) { + c.enqueue(i); + } + c.close(); + }, + }); + + const written: Array<string | number> = []; + const ws = new WritableStream( + { + write(chunk: number): void { + written.push(chunk); + }, + close(): void { + written.push("closed"); + }, + }, + new CountQueuingStrategy({ highWaterMark: CHUNKS }) + ); + + await rs.pipeTo(ws); + const targetValues = []; + for (let i = 0; i < CHUNKS; ++i) { + targetValues.push(i); + } + targetValues.push("closed"); + + assertEquals(written, targetValues, "the correct values must be written"); + + // Ensure both readable and writable are closed by the time the pipe finishes. + await Promise.all([rs.getReader().closed, ws.getWriter().closed]); +}); + +for (const preventAbort of [true, false]) { + unitTest(function undefinedRejectionFromPull() { + const rs = new ReadableStream({ + pull(): Promise<void> { + return Promise.reject(undefined); + }, + }); + + return rs.pipeTo(new WritableStream(), { preventAbort }).then( + () => { + throw new Error("pipeTo promise should be rejected"); + }, + (value) => + assertEquals(value, undefined, "rejection value should be undefined") + ); + }); +} + +for (const preventCancel of [true, false]) { + unitTest(function undefinedRejectionWithPreventCancel() { + const rs = new ReadableStream({ + pull(controller: ReadableStreamDefaultController<number>): void { + controller.enqueue(0); + }, + }); + + const ws = new WritableStream({ + write(): Promise<void> { + return Promise.reject(undefined); + }, + }); + + return rs.pipeTo(ws, { preventCancel }).then( + () => { + throw new Error("pipeTo promise should be rejected"); + }, + (value) => + assertEquals(value, undefined, "rejection value should be undefined") + ); + }); +} diff --git a/cli/tests/unit/streams_transform_test.ts b/cli/tests/unit/streams_transform_test.ts new file mode 100644 index 000000000..f3ec148ae --- /dev/null +++ b/cli/tests/unit/streams_transform_test.ts @@ -0,0 +1,562 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertNotEquals, + assertThrows, +} from "./test_util.ts"; + +function delay(seconds: number): Promise<void> { + return new Promise<void>((resolve) => { + setTimeout(() => { + resolve(); + }, seconds); + }); +} + +function readableStreamToArray<R>( + readable: { getReader(): ReadableStreamDefaultReader<R> }, + reader?: ReadableStreamDefaultReader<R> +): Promise<R[]> { + if (reader === undefined) { + reader = readable.getReader(); + } + + const chunks: R[] = []; + + return pump(); + + function pump(): Promise<R[]> { + return reader!.read().then((result) => { + if (result.done) { + return chunks; + } + + chunks.push(result.value); + return pump(); + }); + } +} + +unitTest(function transformStreamConstructedWithTransformFunction() { + new TransformStream({ transform(): void {} }); +}); + +unitTest(function transformStreamConstructedNoTransform() { + new TransformStream(); + new TransformStream({}); +}); + +unitTest(function transformStreamIntstancesHaveProperProperties() { + const ts = new TransformStream({ transform(): void {} }); + const proto = Object.getPrototypeOf(ts); + + const writableStream = Object.getOwnPropertyDescriptor(proto, "writable"); + assert(writableStream !== undefined, "it has a writable property"); + assert(!writableStream.enumerable, "writable should be non-enumerable"); + assertEquals( + typeof writableStream.get, + "function", + "writable should have a getter" + ); + assertEquals( + writableStream.set, + undefined, + "writable should not have a setter" + ); + assert(writableStream.configurable, "writable should be configurable"); + assert( + ts.writable instanceof WritableStream, + "writable is an instance of WritableStream" + ); + assert( + WritableStream.prototype.getWriter.call(ts.writable), + "writable should pass WritableStream brand check" + ); + + const readableStream = Object.getOwnPropertyDescriptor(proto, "readable"); + assert(readableStream !== undefined, "it has a readable property"); + assert(!readableStream.enumerable, "readable should be non-enumerable"); + assertEquals( + typeof readableStream.get, + "function", + "readable should have a getter" + ); + assertEquals( + readableStream.set, + undefined, + "readable should not have a setter" + ); + assert(readableStream.configurable, "readable should be configurable"); + assert( + ts.readable instanceof ReadableStream, + "readable is an instance of ReadableStream" + ); + assertNotEquals( + ReadableStream.prototype.getReader.call(ts.readable), + undefined, + "readable should pass ReadableStream brand check" + ); +}); + +unitTest(function transformStreamWritableStartsAsWritable() { + const ts = new TransformStream({ transform(): void {} }); + + const writer = ts.writable.getWriter(); + assertEquals(writer.desiredSize, 1, "writer.desiredSize should be 1"); +}); + +unitTest(async function transformStreamReadableCanReadOutOfWritable() { + const ts = new TransformStream(); + + const writer = ts.writable.getWriter(); + writer.write("a"); + assertEquals( + writer.desiredSize, + 0, + "writer.desiredSize should be 0 after write()" + ); + + const result = await ts.readable.getReader().read(); + assertEquals( + result.value, + "a", + "result from reading the readable is the same as was written to writable" + ); + assert(!result.done, "stream should not be done"); + + await delay(0); + assert(writer.desiredSize === 1, "desiredSize should be 1 again"); +}); + +unitTest(async function transformStreamCanReadWhatIsWritten() { + let c: TransformStreamDefaultController; + const ts = new TransformStream({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(chunk: string): void { + c.enqueue(chunk.toUpperCase()); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const result = await ts.readable.getReader().read(); + assertEquals( + result.value, + "A", + "result from reading the readable is the transformation of what was written to writable" + ); + assert(!result.done, "stream should not be done"); +}); + +unitTest(async function transformStreamCanReadBothChunks() { + let c: TransformStreamDefaultController; + const ts = new TransformStream({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(chunk: string): void { + c.enqueue(chunk.toUpperCase()); + c.enqueue(chunk.toUpperCase()); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const reader = ts.readable.getReader(); + + const result1 = await reader.read(); + assertEquals( + result1.value, + "A", + "the first chunk read is the transformation of the single chunk written" + ); + assert(!result1.done, "stream should not be done"); + + const result2 = await reader.read(); + assertEquals( + result2.value, + "A", + "the second chunk read is also the transformation of the single chunk written" + ); + assert(!result2.done, "stream should not be done"); +}); + +unitTest(async function transformStreamCanReadWhatIsWritten() { + let c: TransformStreamDefaultController; + const ts = new TransformStream({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(chunk: string): Promise<void> { + return delay(0).then(() => c.enqueue(chunk.toUpperCase())); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const result = await ts.readable.getReader().read(); + assertEquals( + result.value, + "A", + "result from reading the readable is the transformation of what was written to writable" + ); + assert(!result.done, "stream should not be done"); +}); + +unitTest(async function transformStreamAsyncReadMultipleChunks() { + let doSecondEnqueue: () => void; + let returnFromTransform: () => void; + const ts = new TransformStream({ + transform( + chunk: string, + controller: TransformStreamDefaultController + ): Promise<void> { + delay(0).then(() => controller.enqueue(chunk.toUpperCase())); + doSecondEnqueue = (): void => controller.enqueue(chunk.toUpperCase()); + return new Promise((resolve) => { + returnFromTransform = resolve; + }); + }, + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const result1 = await reader.read(); + assertEquals( + result1.value, + "A", + "the first chunk read is the transformation of the single chunk written" + ); + assert(!result1.done, "stream should not be done"); + doSecondEnqueue!(); + + const result2 = await reader.read(); + assertEquals( + result2.value, + "A", + "the second chunk read is also the transformation of the single chunk written" + ); + assert(!result2.done, "stream should not be done"); + returnFromTransform!(); +}); + +unitTest(function transformStreamClosingWriteClosesRead() { + const ts = new TransformStream({ transform(): void {} }); + + const writer = ts.writable.getWriter(); + writer.close(); + + return Promise.all([writer.closed, ts.readable.getReader().closed]).then( + undefined + ); +}); + +unitTest(async function transformStreamCloseWaitAwaitsTransforms() { + let transformResolve: () => void; + const transformPromise = new Promise<void>((resolve) => { + transformResolve = resolve; + }); + const ts = new TransformStream( + { + transform(): Promise<void> { + return transformPromise; + }, + }, + undefined, + { highWaterMark: 1 } + ); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + let rsClosed = false; + ts.readable.getReader().closed.then(() => { + rsClosed = true; + }); + + await delay(0); + assertEquals(rsClosed, false, "readable is not closed after a tick"); + transformResolve!(); + + await writer.closed; + // TODO: Is this expectation correct? + assertEquals(rsClosed, true, "readable is closed at that point"); +}); + +unitTest(async function transformStreamCloseWriteAfterSyncEnqueues() { + let c: TransformStreamDefaultController<string>; + const ts = new TransformStream<string, string>({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(): Promise<void> { + c.enqueue("x"); + c.enqueue("y"); + return delay(0); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + await writer.closed; + const chunks = await readableChunks; + assertEquals( + chunks, + ["x", "y"], + "both enqueued chunks can be read from the readable" + ); +}); + +unitTest(async function transformStreamWritableCloseAsyncAfterAsyncEnqueues() { + let c: TransformStreamDefaultController<string>; + const ts = new TransformStream<string, string>({ + start(controller: TransformStreamDefaultController<string>): void { + c = controller; + }, + transform(): Promise<void> { + return delay(0) + .then(() => c.enqueue("x")) + .then(() => c.enqueue("y")) + .then(() => delay(0)); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + await writer.closed; + const chunks = await readableChunks; + assertEquals( + chunks, + ["x", "y"], + "both enqueued chunks can be read from the readable" + ); +}); + +unitTest(async function transformStreamTransformerMethodsCalledAsMethods() { + let c: TransformStreamDefaultController<string>; + const transformer = { + suffix: "-suffix", + + start(controller: TransformStreamDefaultController<string>): void { + c = controller; + c.enqueue("start" + this.suffix); + }, + + transform(chunk: string): void { + c.enqueue(chunk + this.suffix); + }, + + flush(): void { + c.enqueue("flushed" + this.suffix); + }, + }; + const ts = new TransformStream(transformer); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + await writer.closed; + const chunks = await readableChunks; + assertEquals( + chunks, + ["start-suffix", "a-suffix", "flushed-suffix"], + "all enqueued chunks have suffixes" + ); +}); + +unitTest(async function transformStreamMethodsShouldNotBeAppliedOrCalled() { + function functionWithOverloads(): void {} + functionWithOverloads.apply = (): void => { + throw new Error("apply() should not be called"); + }; + functionWithOverloads.call = (): void => { + throw new Error("call() should not be called"); + }; + const ts = new TransformStream({ + start: functionWithOverloads, + transform: functionWithOverloads, + flush: functionWithOverloads, + }); + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + await readableStreamToArray(ts.readable); +}); + +unitTest(async function transformStreamCallTransformSync() { + let transformCalled = false; + const ts = new TransformStream( + { + transform(): void { + transformCalled = true; + }, + }, + undefined, + { highWaterMark: Infinity } + ); + // transform() is only called synchronously when there is no backpressure and + // all microtasks have run. + await delay(0); + const writePromise = ts.writable.getWriter().write(undefined); + assert(transformCalled, "transform() should have been called"); + await writePromise; +}); + +unitTest(function transformStreamCloseWriteCloesesReadWithNoChunks() { + const ts = new TransformStream({}, undefined, { highWaterMark: 0 }); + + const writer = ts.writable.getWriter(); + writer.close(); + + return Promise.all([writer.closed, ts.readable.getReader().closed]).then( + undefined + ); +}); + +unitTest(function transformStreamEnqueueThrowsAfterTerminate() { + new TransformStream({ + start(controller: TransformStreamDefaultController): void { + controller.terminate(); + assertThrows(() => { + controller.enqueue(undefined); + }, TypeError); + }, + }); +}); + +unitTest(function transformStreamEnqueueThrowsAfterReadableCancel() { + let controller: TransformStreamDefaultController; + const ts = new TransformStream({ + start(c: TransformStreamDefaultController): void { + controller = c; + }, + }); + const cancelPromise = ts.readable.cancel(); + assertThrows( + () => controller.enqueue(undefined), + TypeError, + undefined, + "enqueue should throw" + ); + return cancelPromise; +}); + +unitTest(function transformStreamSecondTerminateNoOp() { + new TransformStream({ + start(controller: TransformStreamDefaultController): void { + controller.terminate(); + controller.terminate(); + }, + }); +}); + +unitTest(async function transformStreamTerminateAfterReadableCancelIsNoop() { + let controller: TransformStreamDefaultController; + const ts = new TransformStream({ + start(c: TransformStreamDefaultController): void { + controller = c; + }, + }); + const cancelReason = { name: "cancelReason" }; + const cancelPromise = ts.readable.cancel(cancelReason); + controller!.terminate(); + await cancelPromise; + try { + await ts.writable.getWriter().closed; + } catch (e) { + assert(e === cancelReason); + return; + } + throw new Error("closed should have rejected"); +}); + +unitTest(async function transformStreamStartCalledOnce() { + let calls = 0; + new TransformStream({ + start(): void { + ++calls; + }, + }); + await delay(0); + assertEquals(calls, 1, "start() should have been called exactly once"); +}); + +unitTest(function transformStreamReadableTypeThrows() { + assertThrows( + // eslint-disable-next-line + () => new TransformStream({ readableType: "bytes" as any }), + RangeError, + undefined, + "constructor should throw" + ); +}); + +unitTest(function transformStreamWirtableTypeThrows() { + assertThrows( + // eslint-disable-next-line + () => new TransformStream({ writableType: "bytes" as any }), + RangeError, + undefined, + "constructor should throw" + ); +}); + +unitTest(function transformStreamSubclassable() { + class Subclass extends TransformStream { + extraFunction(): boolean { + return true; + } + } + assert( + Object.getPrototypeOf(Subclass.prototype) === TransformStream.prototype, + "Subclass.prototype's prototype should be TransformStream.prototype" + ); + assert( + Object.getPrototypeOf(Subclass) === TransformStream, + "Subclass's prototype should be TransformStream" + ); + const sub = new Subclass(); + assert( + sub instanceof TransformStream, + "Subclass object should be an instance of TransformStream" + ); + assert( + sub instanceof Subclass, + "Subclass object should be an instance of Subclass" + ); + const readableGetter = Object.getOwnPropertyDescriptor( + TransformStream.prototype, + "readable" + )!.get; + assert( + readableGetter!.call(sub) === sub.readable, + "Subclass object should pass brand check" + ); + assert( + sub.extraFunction(), + "extraFunction() should be present on Subclass object" + ); +}); diff --git a/cli/tests/unit/streams_writable_test.ts b/cli/tests/unit/streams_writable_test.ts new file mode 100644 index 000000000..54c1624af --- /dev/null +++ b/cli/tests/unit/streams_writable_test.ts @@ -0,0 +1,253 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals, assertThrows } from "./test_util.ts"; + +unitTest(function writableStreamDesiredSizeOnReleasedWriter() { + const ws = new WritableStream(); + const writer = ws.getWriter(); + writer.releaseLock(); + assertThrows(() => { + writer.desiredSize; + }, TypeError); +}); + +unitTest(function writableStreamDesiredSizeInitialValue() { + const ws = new WritableStream(); + const writer = ws.getWriter(); + assertEquals(writer.desiredSize, 1); +}); + +unitTest(async function writableStreamDesiredSizeClosed() { + const ws = new WritableStream(); + const writer = ws.getWriter(); + await writer.close(); + assertEquals(writer.desiredSize, 0); +}); + +unitTest(function writableStreamStartThrowsDesiredSizeNull() { + const ws = new WritableStream({ + start(c): void { + c.error(); + }, + }); + + const writer = ws.getWriter(); + assertEquals(writer.desiredSize, null, "desiredSize should be null"); +}); + +unitTest(function getWriterOnClosingStream() { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + ws.getWriter(); +}); + +unitTest(async function getWriterOnClosedStream() { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + await writer.close(); + writer.releaseLock(); + + ws.getWriter(); +}); + +unitTest(function getWriterOnAbortedStream() { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + writer.abort(); + writer.releaseLock(); + + ws.getWriter(); +}); + +unitTest(function getWriterOnErroredStream() { + const ws = new WritableStream({ + start(c): void { + c.error(); + }, + }); + + const writer = ws.getWriter(); + return writer.closed.then( + (v) => { + throw new Error(`writer.closed fulfilled unexpectedly with: ${v}`); + }, + () => { + writer.releaseLock(); + ws.getWriter(); + } + ); +}); + +unitTest(function closedAndReadyOnReleasedWriter() { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + writer.releaseLock(); + + return writer.closed.then( + (v) => { + throw new Error("writer.closed fulfilled unexpectedly with: " + v); + }, + (closedRejection) => { + assertEquals( + closedRejection.name, + "TypeError", + "closed promise should reject with a TypeError" + ); + return writer.ready.then( + (v) => { + throw new Error("writer.ready fulfilled unexpectedly with: " + v); + }, + (readyRejection) => + assertEquals( + readyRejection, + closedRejection, + "ready promise should reject with the same error" + ) + ); + } + ); +}); + +unitTest(function sinkMethodsCalledAsMethods() { + let thisObject: Sink | null = null; + // Calls to Sink methods after the first are implicitly ignored. Only the + // first value that is passed to the resolver is used. + class Sink { + start(): void { + assertEquals(this, thisObject, "start should be called as a method"); + } + + write(): void { + assertEquals(this, thisObject, "write should be called as a method"); + } + + close(): void { + assertEquals(this, thisObject, "close should be called as a method"); + } + + abort(): void { + assertEquals(this, thisObject, "abort should be called as a method"); + } + } + + const theSink = new Sink(); + thisObject = theSink; + const ws = new WritableStream(theSink); + + const writer = ws.getWriter(); + + writer.write("a"); + const closePromise = writer.close(); + + const ws2 = new WritableStream(theSink); + const writer2 = ws2.getWriter(); + const abortPromise = writer2.abort(); + + return Promise.all([closePromise, abortPromise]).then(undefined); +}); + +unitTest(function sizeShouldNotBeCalledAsMethod() { + const strategy = { + size(): number { + if (this !== undefined) { + throw new Error("size called as a method"); + } + return 1; + }, + }; + + const ws = new WritableStream({}, strategy); + const writer = ws.getWriter(); + return writer.write("a"); +}); + +unitTest(function redundantReleaseLockIsNoOp() { + const ws = new WritableStream(); + const writer1 = ws.getWriter(); + assertEquals( + undefined, + writer1.releaseLock(), + "releaseLock() should return undefined" + ); + const writer2 = ws.getWriter(); + assertEquals( + undefined, + writer1.releaseLock(), + "no-op releaseLock() should return undefined" + ); + // Calling releaseLock() on writer1 should not interfere with writer2. If it did, then the ready promise would be + // rejected. + return writer2.ready; +}); + +unitTest(function readyPromiseShouldFireBeforeReleaseLock() { + const events: string[] = []; + const ws = new WritableStream(); + const writer = ws.getWriter(); + return writer.ready.then(() => { + // Force the ready promise back to a pending state. + const writerPromise = writer.write("dummy"); + const readyPromise = writer.ready.catch(() => events.push("ready")); + const closedPromise = writer.closed.catch(() => events.push("closed")); + writer.releaseLock(); + return Promise.all([readyPromise, closedPromise]).then(() => { + assertEquals( + events, + ["ready", "closed"], + "ready promise should fire before closed promise" + ); + // Stop the writer promise hanging around after the test has finished. + return Promise.all([writerPromise, ws.abort()]).then(undefined); + }); + }); +}); + +unitTest(function subclassingWritableStream() { + class Subclass extends WritableStream { + extraFunction(): boolean { + return true; + } + } + assert( + Object.getPrototypeOf(Subclass.prototype) === WritableStream.prototype, + "Subclass.prototype's prototype should be WritableStream.prototype" + ); + assert( + Object.getPrototypeOf(Subclass) === WritableStream, + "Subclass's prototype should be WritableStream" + ); + const sub = new Subclass(); + assert( + sub instanceof WritableStream, + "Subclass object should be an instance of WritableStream" + ); + assert( + sub instanceof Subclass, + "Subclass object should be an instance of Subclass" + ); + const lockedGetter = Object.getOwnPropertyDescriptor( + WritableStream.prototype, + "locked" + )!.get!; + assert( + lockedGetter.call(sub) === sub.locked, + "Subclass object should pass brand check" + ); + assert( + sub.extraFunction(), + "extraFunction() should be present on Subclass object" + ); +}); + +unitTest(function lockedGetterShouldReturnTrue() { + const ws = new WritableStream(); + assert(!ws.locked, "stream should not be locked"); + ws.getWriter(); + assert(ws.locked, "stream should be locked"); +}); diff --git a/cli/tests/unit/symlink_test.ts b/cli/tests/unit/symlink_test.ts new file mode 100644 index 000000000..505a49bab --- /dev/null +++ b/cli/tests/unit/symlink_test.ts @@ -0,0 +1,44 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { perms: { read: true, write: true } }, + function symlinkSyncSuccess(): void { + const testDir = Deno.makeTempDirSync(); + const oldname = testDir + "/oldname"; + const newname = testDir + "/newname"; + Deno.mkdirSync(oldname); + // Just for now, until we implement symlink for Windows. + Deno.symlinkSync(oldname, newname); + const newNameInfoLStat = Deno.lstatSync(newname); + const newNameInfoStat = Deno.statSync(newname); + assert(newNameInfoLStat.isSymlink); + assert(newNameInfoStat.isDirectory); + } +); + +unitTest(function symlinkSyncPerm(): void { + let err; + try { + Deno.symlinkSync("oldbaddir", "newbaddir"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { read: true, write: true } }, + async function symlinkSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const oldname = testDir + "/oldname"; + const newname = testDir + "/newname"; + Deno.mkdirSync(oldname); + await Deno.symlink(oldname, newname); + const newNameInfoLStat = Deno.lstatSync(newname); + const newNameInfoStat = Deno.statSync(newname); + assert(newNameInfoLStat.isSymlink, "NOT SYMLINK"); + assert(newNameInfoStat.isDirectory, "NOT DIRECTORY"); + } +); diff --git a/cli/tests/unit/test_util.ts b/cli/tests/unit/test_util.ts new file mode 100644 index 000000000..1c5b6ff21 --- /dev/null +++ b/cli/tests/unit/test_util.ts @@ -0,0 +1,364 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +import { assert, assertEquals } from "../../../std/testing/asserts.ts"; +export { + assert, + assertThrows, + assertEquals, + assertMatch, + assertNotEquals, + assertStrictEq, + assertStrContains, + unreachable, + fail, +} from "../../../std/testing/asserts.ts"; +export { readLines } from "../../../std/io/bufio.ts"; +export { parse as parseArgs } from "../../../std/flags/mod.ts"; + +export interface Permissions { + read: boolean; + write: boolean; + net: boolean; + env: boolean; + run: boolean; + plugin: boolean; + hrtime: boolean; +} + +export function fmtPerms(perms: Permissions): string { + const p = Object.keys(perms) + .filter((e): boolean => perms[e as keyof Permissions] === true) + .map((key) => `--allow-${key}`); + + if (p.length) { + return p.join(" "); + } + + return "<no permissions>"; +} + +const isGranted = async (name: Deno.PermissionName): Promise<boolean> => + (await Deno.permissions.query({ name })).state === "granted"; + +export async function getProcessPermissions(): Promise<Permissions> { + return { + run: await isGranted("run"), + read: await isGranted("read"), + write: await isGranted("write"), + net: await isGranted("net"), + env: await isGranted("env"), + plugin: await isGranted("plugin"), + hrtime: await isGranted("hrtime"), + }; +} + +export function permissionsMatch( + processPerms: Permissions, + requiredPerms: Permissions +): boolean { + for (const permName in processPerms) { + if ( + processPerms[permName as keyof Permissions] !== + requiredPerms[permName as keyof Permissions] + ) { + return false; + } + } + + return true; +} + +export const permissionCombinations: Map<string, Permissions> = new Map(); + +function permToString(perms: Permissions): string { + const r = perms.read ? 1 : 0; + const w = perms.write ? 1 : 0; + const n = perms.net ? 1 : 0; + const e = perms.env ? 1 : 0; + const u = perms.run ? 1 : 0; + const p = perms.plugin ? 1 : 0; + const h = perms.hrtime ? 1 : 0; + return `permR${r}W${w}N${n}E${e}U${u}P${p}H${h}`; +} + +function registerPermCombination(perms: Permissions): void { + const key = permToString(perms); + if (!permissionCombinations.has(key)) { + permissionCombinations.set(key, perms); + } +} + +export async function registerUnitTests(): Promise<void> { + const processPerms = await getProcessPermissions(); + + for (const unitTestDefinition of REGISTERED_UNIT_TESTS) { + if (!permissionsMatch(processPerms, unitTestDefinition.perms)) { + continue; + } + + Deno.test(unitTestDefinition); + } +} + +function normalizeTestPermissions(perms: UnitTestPermissions): Permissions { + return { + read: !!perms.read, + write: !!perms.write, + net: !!perms.net, + run: !!perms.run, + env: !!perms.env, + plugin: !!perms.plugin, + hrtime: !!perms.hrtime, + }; +} + +interface UnitTestPermissions { + read?: boolean; + write?: boolean; + net?: boolean; + env?: boolean; + run?: boolean; + plugin?: boolean; + hrtime?: boolean; +} + +interface UnitTestOptions { + ignore?: boolean; + perms?: UnitTestPermissions; +} + +interface UnitTestDefinition extends Deno.TestDefinition { + ignore: boolean; + perms: Permissions; +} + +type TestFunction = () => void | Promise<void>; + +export const REGISTERED_UNIT_TESTS: UnitTestDefinition[] = []; + +export function unitTest(fn: TestFunction): void; +export function unitTest(options: UnitTestOptions, fn: TestFunction): void; +export function unitTest( + optionsOrFn: UnitTestOptions | TestFunction, + maybeFn?: TestFunction +): void { + assert(optionsOrFn, "At least one argument is required"); + + let options: UnitTestOptions; + let name: string; + let fn: TestFunction; + + if (typeof optionsOrFn === "function") { + options = {}; + fn = optionsOrFn; + name = fn.name; + assert(name, "Missing test function name"); + } else { + options = optionsOrFn; + assert(maybeFn, "Missing test function definition"); + assert( + typeof maybeFn === "function", + "Second argument should be test function definition" + ); + fn = maybeFn; + name = fn.name; + assert(name, "Missing test function name"); + } + + const normalizedPerms = normalizeTestPermissions(options.perms || {}); + registerPermCombination(normalizedPerms); + + const unitTestDefinition: UnitTestDefinition = { + name, + fn, + ignore: !!options.ignore, + perms: normalizedPerms, + }; + + REGISTERED_UNIT_TESTS.push(unitTestDefinition); +} + +export interface ResolvableMethods<T> { + resolve: (value?: T | PromiseLike<T>) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject: (reason?: any) => void; +} + +export type Resolvable<T> = Promise<T> & ResolvableMethods<T>; + +export function createResolvable<T>(): Resolvable<T> { + let methods: ResolvableMethods<T>; + const promise = new Promise<T>((resolve, reject): void => { + methods = { resolve, reject }; + }); + // TypeScript doesn't know that the Promise callback occurs synchronously + // therefore use of not null assertion (`!`) + return Object.assign(promise, methods!) as Resolvable<T>; +} + +const encoder = new TextEncoder(); + +// Replace functions with null, errors with their stack strings, and JSONify. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function serializeTestMessage(message: any): string { + return JSON.stringify({ + start: message.start && { + ...message.start, + tests: message.start.tests.map((test: Deno.TestDefinition) => ({ + ...test, + fn: null, + })), + }, + testStart: message.testStart && { ...message.testStart, fn: null }, + testEnd: message.testEnd && { + ...message.testEnd, + error: String(message.testEnd.error?.stack), + }, + end: message.end && { + ...message.end, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + results: message.end.results.map((result: any) => ({ + ...result, + error: result.error?.stack, + })), + }, + }); +} + +export async function reportToConn( + conn: Deno.Conn, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message: any +): Promise<void> { + const line = serializeTestMessage(message); + const encodedMsg = encoder.encode(line + (message.end == null ? "\n" : "")); + await Deno.writeAll(conn, encodedMsg); + if (message.end != null) { + conn.closeWrite(); + } +} + +unitTest(function permissionsMatches(): void { + assert( + permissionsMatch( + { + read: true, + write: false, + net: false, + env: false, + run: false, + plugin: false, + hrtime: false, + }, + normalizeTestPermissions({ read: true }) + ) + ); + + assert( + permissionsMatch( + { + read: false, + write: false, + net: false, + env: false, + run: false, + plugin: false, + hrtime: false, + }, + normalizeTestPermissions({}) + ) + ); + + assertEquals( + permissionsMatch( + { + read: false, + write: true, + net: true, + env: true, + run: true, + plugin: true, + hrtime: true, + }, + normalizeTestPermissions({ read: true }) + ), + false + ); + + assertEquals( + permissionsMatch( + { + read: true, + write: false, + net: true, + env: false, + run: false, + plugin: false, + hrtime: false, + }, + normalizeTestPermissions({ read: true }) + ), + false + ); + + assert( + permissionsMatch( + { + read: true, + write: true, + net: true, + env: true, + run: true, + plugin: true, + hrtime: true, + }, + { + read: true, + write: true, + net: true, + env: true, + run: true, + plugin: true, + hrtime: true, + } + ) + ); +}); + +/* + * Ensure all unit test files (e.g. xxx_test.ts) are present as imports in + * cli/tests/unit/unit_tests.ts as it is easy to miss this out + */ +unitTest( + { perms: { read: true } }, + function assertAllUnitTestFilesImported(): void { + const directoryTestFiles = [...Deno.readDirSync("./cli/tests/unit/")] + .map((k) => k.name) + .filter( + (file) => + file!.endsWith(".ts") && + !file!.endsWith("unit_tests.ts") && + !file!.endsWith("test_util.ts") && + !file!.endsWith("unit_test_runner.ts") + ); + const unitTestsFile: Uint8Array = Deno.readFileSync( + "./cli/tests/unit/unit_tests.ts" + ); + const importLines = new TextDecoder("utf-8") + .decode(unitTestsFile) + .split("\n") + .filter((line) => line.startsWith("import")); + const importedTestFiles = importLines.map( + (relativeFilePath) => relativeFilePath.match(/\/([^\/]+)";/)![1] + ); + + directoryTestFiles.forEach((dirFile) => { + if (!importedTestFiles.includes(dirFile!)) { + throw new Error( + "cil/tests/unit/unit_tests.ts is missing import of test file: cli/js/" + + dirFile + ); + } + }); + } +); diff --git a/cli/tests/unit/testing_test.ts b/cli/tests/unit/testing_test.ts new file mode 100644 index 000000000..854e7161c --- /dev/null +++ b/cli/tests/unit/testing_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { assertThrows, unitTest } from "./test_util.ts"; + +unitTest(function testFnOverloading(): void { + // just verifying that you can use this test definition syntax + Deno.test("test fn overloading", (): void => {}); +}); + +unitTest(function nameOfTestCaseCantBeEmpty(): void { + assertThrows( + () => { + Deno.test("", () => {}); + }, + TypeError, + "The test name can't be empty" + ); + assertThrows( + () => { + Deno.test({ + name: "", + fn: () => {}, + }); + }, + TypeError, + "The test name can't be empty" + ); +}); diff --git a/cli/tests/unit/text_encoding_test.ts b/cli/tests/unit/text_encoding_test.ts new file mode 100644 index 000000000..87b601f36 --- /dev/null +++ b/cli/tests/unit/text_encoding_test.ts @@ -0,0 +1,206 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest(function btoaSuccess(): void { + const text = "hello world"; + const encoded = btoa(text); + assertEquals(encoded, "aGVsbG8gd29ybGQ="); +}); + +unitTest(function atobSuccess(): void { + const encoded = "aGVsbG8gd29ybGQ="; + const decoded = atob(encoded); + assertEquals(decoded, "hello world"); +}); + +unitTest(function atobWithAsciiWhitespace(): void { + const encodedList = [ + " aGVsbG8gd29ybGQ=", + " aGVsbG8gd29ybGQ=", + "aGVsbG8gd29ybGQ= ", + "aGVsbG8gd29ybGQ=\n", + "aGVsbG\t8gd29ybGQ=", + `aGVsbG\t8g + d29ybGQ=`, + ]; + + for (const encoded of encodedList) { + const decoded = atob(encoded); + assertEquals(decoded, "hello world"); + } +}); + +unitTest(function atobThrows(): void { + let threw = false; + try { + atob("aGVsbG8gd29ybGQ=="); + } catch (e) { + threw = true; + } + assert(threw); +}); + +unitTest(function atobThrows2(): void { + let threw = false; + try { + atob("aGVsbG8gd29ybGQ==="); + } catch (e) { + threw = true; + } + assert(threw); +}); + +unitTest(function btoaFailed(): void { + const text = "你好"; + let err; + try { + btoa(text); + } catch (e) { + err = e; + } + assert(!!err); + assert(err instanceof TypeError); +}); + +unitTest(function textDecoder2(): void { + // prettier-ignore + const fixture = new Uint8Array([ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); + const decoder = new TextDecoder(); + assertEquals(decoder.decode(fixture), "𝓽𝓮𝔁𝓽"); +}); + +unitTest(function textDecoderIgnoreBOM(): void { + // prettier-ignore + const fixture = new Uint8Array([ + 0xef, 0xbb, 0xbf, + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); + const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); + assertEquals(decoder.decode(fixture), "𝓽𝓮𝔁𝓽"); +}); + +unitTest(function textDecoderNotBOM(): void { + // prettier-ignore + const fixture = new Uint8Array([ + 0xef, 0xbb, 0x89, + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); + const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); + assertEquals(decoder.decode(fixture), "ﻉ𝓽𝓮𝔁𝓽"); +}); + +unitTest(function textDecoderASCII(): void { + const fixture = new Uint8Array([0x89, 0x95, 0x9f, 0xbf]); + const decoder = new TextDecoder("ascii"); + assertEquals(decoder.decode(fixture), "‰•Ÿ¿"); +}); + +unitTest(function textDecoderErrorEncoding(): void { + let didThrow = false; + try { + new TextDecoder("foo"); + } catch (e) { + didThrow = true; + assertEquals(e.message, "The encoding label provided ('foo') is invalid."); + } + assert(didThrow); +}); + +unitTest(function textEncoder(): void { + const fixture = "𝓽𝓮𝔁𝓽"; + const encoder = new TextEncoder(); + // prettier-ignore + assertEquals(Array.from(encoder.encode(fixture)), [ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); +}); + +unitTest(function textEncodeInto(): void { + const fixture = "text"; + const encoder = new TextEncoder(); + const bytes = new Uint8Array(5); + const result = encoder.encodeInto(fixture, bytes); + assertEquals(result.read, 4); + assertEquals(result.written, 4); + // prettier-ignore + assertEquals(Array.from(bytes), [ + 0x74, 0x65, 0x78, 0x74, 0x00, + ]); +}); + +unitTest(function textEncodeInto2(): void { + const fixture = "𝓽𝓮𝔁𝓽"; + const encoder = new TextEncoder(); + const bytes = new Uint8Array(17); + const result = encoder.encodeInto(fixture, bytes); + assertEquals(result.read, 8); + assertEquals(result.written, 16); + // prettier-ignore + assertEquals(Array.from(bytes), [ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd, 0x00, + ]); +}); + +unitTest(function textEncodeInto3(): void { + const fixture = "𝓽𝓮𝔁𝓽"; + const encoder = new TextEncoder(); + const bytes = new Uint8Array(5); + const result = encoder.encodeInto(fixture, bytes); + assertEquals(result.read, 2); + assertEquals(result.written, 4); + // prettier-ignore + assertEquals(Array.from(bytes), [ + 0xf0, 0x9d, 0x93, 0xbd, 0x00, + ]); +}); + +unitTest(function textDecoderSharedUint8Array(): void { + const ab = new SharedArrayBuffer(6); + const dataView = new DataView(ab); + const charCodeA = "A".charCodeAt(0); + for (let i = 0; i < ab.byteLength; i++) { + dataView.setUint8(i, charCodeA + i); + } + const ui8 = new Uint8Array(ab); + const decoder = new TextDecoder(); + const actual = decoder.decode(ui8); + assertEquals(actual, "ABCDEF"); +}); + +unitTest(function textDecoderSharedInt32Array(): void { + const ab = new SharedArrayBuffer(8); + const dataView = new DataView(ab); + const charCodeA = "A".charCodeAt(0); + for (let i = 0; i < ab.byteLength; i++) { + dataView.setUint8(i, charCodeA + i); + } + const i32 = new Int32Array(ab); + const decoder = new TextDecoder(); + const actual = decoder.decode(i32); + assertEquals(actual, "ABCDEFGH"); +}); + +unitTest(function toStringShouldBeWebCompatibility(): void { + const encoder = new TextEncoder(); + assertEquals(encoder.toString(), "[object TextEncoder]"); + + const decoder = new TextDecoder(); + assertEquals(decoder.toString(), "[object TextDecoder]"); +}); diff --git a/cli/tests/unit/timers_test.ts b/cli/tests/unit/timers_test.ts new file mode 100644 index 000000000..7adff0095 --- /dev/null +++ b/cli/tests/unit/timers_test.ts @@ -0,0 +1,368 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + createResolvable, + assert, + assertEquals, + assertNotEquals, +} from "./test_util.ts"; + +function deferred(): { + promise: Promise<{}>; + resolve: (value?: {} | PromiseLike<{}>) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject: (reason?: any) => void; +} { + let resolve: (value?: {} | PromiseLike<{}>) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let reject: ((reason?: any) => void) | undefined = undefined; + const promise = new Promise<{}>((res, rej): void => { + resolve = res; + reject = rej; + }); + return { + promise, + resolve: resolve!, + reject: reject!, + }; +} + +function waitForMs(ms: number): Promise<number> { + return new Promise((resolve: () => void): number => setTimeout(resolve, ms)); +} + +unitTest(async function timeoutSuccess(): Promise<void> { + const { promise, resolve } = deferred(); + let count = 0; + setTimeout((): void => { + count++; + resolve(); + }, 500); + await promise; + // count should increment + assertEquals(count, 1); +}); + +unitTest(async function timeoutArgs(): Promise<void> { + const { promise, resolve } = deferred(); + const arg = 1; + setTimeout( + (a, b, c): void => { + assertEquals(a, arg); + assertEquals(b, arg.toString()); + assertEquals(c, [arg]); + resolve(); + }, + 10, + arg, + arg.toString(), + [arg] + ); + await promise; +}); + +unitTest(async function timeoutCancelSuccess(): Promise<void> { + let count = 0; + const id = setTimeout((): void => { + count++; + }, 1); + // Cancelled, count should not increment + clearTimeout(id); + await waitForMs(600); + assertEquals(count, 0); +}); + +unitTest(async function timeoutCancelMultiple(): Promise<void> { + function uncalled(): never { + throw new Error("This function should not be called."); + } + + // Set timers and cancel them in the same order. + const t1 = setTimeout(uncalled, 10); + const t2 = setTimeout(uncalled, 10); + const t3 = setTimeout(uncalled, 10); + clearTimeout(t1); + clearTimeout(t2); + clearTimeout(t3); + + // Set timers and cancel them in reverse order. + const t4 = setTimeout(uncalled, 20); + const t5 = setTimeout(uncalled, 20); + const t6 = setTimeout(uncalled, 20); + clearTimeout(t6); + clearTimeout(t5); + clearTimeout(t4); + + // Sleep until we're certain that the cancelled timers aren't gonna fire. + await waitForMs(50); +}); + +unitTest(async function timeoutCancelInvalidSilentFail(): Promise<void> { + // Expect no panic + const { promise, resolve } = deferred(); + let count = 0; + const id = setTimeout((): void => { + count++; + // Should have no effect + clearTimeout(id); + resolve(); + }, 500); + await promise; + assertEquals(count, 1); + + // Should silently fail (no panic) + clearTimeout(2147483647); +}); + +unitTest(async function intervalSuccess(): Promise<void> { + const { promise, resolve } = deferred(); + let count = 0; + const id = setInterval((): void => { + count++; + clearInterval(id); + resolve(); + }, 100); + await promise; + // Clear interval + clearInterval(id); + // count should increment twice + assertEquals(count, 1); + // Similar false async leaking alarm. + // Force next round of polling. + await waitForMs(0); +}); + +unitTest(async function intervalCancelSuccess(): Promise<void> { + let count = 0; + const id = setInterval((): void => { + count++; + }, 1); + clearInterval(id); + await waitForMs(500); + assertEquals(count, 0); +}); + +unitTest(async function intervalOrdering(): Promise<void> { + const timers: number[] = []; + let timeouts = 0; + function onTimeout(): void { + ++timeouts; + for (let i = 1; i < timers.length; i++) { + clearTimeout(timers[i]); + } + } + for (let i = 0; i < 10; i++) { + timers[i] = setTimeout(onTimeout, 1); + } + await waitForMs(500); + assertEquals(timeouts, 1); +}); + +unitTest(function intervalCancelInvalidSilentFail(): void { + // Should silently fail (no panic) + clearInterval(2147483647); +}); + +unitTest(async function fireCallbackImmediatelyWhenDelayOverMaxValue(): Promise< + void +> { + let count = 0; + setTimeout((): void => { + count++; + }, 2 ** 31); + await waitForMs(1); + assertEquals(count, 1); +}); + +unitTest(async function timeoutCallbackThis(): Promise<void> { + const { promise, resolve } = deferred(); + const obj = { + foo(): void { + assertEquals(this, window); + resolve(); + }, + }; + setTimeout(obj.foo, 1); + await promise; +}); + +unitTest(async function timeoutBindThis(): Promise<void> { + const thisCheckPassed = [null, undefined, window, globalThis]; + + const thisCheckFailed = [ + 0, + "", + true, + false, + {}, + [], + "foo", + (): void => {}, + Object.prototype, + ]; + + for (const thisArg of thisCheckPassed) { + const resolvable = createResolvable(); + let hasThrown = 0; + try { + setTimeout.call(thisArg, () => resolvable.resolve(), 1); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + await resolvable; + assertEquals(hasThrown, 1); + } + + for (const thisArg of thisCheckFailed) { + let hasThrown = 0; + try { + setTimeout.call(thisArg, () => {}, 1); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + } +}); + +unitTest(function clearTimeoutShouldConvertToNumber(): void { + let called = false; + const obj = { + valueOf(): number { + called = true; + return 1; + }, + }; + clearTimeout((obj as unknown) as number); + assert(called); +}); + +unitTest(function setTimeoutShouldThrowWithBigint(): void { + let hasThrown = 0; + try { + setTimeout((): void => {}, (1n as unknown) as number); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); +}); + +unitTest(function clearTimeoutShouldThrowWithBigint(): void { + let hasThrown = 0; + try { + clearTimeout((1n as unknown) as number); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); +}); + +unitTest(function testFunctionName(): void { + assertEquals(clearTimeout.name, "clearTimeout"); + assertEquals(clearInterval.name, "clearInterval"); +}); + +unitTest(function testFunctionParamsLength(): void { + assertEquals(setTimeout.length, 1); + assertEquals(setInterval.length, 1); + assertEquals(clearTimeout.length, 0); + assertEquals(clearInterval.length, 0); +}); + +unitTest(function clearTimeoutAndClearIntervalNotBeEquals(): void { + assertNotEquals(clearTimeout, clearInterval); +}); + +unitTest(async function timerMaxCpuBug(): Promise<void> { + // There was a bug where clearing a timeout would cause Deno to use 100% CPU. + clearTimeout(setTimeout(() => {}, 1000)); + // We can check this by counting how many ops have triggered in the interim. + // Certainly less than 10 ops should have been dispatched in next 100 ms. + const { opsDispatched } = Deno.metrics(); + await waitForMs(100); + const opsDispatched_ = Deno.metrics().opsDispatched; + assert(opsDispatched_ - opsDispatched < 10); +}); + +unitTest(async function timerBasicMicrotaskOrdering(): Promise<void> { + let s = ""; + let count = 0; + const { promise, resolve } = deferred(); + setTimeout(() => { + Promise.resolve().then(() => { + count++; + s += "de"; + if (count === 2) { + resolve(); + } + }); + }); + setTimeout(() => { + count++; + s += "no"; + if (count === 2) { + resolve(); + } + }); + await promise; + assertEquals(s, "deno"); +}); + +unitTest(async function timerNestedMicrotaskOrdering(): Promise<void> { + let s = ""; + const { promise, resolve } = deferred(); + s += "0"; + setTimeout(() => { + s += "4"; + setTimeout(() => (s += "A")); + Promise.resolve() + .then(() => { + setTimeout(() => { + s += "B"; + resolve(); + }); + }) + .then(() => { + s += "5"; + }); + }); + setTimeout(() => (s += "6")); + Promise.resolve().then(() => (s += "2")); + Promise.resolve().then(() => + setTimeout(() => { + s += "7"; + Promise.resolve() + .then(() => (s += "8")) + .then(() => { + s += "9"; + }); + }) + ); + Promise.resolve().then(() => Promise.resolve().then(() => (s += "3"))); + s += "1"; + await promise; + assertEquals(s, "0123456789AB"); +}); + +unitTest(function testQueueMicrotask() { + assertEquals(typeof queueMicrotask, "function"); +}); diff --git a/cli/tests/unit/tls_test.ts b/cli/tests/unit/tls_test.ts new file mode 100644 index 000000000..3d071441d --- /dev/null +++ b/cli/tests/unit/tls_test.ts @@ -0,0 +1,262 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + assert, + assertEquals, + createResolvable, + unitTest, +} from "./test_util.ts"; +import { BufWriter, BufReader } from "../../../std/io/bufio.ts"; +import { TextProtoReader } from "../../../std/textproto/mod.ts"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +unitTest(async function connectTLSNoPerm(): Promise<void> { + let err; + try { + await Deno.connectTls({ hostname: "github.com", port: 443 }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest(async function connectTLSCertFileNoReadPerm(): Promise<void> { + let err; + try { + await Deno.connectTls({ + hostname: "github.com", + port: 443, + certFile: "cli/tests/tls/RootCA.crt", + }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { perms: { read: true, net: true } }, + function listenTLSNonExistentCertKeyFiles(): void { + let err; + const options = { + hostname: "localhost", + port: 3500, + certFile: "cli/tests/tls/localhost.crt", + keyFile: "cli/tests/tls/localhost.key", + }; + + try { + Deno.listenTls({ + ...options, + certFile: "./non/existent/file", + }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + + try { + Deno.listenTls({ + ...options, + keyFile: "./non/existent/file", + }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.NotFound); + } +); + +unitTest({ perms: { net: true } }, function listenTLSNoReadPerm(): void { + let err; + try { + Deno.listenTls({ + hostname: "localhost", + port: 3500, + certFile: "cli/tests/tls/localhost.crt", + keyFile: "cli/tests/tls/localhost.key", + }); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest( + { + perms: { read: true, write: true, net: true }, + }, + function listenTLSEmptyKeyFile(): void { + let err; + const options = { + hostname: "localhost", + port: 3500, + certFile: "cli/tests/tls/localhost.crt", + keyFile: "cli/tests/tls/localhost.key", + }; + + const testDir = Deno.makeTempDirSync(); + const keyFilename = testDir + "/key.pem"; + Deno.writeFileSync(keyFilename, new Uint8Array([]), { + mode: 0o666, + }); + + try { + Deno.listenTls({ + ...options, + keyFile: keyFilename, + }); + } catch (e) { + err = e; + } + assert(err instanceof Error); + } +); + +unitTest( + { perms: { read: true, write: true, net: true } }, + function listenTLSEmptyCertFile(): void { + let err; + const options = { + hostname: "localhost", + port: 3500, + certFile: "cli/tests/tls/localhost.crt", + keyFile: "cli/tests/tls/localhost.key", + }; + + const testDir = Deno.makeTempDirSync(); + const certFilename = testDir + "/cert.crt"; + Deno.writeFileSync(certFilename, new Uint8Array([]), { + mode: 0o666, + }); + + try { + Deno.listenTls({ + ...options, + certFile: certFilename, + }); + } catch (e) { + err = e; + } + assert(err instanceof Error); + } +); + +unitTest( + { perms: { read: true, net: true } }, + async function dialAndListenTLS(): Promise<void> { + const resolvable = createResolvable(); + const hostname = "localhost"; + const port = 3500; + + const listener = Deno.listenTls({ + hostname, + port, + certFile: "cli/tests/tls/localhost.crt", + keyFile: "cli/tests/tls/localhost.key", + }); + + const response = encoder.encode( + "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n" + ); + + listener.accept().then( + async (conn): Promise<void> => { + assert(conn.remoteAddr != null); + assert(conn.localAddr != null); + await conn.write(response); + // TODO(bartlomieju): this might be a bug + setTimeout(() => { + conn.close(); + resolvable.resolve(); + }, 0); + } + ); + + const conn = await Deno.connectTls({ + hostname, + port, + certFile: "cli/tests/tls/RootCA.pem", + }); + assert(conn.rid > 0); + const w = new BufWriter(conn); + const r = new BufReader(conn); + const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`; + const writeResult = await w.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await w.flush(); + const tpr = new TextProtoReader(r); + const statusLine = await tpr.readLine(); + assert(statusLine !== null, `line must be read: ${String(statusLine)}`); + const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); + assert(m !== null, "must be matched"); + const [_, proto, status, ok] = m; + assertEquals(proto, "HTTP/1.1"); + assertEquals(status, "200"); + assertEquals(ok, "OK"); + const headers = await tpr.readMIMEHeader(); + assert(headers !== null); + const contentLength = parseInt(headers.get("content-length")!); + const bodyBuf = new Uint8Array(contentLength); + await r.readFull(bodyBuf); + assertEquals(decoder.decode(bodyBuf), "Hello World\n"); + conn.close(); + listener.close(); + await resolvable; + } +); + +unitTest( + { perms: { read: true, net: true } }, + async function startTls(): Promise<void> { + const hostname = "smtp.gmail.com"; + const port = 587; + const encoder = new TextEncoder(); + + let conn = await Deno.connect({ + hostname, + port, + }); + + let writer = new BufWriter(conn); + let reader = new TextProtoReader(new BufReader(conn)); + + let line: string | null = (await reader.readLine()) as string; + assert(line.startsWith("220")); + + await writer.write(encoder.encode(`EHLO ${hostname}\r\n`)); + await writer.flush(); + + while ((line = (await reader.readLine()) as string)) { + assert(line.startsWith("250")); + if (line.startsWith("250 ")) break; + } + + await writer.write(encoder.encode("STARTTLS\r\n")); + await writer.flush(); + + line = await reader.readLine(); + + // Received the message that the server is ready to establish TLS + assertEquals(line, "220 2.0.0 Ready to start TLS"); + + conn = await Deno.startTls(conn, { hostname }); + writer = new BufWriter(conn); + reader = new TextProtoReader(new BufReader(conn)); + + // After that use TLS communication again + await writer.write(encoder.encode(`EHLO ${hostname}\r\n`)); + await writer.flush(); + + while ((line = (await reader.readLine()) as string)) { + assert(line.startsWith("250")); + if (line.startsWith("250 ")) break; + } + + conn.close(); + } +); diff --git a/cli/tests/unit/truncate_test.ts b/cli/tests/unit/truncate_test.ts new file mode 100644 index 000000000..3fd85c990 --- /dev/null +++ b/cli/tests/unit/truncate_test.ts @@ -0,0 +1,80 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals, assert } from "./test_util.ts"; + +function readDataSync(name: string): string { + const data = Deno.readFileSync(name); + const decoder = new TextDecoder("utf-8"); + const text = decoder.decode(data); + return text; +} + +async function readData(name: string): Promise<string> { + const data = await Deno.readFile(name); + const decoder = new TextDecoder("utf-8"); + const text = decoder.decode(data); + return text; +} + +unitTest( + { perms: { read: true, write: true } }, + function truncateSyncSuccess(): void { + const enc = new TextEncoder(); + const d = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test_truncateSync.txt"; + Deno.writeFileSync(filename, d); + Deno.truncateSync(filename, 20); + let data = readDataSync(filename); + assertEquals(data.length, 20); + Deno.truncateSync(filename, 5); + data = readDataSync(filename); + assertEquals(data.length, 5); + Deno.truncateSync(filename, -5); + data = readDataSync(filename); + assertEquals(data.length, 0); + Deno.removeSync(filename); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function truncateSuccess(): Promise<void> { + const enc = new TextEncoder(); + const d = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test_truncate.txt"; + await Deno.writeFile(filename, d); + await Deno.truncate(filename, 20); + let data = await readData(filename); + assertEquals(data.length, 20); + await Deno.truncate(filename, 5); + data = await readData(filename); + assertEquals(data.length, 5); + await Deno.truncate(filename, -5); + data = await readData(filename); + assertEquals(data.length, 0); + await Deno.remove(filename); + } +); + +unitTest({ perms: { write: false } }, function truncateSyncPerm(): void { + let err; + try { + Deno.mkdirSync("/test_truncateSyncPermission.txt"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); + +unitTest({ perms: { write: false } }, async function truncatePerm(): Promise< + void +> { + let err; + try { + await Deno.mkdir("/test_truncatePermission.txt"); + } catch (e) { + err = e; + } + assert(err instanceof Deno.errors.PermissionDenied); + assertEquals(err.name, "PermissionDenied"); +}); diff --git a/cli/tests/unit/tty_test.ts b/cli/tests/unit/tty_test.ts new file mode 100644 index 000000000..116b0dfe9 --- /dev/null +++ b/cli/tests/unit/tty_test.ts @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +// Note tests for Deno.setRaw is in integration tests. + +unitTest({ perms: { read: true } }, function isatty(): void { + // CI not under TTY, so cannot test stdin/stdout/stderr. + const f = Deno.openSync("cli/tests/hello.txt"); + assert(!Deno.isatty(f.rid)); + f.close(); +}); + +unitTest(function isattyError(): void { + let caught = false; + try { + // Absurdly large rid. + Deno.isatty(0x7fffffff); + } catch (e) { + caught = true; + assert(e instanceof Deno.errors.BadResource); + } + assert(caught); +}); diff --git a/cli/tests/unit/umask_test.ts b/cli/tests/unit/umask_test.ts new file mode 100644 index 000000000..bfac65d52 --- /dev/null +++ b/cli/tests/unit/umask_test.ts @@ -0,0 +1,15 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assertEquals } from "./test_util.ts"; + +unitTest( + { + ignore: Deno.build.os === "windows", + }, + function umaskSuccess(): void { + const prevMask = Deno.umask(0o020); + const newMask = Deno.umask(prevMask); + const finalMask = Deno.umask(); + assertEquals(newMask, 0o020); + assertEquals(finalMask, prevMask); + } +); diff --git a/cli/tests/unit/unit_test_runner.ts b/cli/tests/unit/unit_test_runner.ts new file mode 100755 index 000000000..715dda500 --- /dev/null +++ b/cli/tests/unit/unit_test_runner.ts @@ -0,0 +1,309 @@ +#!/usr/bin/env -S deno run --reload --allow-run +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import "./unit_tests.ts"; +import { + readLines, + permissionCombinations, + Permissions, + registerUnitTests, + fmtPerms, + parseArgs, + reportToConn, +} from "./test_util.ts"; + +// @ts-ignore +const internalObj = Deno[Deno.internal]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const reportToConsole = internalObj.reportToConsole as (message: any) => void; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runTests = internalObj.runTests as (options: any) => Promise<any>; + +interface PermissionSetTestResult { + perms: Permissions; + passed: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + endMessage: any; + permsStr: string; +} + +const PERMISSIONS: Deno.PermissionName[] = [ + "read", + "write", + "net", + "env", + "run", + "plugin", + "hrtime", +]; + +/** + * Take a list of permissions and revoke missing permissions. + */ +async function dropWorkerPermissions( + requiredPermissions: Deno.PermissionName[] +): Promise<void> { + const permsToDrop = PERMISSIONS.filter((p): boolean => { + return !requiredPermissions.includes(p); + }); + + for (const perm of permsToDrop) { + await Deno.permissions.revoke({ name: perm }); + } +} + +async function workerRunnerMain( + addrStr: string, + permsStr: string, + filter?: string +): Promise<void> { + const [hostname, port] = addrStr.split(":"); + const addr = { hostname, port: Number(port) }; + + let perms: Deno.PermissionName[] = []; + if (permsStr.length > 0) { + perms = permsStr.split(",") as Deno.PermissionName[]; + } + // Setup reporter + const conn = await Deno.connect(addr); + // Drop current process permissions to requested set + await dropWorkerPermissions(perms); + // Register unit tests that match process permissions + await registerUnitTests(); + // Execute tests + await runTests({ + exitOnFail: false, + filter, + reportToConsole: false, + onMessage: reportToConn.bind(null, conn), + }); +} + +function spawnWorkerRunner( + verbose: boolean, + addr: string, + perms: Permissions, + filter?: string +): Deno.Process { + // run subsequent tests using same deno executable + const permStr = Object.keys(perms) + .filter((permName): boolean => { + return perms[permName as Deno.PermissionName] === true; + }) + .join(","); + + const cmd = [ + Deno.execPath(), + "run", + "--unstable", // TODO(ry) be able to test stable vs unstable + "-A", + "cli/tests/unit/unit_test_runner.ts", + "--worker", + `--addr=${addr}`, + `--perms=${permStr}`, + ]; + + if (filter) { + cmd.push("--"); + cmd.push(filter); + } + + const ioMode = verbose ? "inherit" : "null"; + + const p = Deno.run({ + cmd, + stdin: ioMode, + stdout: ioMode, + stderr: ioMode, + }); + + return p; +} + +async function runTestsForPermissionSet( + listener: Deno.Listener, + addrStr: string, + verbose: boolean, + perms: Permissions, + filter?: string +): Promise<PermissionSetTestResult> { + const permsFmt = fmtPerms(perms); + console.log(`Running tests for: ${permsFmt}`); + const workerProcess = spawnWorkerRunner(verbose, addrStr, perms, filter); + // Wait for worker subprocess to go online + const conn = await listener.accept(); + + let expectedPassedTests; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let endMessage: any; + + try { + for await (const line of readLines(conn)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = JSON.parse(line) as any; + reportToConsole(message); + if (message.start != null) { + expectedPassedTests = message.start.tests.length; + } else if (message.end != null) { + endMessage = message.end; + } + } + } finally { + // Close socket to worker. + conn.close(); + } + + if (expectedPassedTests == null) { + throw new Error("Worker runner didn't report start"); + } + + if (endMessage == null) { + throw new Error("Worker runner didn't report end"); + } + + const workerStatus = await workerProcess.status(); + if (!workerStatus.success) { + throw new Error( + `Worker runner exited with status code: ${workerStatus.code}` + ); + } + + workerProcess.close(); + + const passed = expectedPassedTests === endMessage.passed + endMessage.ignored; + + return { + perms, + passed, + permsStr: permsFmt, + endMessage, + }; +} + +async function masterRunnerMain( + verbose: boolean, + filter?: string +): Promise<void> { + console.log( + "Discovered permission combinations for tests:", + permissionCombinations.size + ); + + for (const perms of permissionCombinations.values()) { + console.log("\t" + fmtPerms(perms)); + } + + const testResults = new Set<PermissionSetTestResult>(); + const addr = { hostname: "127.0.0.1", port: 4510 }; + const addrStr = `${addr.hostname}:${addr.port}`; + const listener = Deno.listen(addr); + + for (const perms of permissionCombinations.values()) { + const result = await runTestsForPermissionSet( + listener, + addrStr, + verbose, + perms, + filter + ); + testResults.add(result); + } + + // if any run tests returned non-zero status then whole test + // run should fail + let testsPassed = true; + + for (const testResult of testResults) { + const { permsStr, endMessage } = testResult; + console.log(`Summary for ${permsStr}`); + reportToConsole({ end: endMessage }); + testsPassed = testsPassed && testResult.passed; + } + + if (!testsPassed) { + console.error("Unit tests failed"); + Deno.exit(1); + } + + console.log("Unit tests passed"); +} + +const HELP = `Unit test runner + +Run tests matching current process permissions: + + deno --allow-write unit_test_runner.ts + + deno --allow-net --allow-hrtime unit_test_runner.ts + + deno --allow-write unit_test_runner.ts -- testWriteFile + +Run "master" process that creates "worker" processes +for each discovered permission combination: + + deno -A unit_test_runner.ts --master + +Run worker process for given permissions: + + deno -A unit_test_runner.ts --worker --perms=net,read,write --addr=127.0.0.1:4500 + + +OPTIONS: + --master + Run in master mode, spawning worker processes for + each discovered permission combination + + --worker + Run in worker mode, requires "perms" and "addr" flags, + should be run with "-A" flag; after setup worker will + drop permissions to required set specified in "perms" + + --perms=<perm_name>... + Set of permissions this process should run tests with, + + --addr=<addr> + Address of TCP socket for reporting + +ARGS: + -- <filter>... + Run only tests with names matching filter, must + be used after "--" +`; + +function assertOrHelp(expr: unknown): asserts expr { + if (!expr) { + console.log(HELP); + Deno.exit(1); + } +} + +async function main(): Promise<void> { + const args = parseArgs(Deno.args, { + boolean: ["master", "worker", "verbose"], + "--": true, + }); + + if (args.help) { + console.log(HELP); + return; + } + + const filter = args["--"][0]; + + // Master mode + if (args.master) { + return masterRunnerMain(args.verbose, filter); + } + + // Worker mode + if (args.worker) { + assertOrHelp(typeof args.addr === "string"); + assertOrHelp(typeof args.perms === "string"); + return workerRunnerMain(args.addr, args.perms, filter); + } + + // Running tests matching current process permissions + await registerUnitTests(); + await runTests({ filter }); +} + +main(); diff --git a/cli/tests/unit/unit_tests.ts b/cli/tests/unit/unit_tests.ts new file mode 100644 index 000000000..7327bcc05 --- /dev/null +++ b/cli/tests/unit/unit_tests.ts @@ -0,0 +1,72 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This test is executed as part of unit test suite. +// +// Test runner automatically spawns subprocesses for each required permissions combination. + +import "./abort_controller_test.ts"; +import "./blob_test.ts"; +import "./body_test.ts"; +import "./buffer_test.ts"; +import "./build_test.ts"; +import "./chmod_test.ts"; +import "./chown_test.ts"; +import "./console_test.ts"; +import "./copy_file_test.ts"; +import "./custom_event_test.ts"; +import "./dir_test.ts"; +import "./dispatch_minimal_test.ts"; +import "./dispatch_json_test.ts"; +import "./dom_exception_test.ts"; +import "./error_stack_test.ts"; +import "./event_test.ts"; +import "./event_target_test.ts"; +import "./fetch_test.ts"; +import "./file_test.ts"; +import "./files_test.ts"; +import "./form_data_test.ts"; +import "./format_error_test.ts"; +import "./fs_events_test.ts"; +import "./get_random_values_test.ts"; +import "./globals_test.ts"; +import "./headers_test.ts"; +import "./internals_test.ts"; +import "./io_test.ts"; +import "./link_test.ts"; +import "./make_temp_test.ts"; +import "./metrics_test.ts"; +import "./dom_iterable_test.ts"; +import "./mkdir_test.ts"; +import "./net_test.ts"; +import "./os_test.ts"; +import "./permissions_test.ts"; +import "./process_test.ts"; +import "./real_path_test.ts"; +import "./read_dir_test.ts"; +import "./read_text_file_test.ts"; +import "./read_file_test.ts"; +import "./read_link_test.ts"; +import "./remove_test.ts"; +import "./rename_test.ts"; +import "./request_test.ts"; +import "./resources_test.ts"; +import "./signal_test.ts"; +import "./stat_test.ts"; +import "./streams_piping_test.ts"; +import "./streams_transform_test.ts"; +import "./streams_writable_test.ts"; +import "./symlink_test.ts"; +import "./text_encoding_test.ts"; +import "./testing_test.ts"; +import "./timers_test.ts"; +import "./tls_test.ts"; +import "./truncate_test.ts"; +import "./tty_test.ts"; +import "./umask_test.ts"; +import "./url_test.ts"; +import "./url_search_params_test.ts"; +import "./utime_test.ts"; +import "./write_file_test.ts"; +import "./write_text_file_test.ts"; +import "./performance_test.ts"; +import "./version_test.ts"; diff --git a/cli/tests/unit/url_search_params_test.ts b/cli/tests/unit/url_search_params_test.ts new file mode 100644 index 000000000..7b7dbab76 --- /dev/null +++ b/cli/tests/unit/url_search_params_test.ts @@ -0,0 +1,275 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest(function urlSearchParamsInitString(): void { + const init = "c=4&a=2&b=3&%C3%A1=1"; + const searchParams = new URLSearchParams(init); + assert( + init === searchParams.toString(), + "The init query string does not match" + ); +}); + +unitTest(function urlSearchParamsInitIterable(): void { + const init = [ + ["a", "54"], + ["b", "true"], + ]; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.toString(), "a=54&b=true"); +}); + +unitTest(function urlSearchParamsInitRecord(): void { + const init = { a: "54", b: "true" }; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.toString(), "a=54&b=true"); +}); + +unitTest(function urlSearchParamsInit(): void { + const params1 = new URLSearchParams("a=b"); + assertEquals(params1.toString(), "a=b"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const params2 = new URLSearchParams(params1 as any); + assertEquals(params2.toString(), "a=b"); +}); + +unitTest(function urlSearchParamsAppendSuccess(): void { + const searchParams = new URLSearchParams(); + searchParams.append("a", "true"); + assertEquals(searchParams.toString(), "a=true"); +}); + +unitTest(function urlSearchParamsDeleteSuccess(): void { + const init = "a=54&b=true"; + const searchParams = new URLSearchParams(init); + searchParams.delete("b"); + assertEquals(searchParams.toString(), "a=54"); +}); + +unitTest(function urlSearchParamsGetAllSuccess(): void { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.getAll("a"), ["54", "true"]); + assertEquals(searchParams.getAll("b"), ["true"]); + assertEquals(searchParams.getAll("c"), []); +}); + +unitTest(function urlSearchParamsGetSuccess(): void { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.get("a"), "54"); + assertEquals(searchParams.get("b"), "true"); + assertEquals(searchParams.get("c"), null); +}); + +unitTest(function urlSearchParamsHasSuccess(): void { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + assert(searchParams.has("a")); + assert(searchParams.has("b")); + assert(!searchParams.has("c")); +}); + +unitTest(function urlSearchParamsSetReplaceFirstAndRemoveOthers(): void { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + searchParams.set("a", "false"); + assertEquals(searchParams.toString(), "a=false&b=true"); +}); + +unitTest(function urlSearchParamsSetAppendNew(): void { + const init = "a=54&b=true&a=true"; + const searchParams = new URLSearchParams(init); + searchParams.set("c", "foo"); + assertEquals(searchParams.toString(), "a=54&b=true&a=true&c=foo"); +}); + +unitTest(function urlSearchParamsSortSuccess(): void { + const init = "c=4&a=2&b=3&a=1"; + const searchParams = new URLSearchParams(init); + searchParams.sort(); + assertEquals(searchParams.toString(), "a=2&a=1&b=3&c=4"); +}); + +unitTest(function urlSearchParamsForEachSuccess(): void { + const init = [ + ["a", "54"], + ["b", "true"], + ]; + const searchParams = new URLSearchParams(init); + let callNum = 0; + searchParams.forEach((value, key, parent): void => { + assertEquals(searchParams, parent); + assertEquals(value, init[callNum][1]); + assertEquals(key, init[callNum][0]); + callNum++; + }); + assertEquals(callNum, init.length); +}); + +unitTest(function urlSearchParamsMissingName(): void { + const init = "=4"; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.get(""), "4"); + assertEquals(searchParams.toString(), "=4"); +}); + +unitTest(function urlSearchParamsMissingValue(): void { + const init = "4="; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.get("4"), ""); + assertEquals(searchParams.toString(), "4="); +}); + +unitTest(function urlSearchParamsMissingEqualSign(): void { + const init = "4"; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.get("4"), ""); + assertEquals(searchParams.toString(), "4="); +}); + +unitTest(function urlSearchParamsMissingPair(): void { + const init = "c=4&&a=54&"; + const searchParams = new URLSearchParams(init); + assertEquals(searchParams.toString(), "c=4&a=54"); +}); + +// If pair does not contain exactly two items, then throw a TypeError. +// ref https://url.spec.whatwg.org/#interface-urlsearchparams +unitTest(function urlSearchParamsShouldThrowTypeError(): void { + let hasThrown = 0; + + try { + new URLSearchParams([["1"]]); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + + assertEquals(hasThrown, 2); + + try { + new URLSearchParams([["1", "2", "3"]]); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + + assertEquals(hasThrown, 2); +}); + +unitTest(function urlSearchParamsAppendArgumentsCheck(): void { + const methodRequireOneParam = ["delete", "getAll", "get", "has", "forEach"]; + + const methodRequireTwoParams = ["append", "set"]; + + methodRequireOneParam + .concat(methodRequireTwoParams) + .forEach((method: string): void => { + const searchParams = new URLSearchParams(); + let hasThrown = 0; + try { + // @ts-ignore + searchParams[method](); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + }); + + methodRequireTwoParams.forEach((method: string): void => { + const searchParams = new URLSearchParams(); + let hasThrown = 0; + try { + // @ts-ignore + searchParams[method]("foo"); + hasThrown = 1; + } catch (err) { + if (err instanceof TypeError) { + hasThrown = 2; + } else { + hasThrown = 3; + } + } + assertEquals(hasThrown, 2); + }); +}); + +// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-delete.any.js +unitTest(function urlSearchParamsDeletingAppendedMultiple(): void { + const params = new URLSearchParams(); + params.append("first", (1 as unknown) as string); + assert(params.has("first")); + assertEquals(params.get("first"), "1"); + params.delete("first"); + assertEquals(params.has("first"), false); + params.append("first", (1 as unknown) as string); + params.append("first", (10 as unknown) as string); + params.delete("first"); + assertEquals(params.has("first"), false); +}); + +// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-constructor.any.js#L176-L182 +unitTest(function urlSearchParamsCustomSymbolIterator(): void { + const params = new URLSearchParams(); + params[Symbol.iterator] = function* (): IterableIterator<[string, string]> { + yield ["a", "b"]; + }; + const params1 = new URLSearchParams((params as unknown) as string[][]); + assertEquals(params1.get("a"), "b"); +}); + +unitTest( + function urlSearchParamsCustomSymbolIteratorWithNonStringParams(): void { + const params = {}; + // @ts-ignore + params[Symbol.iterator] = function* (): IterableIterator<[number, number]> { + yield [1, 2]; + }; + const params1 = new URLSearchParams((params as unknown) as string[][]); + assertEquals(params1.get("1"), "2"); + } +); + +// If a class extends URLSearchParams, override one method should not change another's behavior. +unitTest( + function urlSearchParamsOverridingAppendNotChangeConstructorAndSet(): void { + let overridedAppendCalled = 0; + class CustomSearchParams extends URLSearchParams { + append(name: string, value: string): void { + ++overridedAppendCalled; + super.append(name, value); + } + } + new CustomSearchParams("foo=bar"); + new CustomSearchParams([["foo", "bar"]]); + new CustomSearchParams(new CustomSearchParams({ foo: "bar" })); + new CustomSearchParams().set("foo", "bar"); + assertEquals(overridedAppendCalled, 0); + } +); + +unitTest(function urlSearchParamsOverridingEntriesNotChangeForEach(): void { + class CustomSearchParams extends URLSearchParams { + *entries(): IterableIterator<[string, string]> { + yield* []; + } + } + let loopCount = 0; + const params = new CustomSearchParams({ foo: "bar" }); + params.forEach(() => void ++loopCount); + assertEquals(loopCount, 1); +}); diff --git a/cli/tests/unit/url_test.ts b/cli/tests/unit/url_test.ts new file mode 100644 index 000000000..68fcbd95e --- /dev/null +++ b/cli/tests/unit/url_test.ts @@ -0,0 +1,407 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals, assertThrows } from "./test_util.ts"; + +unitTest(function urlParsing(): void { + const url = new URL( + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + assertEquals(url.hash, "#qat"); + assertEquals(url.host, "baz.qat:8000"); + assertEquals(url.hostname, "baz.qat"); + assertEquals( + url.href, + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + assertEquals(url.origin, "https://baz.qat:8000"); + assertEquals(url.password, "bar"); + assertEquals(url.pathname, "/qux/quux"); + assertEquals(url.port, "8000"); + assertEquals(url.protocol, "https:"); + assertEquals(url.search, "?foo=bar&baz=12"); + assertEquals(url.searchParams.getAll("foo"), ["bar"]); + assertEquals(url.searchParams.getAll("baz"), ["12"]); + assertEquals(url.username, "foo"); + assertEquals( + String(url), + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + assertEquals( + JSON.stringify({ key: url }), + `{"key":"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"}` + ); +}); + +unitTest(function urlModifications(): void { + const url = new URL( + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + url.hash = ""; + assertEquals( + url.href, + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12" + ); + url.host = "qat.baz:8080"; + assertEquals( + url.href, + "https://foo:bar@qat.baz:8080/qux/quux?foo=bar&baz=12" + ); + url.hostname = "foo.bar"; + assertEquals( + url.href, + "https://foo:bar@foo.bar:8080/qux/quux?foo=bar&baz=12" + ); + url.password = "qux"; + assertEquals( + url.href, + "https://foo:qux@foo.bar:8080/qux/quux?foo=bar&baz=12" + ); + url.pathname = "/foo/bar%qat"; + assertEquals( + url.href, + "https://foo:qux@foo.bar:8080/foo/bar%qat?foo=bar&baz=12" + ); + url.port = ""; + assertEquals(url.href, "https://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12"); + url.protocol = "http:"; + assertEquals(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12"); + url.search = "?foo=bar&foo=baz"; + assertEquals(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz"); + assertEquals(url.searchParams.getAll("foo"), ["bar", "baz"]); + url.username = "foo@bar"; + assertEquals( + url.href, + "http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz" + ); + url.searchParams.set("bar", "qat"); + assertEquals( + url.href, + "http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz&bar=qat" + ); + url.searchParams.delete("foo"); + assertEquals(url.href, "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat"); + url.searchParams.append("foo", "bar"); + assertEquals( + url.href, + "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat&foo=bar" + ); +}); + +unitTest(function urlModifyHref(): void { + const url = new URL("http://example.com/"); + url.href = "https://foo:bar@example.com:8080/baz/qat#qux"; + assertEquals(url.protocol, "https:"); + assertEquals(url.username, "foo"); + assertEquals(url.password, "bar"); + assertEquals(url.host, "example.com:8080"); + assertEquals(url.hostname, "example.com"); + assertEquals(url.pathname, "/baz/qat"); + assertEquals(url.hash, "#qux"); +}); + +unitTest(function urlNormalize(): void { + const url = new URL("http://example.com"); + assertEquals(url.pathname, "/"); + assertEquals(url.href, "http://example.com/"); +}); + +unitTest(function urlModifyPathname(): void { + const url = new URL("http://foo.bar/baz%qat/qux%quux"); + assertEquals(url.pathname, "/baz%qat/qux%quux"); + url.pathname = url.pathname; + assertEquals(url.pathname, "/baz%qat/qux%quux"); + url.pathname = "baz#qat qux"; + assertEquals(url.pathname, "/baz%23qat%20qux"); + url.pathname = url.pathname; + assertEquals(url.pathname, "/baz%23qat%20qux"); +}); + +unitTest(function urlModifyHash(): void { + const url = new URL("http://foo.bar"); + url.hash = "%foo bar/qat%qux#bar"; + assertEquals(url.hash, "#%foo%20bar/qat%qux#bar"); + url.hash = url.hash; + assertEquals(url.hash, "#%foo%20bar/qat%qux#bar"); +}); + +unitTest(function urlSearchParamsReuse(): void { + const url = new URL( + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + const sp = url.searchParams; + url.host = "baz.qat"; + assert(sp === url.searchParams, "Search params should be reused."); +}); + +unitTest(function urlBackSlashes(): void { + const url = new URL( + "https:\\\\foo:bar@baz.qat:8000\\qux\\quux?foo=bar&baz=12#qat" + ); + assertEquals( + url.href, + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); +}); + +unitTest(function urlRequireHost(): void { + assertEquals(new URL("file:///").href, "file:///"); + assertThrows(() => { + new URL("ftp:///"); + }); + assertThrows(() => { + new URL("http:///"); + }); + assertThrows(() => { + new URL("https:///"); + }); + assertThrows(() => { + new URL("ws:///"); + }); + assertThrows(() => { + new URL("wss:///"); + }); +}); + +unitTest(function urlDriveLetter() { + assertEquals( + new URL("file:///C:").href, + Deno.build.os == "windows" ? "file:///C:/" : "file:///C:" + ); + assertEquals(new URL("http://example.com/C:").href, "http://example.com/C:"); +}); + +unitTest(function urlHostnameUpperCase() { + assertEquals(new URL("https://EXAMPLE.COM").href, "https://example.com/"); +}); + +unitTest(function urlTrim() { + assertEquals(new URL(" https://example.com ").href, "https://example.com/"); +}); + +unitTest(function urlEncoding() { + assertEquals( + new URL("https://a !$&*()=,;+'\"@example.com").username, + "a%20!$&*()%3D,%3B+%27%22" + ); + assertEquals( + new URL("https://:a !$&*()=,;+'\"@example.com").password, + "a%20!$&*()%3D,%3B+%27%22" + ); + // FIXME: https://url.spec.whatwg.org/#idna + // assertEquals( + // new URL("https://a !$&*()=,+'\"").hostname, + // "a%20%21%24%26%2A%28%29%3D%2C+%27%22" + // ); + assertEquals( + new URL("https://example.com/a ~!@$&*()=:/,;+'\"\\").pathname, + "/a%20~!@$&*()=:/,;+'%22/" + ); + assertEquals( + new URL("https://example.com?a ~!@$&*()=:/,;?+'\"\\").search, + "?a%20~!@$&*()=:/,;?+%27%22\\" + ); + assertEquals( + new URL("https://example.com#a ~!@#$&*()=:/,;?+'\"\\").hash, + "#a%20~!@#$&*()=:/,;?+'%22\\" + ); +}); + +unitTest(function urlBaseURL(): void { + const base = new URL( + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + const url = new URL("/foo/bar?baz=foo#qux", base); + assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux"); +}); + +unitTest(function urlBaseString(): void { + const url = new URL( + "/foo/bar?baz=foo#qux", + "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat" + ); + assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux"); +}); + +unitTest(function urlRelativeWithBase(): void { + assertEquals(new URL("", "file:///a/a/a").href, "file:///a/a/a"); + assertEquals(new URL(".", "file:///a/a/a").href, "file:///a/a/"); + assertEquals(new URL("..", "file:///a/a/a").href, "file:///a/"); + assertEquals(new URL("b", "file:///a/a/a").href, "file:///a/a/b"); + assertEquals(new URL("b", "file:///a/a/a/").href, "file:///a/a/a/b"); + assertEquals(new URL("b/", "file:///a/a/a").href, "file:///a/a/b/"); + assertEquals(new URL("../b", "file:///a/a/a").href, "file:///a/b"); +}); + +unitTest(function urlDriveLetterBase() { + assertEquals( + new URL("/b", "file:///C:/a/b").href, + Deno.build.os == "windows" ? "file:///C:/b" : "file:///b" + ); + assertEquals( + new URL("D:", "file:///C:/a/b").href, + Deno.build.os == "windows" ? "file:///D:/" : "file:///C:/a/D:" + ); + assertEquals( + new URL("/D:", "file:///C:/a/b").href, + Deno.build.os == "windows" ? "file:///D:/" : "file:///D:" + ); + assertEquals( + new URL("D:/b", "file:///C:/a/b").href, + Deno.build.os == "windows" ? "file:///D:/b" : "file:///C:/a/D:/b" + ); +}); + +unitTest(function emptyBasePath(): void { + assertEquals(new URL("", "http://example.com").href, "http://example.com/"); +}); + +unitTest(function deletingAllParamsRemovesQuestionMarkFromURL(): void { + const url = new URL("http://example.com/?param1¶m2"); + url.searchParams.delete("param1"); + url.searchParams.delete("param2"); + assertEquals(url.href, "http://example.com/"); + assertEquals(url.search, ""); +}); + +unitTest(function removingNonExistentParamRemovesQuestionMarkFromURL(): void { + const url = new URL("http://example.com/?"); + assertEquals(url.href, "http://example.com/?"); + url.searchParams.delete("param1"); + assertEquals(url.href, "http://example.com/"); + assertEquals(url.search, ""); +}); + +unitTest(function sortingNonExistentParamRemovesQuestionMarkFromURL(): void { + const url = new URL("http://example.com/?"); + assertEquals(url.href, "http://example.com/?"); + url.searchParams.sort(); + assertEquals(url.href, "http://example.com/"); + assertEquals(url.search, ""); +}); + +unitTest( + { + // FIXME(bartlomieju) + ignore: true, + }, + function customInspectFunction(): void { + const url = new URL("http://example.com/?"); + assertEquals( + Deno.inspect(url), + 'URL { href: "http://example.com/?", origin: "http://example.com", protocol: "http:", username: "", password: "", host: "example.com", hostname: "example.com", port: "", pathname: "/", hash: "", search: "?" }' + ); + } +); + +unitTest(function protocolNotHttpOrFile() { + const url = new URL("about:blank"); + assertEquals(url.href, "about:blank"); + assertEquals(url.protocol, "about:"); + assertEquals(url.origin, "null"); +}); + +unitTest(function createBadUrl(): void { + assertThrows(() => { + new URL("0.0.0.0:8080"); + }); +}); + +unitTest(function throwForInvalidPortConstructor(): void { + const urls = [ + // If port is greater than 2^16 − 1, validation error, return failure. + `https://baz.qat:${2 ** 16}`, + "https://baz.qat:-32", + "https://baz.qat:deno", + "https://baz.qat:9land", + "https://baz.qat:10.5", + ]; + + for (const url of urls) { + assertThrows(() => new URL(url), TypeError, "Invalid URL."); + } + + // Do not throw for 0 & 65535 + new URL("https://baz.qat:65535"); + new URL("https://baz.qat:0"); +}); + +unitTest(function throwForInvalidSchemeConstructor(): void { + assertThrows( + () => new URL("invalid_scheme://baz.qat"), + TypeError, + "Invalid URL." + ); +}); + +unitTest(function doNotOverridePortIfInvalid(): void { + const initialPort = "3000"; + const ports = [ + // If port is greater than 2^16 − 1, validation error, return failure. + `${2 ** 16}`, + "-32", + "deno", + "9land", + "10.5", + ]; + + for (const port of ports) { + const url = new URL(`https://deno.land:${initialPort}`); + url.port = port; + assertEquals(url.port, initialPort); + } +}); + +unitTest(function emptyPortForSchemeDefaultPort(): void { + const nonDefaultPort = "3500"; + const urls = [ + { url: "ftp://baz.qat:21", port: "21", protocol: "ftp:" }, + { url: "https://baz.qat:443", port: "443", protocol: "https:" }, + { url: "wss://baz.qat:443", port: "443", protocol: "wss:" }, + { url: "http://baz.qat:80", port: "80", protocol: "http:" }, + { url: "ws://baz.qat:80", port: "80", protocol: "ws:" }, + { url: "file://home/index.html", port: "", protocol: "file:" }, + { url: "/foo", baseUrl: "ftp://baz.qat:21", port: "21", protocol: "ftp:" }, + { + url: "/foo", + baseUrl: "https://baz.qat:443", + port: "443", + protocol: "https:", + }, + { + url: "/foo", + baseUrl: "wss://baz.qat:443", + port: "443", + protocol: "wss:", + }, + { + url: "/foo", + baseUrl: "http://baz.qat:80", + port: "80", + protocol: "http:", + }, + { url: "/foo", baseUrl: "ws://baz.qat:80", port: "80", protocol: "ws:" }, + { + url: "/foo", + baseUrl: "file://home/index.html", + port: "", + protocol: "file:", + }, + ]; + + for (const { url: urlString, baseUrl, port, protocol } of urls) { + const url = new URL(urlString, baseUrl); + assertEquals(url.port, ""); + + url.port = nonDefaultPort; + assertEquals(url.port, nonDefaultPort); + + url.port = port; + assertEquals(url.port, ""); + + // change scheme + url.protocol = "sftp:"; + assertEquals(url.port, port); + + url.protocol = protocol; + assertEquals(url.port, ""); + } +}); diff --git a/cli/tests/unit/utime_test.ts b/cli/tests/unit/utime_test.ts new file mode 100644 index 000000000..63727ae62 --- /dev/null +++ b/cli/tests/unit/utime_test.ts @@ -0,0 +1,230 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert } from "./test_util.ts"; + +// Allow 10 second difference. +// Note this might not be enough for FAT (but we are not testing on such fs). +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function assertFuzzyTimestampEquals(t1: Date | null, t2: Date): void { + assert(t1 instanceof Date); + assert(Math.abs(t1.valueOf() - t2.valueOf()) < 10_000); +} + +unitTest( + { perms: { read: true, write: true } }, + function utimeSyncFileSuccess(): void { + const testDir = Deno.makeTempDirSync(); + const filename = testDir + "/file.txt"; + Deno.writeFileSync(filename, new TextEncoder().encode("hello"), { + mode: 0o666, + }); + + const atime = 1000; + const mtime = 50000; + Deno.utimeSync(filename, atime, mtime); + + const fileInfo = Deno.statSync(filename); + assertFuzzyTimestampEquals(fileInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(fileInfo.mtime, new Date(mtime * 1000)); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function utimeSyncDirectorySuccess(): void { + const testDir = Deno.makeTempDirSync(); + + const atime = 1000; + const mtime = 50000; + Deno.utimeSync(testDir, atime, mtime); + + const dirInfo = Deno.statSync(testDir); + assertFuzzyTimestampEquals(dirInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(dirInfo.mtime, new Date(mtime * 1000)); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function utimeSyncDateSuccess(): void { + const testDir = Deno.makeTempDirSync(); + + const atime = new Date(1000_000); + const mtime = new Date(50000_000); + Deno.utimeSync(testDir, atime, mtime); + + const dirInfo = Deno.statSync(testDir); + assertFuzzyTimestampEquals(dirInfo.atime, atime); + assertFuzzyTimestampEquals(dirInfo.mtime, mtime); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function utimeSyncFileDateSuccess() { + const testDir = Deno.makeTempDirSync(); + const filename = testDir + "/file.txt"; + Deno.writeFileSync(filename, new TextEncoder().encode("hello"), { + mode: 0o666, + }); + const atime = new Date(); + const mtime = new Date(); + Deno.utimeSync(filename, atime, mtime); + + const fileInfo = Deno.statSync(filename); + assertFuzzyTimestampEquals(fileInfo.atime, atime); + assertFuzzyTimestampEquals(fileInfo.mtime, mtime); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function utimeSyncLargeNumberSuccess(): void { + const testDir = Deno.makeTempDirSync(); + + // There are Rust side caps (might be fs relate), + // so JUST make them slightly larger than UINT32_MAX. + const atime = 0x100000001; + const mtime = 0x100000002; + Deno.utimeSync(testDir, atime, mtime); + + const dirInfo = Deno.statSync(testDir); + assertFuzzyTimestampEquals(dirInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(dirInfo.mtime, new Date(mtime * 1000)); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function utimeSyncNotFound(): void { + const atime = 1000; + const mtime = 50000; + + let caughtError = false; + try { + Deno.utimeSync("/baddir", atime, mtime); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: false } }, + function utimeSyncPerm(): void { + const atime = 1000; + const mtime = 50000; + + let caughtError = false; + try { + Deno.utimeSync("/some_dir", atime, mtime); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function utimeFileSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const filename = testDir + "/file.txt"; + Deno.writeFileSync(filename, new TextEncoder().encode("hello"), { + mode: 0o666, + }); + + const atime = 1000; + const mtime = 50000; + await Deno.utime(filename, atime, mtime); + + const fileInfo = Deno.statSync(filename); + assertFuzzyTimestampEquals(fileInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(fileInfo.mtime, new Date(mtime * 1000)); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function utimeDirectorySuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + + const atime = 1000; + const mtime = 50000; + await Deno.utime(testDir, atime, mtime); + + const dirInfo = Deno.statSync(testDir); + assertFuzzyTimestampEquals(dirInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(dirInfo.mtime, new Date(mtime * 1000)); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function utimeDateSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + + const atime = new Date(100_000); + const mtime = new Date(5000_000); + await Deno.utime(testDir, atime, mtime); + + const dirInfo = Deno.statSync(testDir); + assertFuzzyTimestampEquals(dirInfo.atime, atime); + assertFuzzyTimestampEquals(dirInfo.mtime, mtime); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function utimeFileDateSuccess(): Promise<void> { + const testDir = Deno.makeTempDirSync(); + const filename = testDir + "/file.txt"; + Deno.writeFileSync(filename, new TextEncoder().encode("hello"), { + mode: 0o666, + }); + + const atime = new Date(); + const mtime = new Date(); + await Deno.utime(filename, atime, mtime); + + const fileInfo = Deno.statSync(filename); + assertFuzzyTimestampEquals(fileInfo.atime, atime); + assertFuzzyTimestampEquals(fileInfo.mtime, mtime); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function utimeNotFound(): Promise<void> { + const atime = 1000; + const mtime = 50000; + + let caughtError = false; + try { + await Deno.utime("/baddir", atime, mtime); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: false } }, + async function utimeSyncPerm(): Promise<void> { + const atime = 1000; + const mtime = 50000; + + let caughtError = false; + try { + await Deno.utime("/some_dir", atime, mtime); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); diff --git a/cli/tests/unit/version_test.ts b/cli/tests/unit/version_test.ts new file mode 100644 index 000000000..0417b27de --- /dev/null +++ b/cli/tests/unit/version_test.ts @@ -0,0 +1,8 @@ +import { unitTest, assert } from "./test_util.ts"; + +unitTest(function version(): void { + const pattern = /^\d+\.\d+\.\d+/; + assert(pattern.test(Deno.version.deno)); + assert(pattern.test(Deno.version.v8)); + assert(pattern.test(Deno.version.typescript)); +}); diff --git a/cli/tests/unit/write_file_test.ts b/cli/tests/unit/write_file_test.ts new file mode 100644 index 000000000..80f711dbc --- /dev/null +++ b/cli/tests/unit/write_file_test.ts @@ -0,0 +1,228 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { perms: { read: true, write: true } }, + function writeFileSyncSuccess(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data); + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + const actual = dec.decode(dataRead); + assertEquals("Hello", actual); + } +); + +unitTest({ perms: { write: true } }, function writeFileSyncFail(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = "/baddir/test.txt"; + // The following should fail because /baddir doesn't exist (hopefully). + let caughtError = false; + try { + Deno.writeFileSync(filename, data); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); +}); + +unitTest({ perms: { write: false } }, function writeFileSyncPerm(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = "/baddir/test.txt"; + // The following should fail due to no write permission + let caughtError = false; + try { + Deno.writeFileSync(filename, data); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest( + { perms: { read: true, write: true } }, + function writeFileSyncUpdateMode(): void { + if (Deno.build.os !== "windows") { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data, { mode: 0o755 }); + assertEquals(Deno.statSync(filename).mode! & 0o777, 0o755); + Deno.writeFileSync(filename, data, { mode: 0o666 }); + assertEquals(Deno.statSync(filename).mode! & 0o777, 0o666); + } + } +); + +unitTest( + { perms: { read: true, write: true } }, + function writeFileSyncCreate(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + let caughtError = false; + // if create turned off, the file won't be created + try { + Deno.writeFileSync(filename, data, { create: false }); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + + // Turn on create, should have no error + Deno.writeFileSync(filename, data, { create: true }); + Deno.writeFileSync(filename, data, { create: false }); + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + const actual = dec.decode(dataRead); + assertEquals("Hello", actual); + } +); + +unitTest( + { perms: { read: true, write: true } }, + function writeFileSyncAppend(): void { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeFileSync(filename, data); + Deno.writeFileSync(filename, data, { append: true }); + let dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + let actual = dec.decode(dataRead); + assertEquals("HelloHello", actual); + // Now attempt overwrite + Deno.writeFileSync(filename, data, { append: false }); + dataRead = Deno.readFileSync(filename); + actual = dec.decode(dataRead); + assertEquals("Hello", actual); + // append not set should also overwrite + Deno.writeFileSync(filename, data); + dataRead = Deno.readFileSync(filename); + actual = dec.decode(dataRead); + assertEquals("Hello", actual); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function writeFileSuccess(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + await Deno.writeFile(filename, data); + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + const actual = dec.decode(dataRead); + assertEquals("Hello", actual); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function writeFileNotFound(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = "/baddir/test.txt"; + // The following should fail because /baddir doesn't exist (hopefully). + let caughtError = false; + try { + await Deno.writeFile(filename, data); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: false } }, + async function writeFilePerm(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = "/baddir/test.txt"; + // The following should fail due to no write permission + let caughtError = false; + try { + await Deno.writeFile(filename, data); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function writeFileUpdateMode(): Promise<void> { + if (Deno.build.os !== "windows") { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + await Deno.writeFile(filename, data, { mode: 0o755 }); + assertEquals(Deno.statSync(filename).mode! & 0o777, 0o755); + await Deno.writeFile(filename, data, { mode: 0o666 }); + assertEquals(Deno.statSync(filename).mode! & 0o777, 0o666); + } + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function writeFileCreate(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + let caughtError = false; + // if create turned off, the file won't be created + try { + await Deno.writeFile(filename, data, { create: false }); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + + // Turn on create, should have no error + await Deno.writeFile(filename, data, { create: true }); + await Deno.writeFile(filename, data, { create: false }); + const dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + const actual = dec.decode(dataRead); + assertEquals("Hello", actual); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function writeFileAppend(): Promise<void> { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + const filename = Deno.makeTempDirSync() + "/test.txt"; + await Deno.writeFile(filename, data); + await Deno.writeFile(filename, data, { append: true }); + let dataRead = Deno.readFileSync(filename); + const dec = new TextDecoder("utf-8"); + let actual = dec.decode(dataRead); + assertEquals("HelloHello", actual); + // Now attempt overwrite + await Deno.writeFile(filename, data, { append: false }); + dataRead = Deno.readFileSync(filename); + actual = dec.decode(dataRead); + assertEquals("Hello", actual); + // append not set should also overwrite + await Deno.writeFile(filename, data); + dataRead = Deno.readFileSync(filename); + actual = dec.decode(dataRead); + assertEquals("Hello", actual); + } +); diff --git a/cli/tests/unit/write_text_file_test.ts b/cli/tests/unit/write_text_file_test.ts new file mode 100644 index 000000000..321189b0e --- /dev/null +++ b/cli/tests/unit/write_text_file_test.ts @@ -0,0 +1,79 @@ +import { unitTest, assert, assertEquals } from "./test_util.ts"; + +unitTest( + { perms: { read: true, write: true } }, + function writeTextFileSyncSuccess(): void { + const filename = Deno.makeTempDirSync() + "/test.txt"; + Deno.writeTextFileSync(filename, "Hello"); + const dataRead = Deno.readTextFileSync(filename); + assertEquals("Hello", dataRead); + } +); + +unitTest({ perms: { write: true } }, function writeTextFileSyncFail(): void { + const filename = "/baddir/test.txt"; + // The following should fail because /baddir doesn't exist (hopefully). + let caughtError = false; + try { + Deno.writeTextFileSync(filename, "hello"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); +}); + +unitTest({ perms: { write: false } }, function writeTextFileSyncPerm(): void { + const filename = "/baddir/test.txt"; + // The following should fail due to no write permission + let caughtError = false; + try { + Deno.writeTextFileSync(filename, "Hello"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); +}); + +unitTest( + { perms: { read: true, write: true } }, + async function writeTextFileSuccess(): Promise<void> { + const filename = Deno.makeTempDirSync() + "/test.txt"; + await Deno.writeTextFile(filename, "Hello"); + const dataRead = Deno.readTextFileSync(filename); + assertEquals("Hello", dataRead); + } +); + +unitTest( + { perms: { read: true, write: true } }, + async function writeTextFileNotFound(): Promise<void> { + const filename = "/baddir/test.txt"; + // The following should fail because /baddir doesn't exist (hopefully). + let caughtError = false; + try { + await Deno.writeTextFile(filename, "Hello"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.NotFound); + } + assert(caughtError); + } +); + +unitTest( + { perms: { write: false } }, + async function writeTextFilePerm(): Promise<void> { + const filename = "/baddir/test.txt"; + // The following should fail due to no write permission + let caughtError = false; + try { + await Deno.writeTextFile(filename, "Hello"); + } catch (e) { + caughtError = true; + assert(e instanceof Deno.errors.PermissionDenied); + } + assert(caughtError); + } +); |