diff --git a/.github/workflows/docker-test.sh b/.github/workflows/docker-test.sh new file mode 100644 index 0000000..c34fcd8 --- /dev/null +++ b/.github/workflows/docker-test.sh @@ -0,0 +1,15 @@ +name: Docker Image CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run AsyncContext tests with benjamn/deno:async-context Docker image + run: docker/tests/docker-run.sh benjamn/deno:async-context \ No newline at end of file diff --git a/.mocharc.json b/.mocharc.json index fadb4ee..a79e858 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,7 +1,7 @@ { "require": "@esbuild-kit/cjs-loader", "loader": "@esbuild-kit/esm-loader", - "spec": ["tests/**"], + "spec": ["tests/*.test.ts"], "extensions": ["ts"], "watch-files": ["src"], "node-option": ["no-warnings"] diff --git a/tests/docker-native/docker-run.sh b/tests/docker-native/docker-run.sh new file mode 100755 index 0000000..633dfda --- /dev/null +++ b/tests/docker-native/docker-run.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +DENO_IMAGE=${1:-benjamn/deno:async-context} +echo "Running tests using $DENO_IMAGE" + +# Try changing :async-context to :unmodified to observe the tests fail, most +# immediately because AsyncContext is not defined globally. +exec docker run -v $(pwd):/deno --rm $DENO_IMAGE test --allow-read --trace-ops *.ts diff --git a/tests/docker-native/polyfillable.ts b/tests/docker-native/polyfillable.ts new file mode 100644 index 0000000..c00dc34 --- /dev/null +++ b/tests/docker-native/polyfillable.ts @@ -0,0 +1,254 @@ +// These tests were directly adapted from ../async-context.test.ts, with +// different imports and no Promise.prototype.then wrapping. These tests are +// "polyfillable" in the sense that they could pass using a polyfill +// implementation like the one from PR #14, but no polyfill is needed when the +// tests are executed using benjamn/deno:async-context. + +import * as assert from "https://deno.land/std@0.173.0/node/assert.ts"; +import { describe, it } from "https://deno.land/std@0.173.0/testing/bdd.ts"; + +type Value = { id: number }; +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + +describe("sync", () => { + describe("run and get", () => { + it("has initial undefined state", () => { + const ctx = new AsyncContext(); + + const actual = ctx.get(); + + assert.equal(actual, undefined); + }); + + it("return value", () => { + const ctx = new AsyncContext(); + const expected = { id: 1 }; + + const actual = ctx.run({ id: 2 }, () => expected); + + assert.equal(actual, expected); + }); + + it("get returns current context value", () => { + const ctx = new AsyncContext(); + const expected = { id: 1 }; + + const actual = ctx.run(expected, () => ctx.get()); + + assert.equal(actual, expected); + }); + + it("get within nesting contexts", () => { + const ctx = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const actual = ctx.run(first, () => { + return [ctx.get(), ctx.run(second, () => ctx.get()), ctx.get()]; + }); + + assert.deepStrictEqual(actual, [first, second, first]); + }); + + it("get within nesting different contexts", () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const actual = a.run(first, () => { + return [ + a.get(), + b.get(), + ...b.run(second, () => [a.get(), b.get()]), + a.get(), + b.get(), + ]; + }); + + assert.deepStrictEqual(actual, [ + first, + undefined, + first, + second, + first, + undefined, + ]); + }); + }); + + describe("wrap", () => { + it("stores initial undefined state", () => { + const ctx = new AsyncContext(); + const wrapped = AsyncContext.wrap(() => ctx.get()); + + const actual = ctx.run({ id: 1 }, () => wrapped()); + + assert.equal(actual, undefined); + }); + + it("stores current state", () => { + const ctx = new AsyncContext(); + const expected = { id: 1 }; + + const wrapped = ctx.run(expected, () => { + return AsyncContext.wrap(() => ctx.get()); + }); + + const actual = wrapped(); + + assert.equal(actual, expected); + }); + + it("wrap within nesting contexts", () => { + const ctx = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const actual = ctx.run(first, () => { + const wrapped = ctx.run(second, () => { + return AsyncContext.wrap(() => ctx.get()); + }); + return [ctx.get(), wrapped(), ctx.get()]; + }); + + assert.deepStrictEqual(actual, [first, second, first]); + }); + + it("wrap out of order", () => { + const ctx = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const firstWrap = ctx.run(first, () => { + return AsyncContext.wrap(() => ctx.get()); + }); + const secondWrap = ctx.run(second, () => { + return AsyncContext.wrap(() => ctx.get()); + }); + const actual = [firstWrap(), secondWrap(), firstWrap(), secondWrap()]; + + assert.deepStrictEqual(actual, [first, second, first, second]); + }); + }); +}); + +describe("async via promises", () => { + // beforeEach(() => { + // Promise.prototype.then = then; + // }); + // afterEach(() => { + // Promise.prototype.then = nativeThen; + // }); + + describe("run and get", () => { + it("get returns current context value", async () => { + const ctx = new AsyncContext(); + const expected = { id: 1 }; + + const actual = await ctx.run(expected, () => { + return Promise.resolve().then(() => ctx.get()); + }); + + assert.equal(actual, expected); + }); + + it("get within nesting contexts", async () => { + const ctx = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const actual = await ctx.run(first, () => { + return Promise.resolve([]) + .then((temp) => { + temp.push(ctx.get()); + return temp; + }) + .then((temp) => { + return ctx.run(second, () => { + return Promise.resolve().then(() => { + temp.push(ctx.get()); + return temp; + }); + }); + }) + .then((temp) => { + temp.push(ctx.get()); + return temp; + }); + }); + + assert.deepStrictEqual(actual, [first, second, first]); + }); + + it("get within nesting different contexts", async () => { + const a = new AsyncContext(); + const b = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const actual = await a.run(first, () => { + return Promise.resolve([]) + .then((temp) => { + temp.push(a.get(), b.get()); + return temp; + }) + .then((temp) => { + return b.run(second, () => { + return Promise.resolve().then(() => { + temp.push(a.get(), b.get()); + return temp; + }); + }); + }) + .then((temp) => { + temp.push(a.get(), b.get()); + return temp; + }); + }); + + assert.deepStrictEqual(actual, [ + first, + undefined, + first, + second, + first, + undefined, + ]); + }); + + it("get out of order", async () => { + const ctx = new AsyncContext(); + const first = { id: 1 }; + const second = { id: 2 }; + + const firstRun = ctx.run(first, () => { + return [ + sleep(10).then(() => ctx.get()), + sleep(20).then(() => ctx.get()), + sleep(30).then(() => ctx.get()), + ]; + }); + const secondRun = ctx.run(second, () => { + return [ + sleep(25).then(() => ctx.get()), + sleep(15).then(() => ctx.get()), + sleep(5).then(() => ctx.get()), + ]; + }); + + const actual = await Promise.all(firstRun.concat(secondRun)); + + assert.deepStrictEqual(actual, [ + first, + first, + first, + second, + second, + second, + ]); + }); + }); +}); diff --git a/tests/docker-native/unpolyfillable.ts b/tests/docker-native/unpolyfillable.ts new file mode 100644 index 0000000..2d0d27e --- /dev/null +++ b/tests/docker-native/unpolyfillable.ts @@ -0,0 +1,28 @@ +import * as assert from "https://deno.land/std@0.173.0/node/assert.ts"; +import { describe, it } from "https://deno.land/std@0.173.0/testing/bdd.ts"; + +describe("async via native async/await", () => { + it("works after awaited setTimeout result", async () => { + const ctx = new AsyncContext(); + const ctxRunResult = await ctx.run(1234, async () => { + assert.strictEqual(ctx.get(), 1234); + const setTimeoutResult = await ctx.run( + 2345, + () => new Promise(resolve => { + setTimeout(() => resolve(ctx.get()), 20); + }), + ); + assert.strictEqual(setTimeoutResult, 2345); + assert.strictEqual(ctx.get(), 1234); + return "final result"; + }).then(result => { + assert.strictEqual(result, "final result"); + // The code that generated the Promise has access to the 1234 value + // provided to ctx.run above, but consumers of the Promise do not + // automatically inherit it. + assert.strictEqual(ctx.get(), void 0); + return "ctx.run result 👋"; + }); + assert.strictEqual(ctxRunResult, "ctx.run result 👋"); + }); +});