Skip to content

Commit 5e704eb

Browse files
authored
Merge pull request #94 from thefrontside/cl/test-adapter-before-all
✨Add global "onetime" setup to test adapter
2 parents 4972176 + 322f58d commit 5e704eb

File tree

6 files changed

+150
-30
lines changed

6 files changed

+150
-30
lines changed

bdd/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@effectionx/bdd",
33
"exports": "./mod.ts",
4-
"version": "0.1.0",
4+
"version": "0.2.0",
55
"license": "MIT",
66
"imports": {
77
"effection": "npm:effection@^3",

bdd/mod.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export function describe(name: string, body: () => void) {
2525
describe.skip = $describe.skip;
2626
describe.only = $describe.only;
2727

28+
export function beforeAll(body: () => Operation<void>) {
29+
current?.addOnetimeSetup(body);
30+
}
31+
2832
export function beforeEach(body: () => Operation<void>) {
2933
current?.addSetup(body);
3034
}

test-adapter/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ describe("something", () => {
2525

2626
adapter.addSetup(function*() {
2727
/* do some setup. equivalent of beforeEach() */
28-
/* contexts set here will be visible in the test */*
28+
/* contexts set here will be visible in the test */
2929
});
3030

31+
adapter.addOnetimeSetup(function*() {
32+
/* do some setup that will only happen once. equivalent of beforeAll() */
33+
/* contexts set here will be visible in the test */
34+
});
35+
3136
it("does a thing", async () => {
3237
await adapter.runTest(function*() {
3338
/* ... the body of the test */

test-adapter/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@effectionx/test-adapter",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"license": "MIT",
55
"exports": "./mod.ts",
66
"imports": {

test-adapter/mod.ts

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Future, Operation, Result, Scope } from "effection";
2-
import { createScope, Err, Ok } from "effection";
2+
import { Err, Ok, run, suspend, useScope, withResolvers } from "effection";
33

44
export interface TestOperation {
55
(): Operation<void>;
@@ -23,12 +23,6 @@ export interface TestAdapter {
2323
*/
2424
readonly fullname: string;
2525

26-
/**
27-
* Every test adapter has its own Effection `Scope` which holds the resources necessary
28-
* to run this test.
29-
*/
30-
readonly scope: Scope;
31-
3226
/**
3327
* A list of this test adapter and every adapter that it descends from.
3428
*/
@@ -38,14 +32,20 @@ export interface TestAdapter {
3832
* The setup operations that will be run by this test adapter. It only includes those
3933
* setups that are associated with this adapter, not those of its ancestors.
4034
*/
41-
readonly setups: TestOperation[];
35+
readonly setup: { all: TestOperation[]; each: TestOperation[] };
4236

4337
/**
4438
* Add a setup operation to every test that is part of this adapter. In BDD integrations,
4539
* this is usually called by `beforEach()`
4640
*/
4741
addSetup(op: TestOperation): void;
4842

43+
/**
44+
* Add a setup operation that will run exactly once before any tests that are run in this
45+
* adapter. In BDD integrations, this is usually called by beforeAll()
46+
*/
47+
addOnetimeSetup(op: TestOperation): void;
48+
4949
/**
5050
* Actually run a test. This evaluates all setup operations, and then after those have completed
5151
* it runs the body of the test itself.
@@ -57,6 +57,13 @@ export interface TestAdapter {
5757
* This basically destroys the Effection `Scope` associated with this adapter.
5858
*/
5959
destroy(): Future<void>;
60+
61+
/**
62+
* Used internally to prepare adapters to run test
63+
*
64+
* @ignore
65+
*/
66+
["@@init@@"](): Operation<Scope>;
6067
}
6168

6269
export interface TestAdapterOptions {
@@ -87,16 +94,18 @@ const anonymousNames: Iterator<string, never> = (function* () {
8794
export function createTestAdapter(
8895
options: TestAdapterOptions = {},
8996
): TestAdapter {
90-
const setups: TestOperation[] = [];
97+
const setup = {
98+
all: [] as TestOperation[],
99+
each: [] as TestOperation[],
100+
};
91101
const { parent, name = anonymousNames.next().value } = options;
92102

93-
const [scope, destroy] = createScope(parent?.scope);
103+
let scope: Scope | undefined = undefined;
94104

95105
const adapter: TestAdapter = {
96106
parent,
97107
name,
98-
scope,
99-
setups,
108+
setup,
100109
get lineage() {
101110
const lineage = [adapter];
102111
for (let current = parent; current; current = current.parent) {
@@ -108,27 +117,69 @@ export function createTestAdapter(
108117
return adapter.lineage.map((adapter) => adapter.name).join(" > ");
109118
},
110119
addSetup(op) {
111-
setups.push(op);
120+
setup.each.push(op);
121+
},
122+
addOnetimeSetup(op) {
123+
setup.all.push(op);
112124
},
113125
runTest(op) {
114-
return scope.run(function* () {
115-
const allSetups = adapter.lineage.reduce(
116-
(all, adapter) => all.concat(adapter.setups),
117-
[] as TestOperation[],
118-
);
119-
try {
120-
for (const setup of allSetups) {
121-
yield* setup();
122-
}
126+
return run(() =>
127+
box(function* () {
128+
const setups = adapter.lineage.reduce(
129+
(all, adapter) => all.concat(adapter.setup.each),
130+
[] as TestOperation[],
131+
);
132+
133+
let scope = yield* adapter["@@init@@"]();
134+
135+
let test = yield* scope.spawn(function* () {
136+
for (const setup of setups) {
137+
yield* setup();
138+
}
139+
yield* op();
140+
});
141+
142+
yield* test;
143+
}())
144+
);
145+
},
146+
147+
// no-op that will be replaced once initialze
148+
destroy: () => run(function* () {}),
149+
150+
*["@@init@@"]() {
151+
if (scope) {
152+
return scope;
153+
}
154+
155+
let parentScope = parent
156+
? yield* parent["@@init@@"]()
157+
: yield* useScope();
158+
159+
let initialized = withResolvers<Scope>();
160+
161+
let task = yield* parentScope.spawn(function* () {
162+
scope = yield* useScope();
163+
for (let op of setup.all) {
123164
yield* op();
124-
return Ok<void>(void 0);
125-
} catch (error) {
126-
return Err(error as Error);
127165
}
166+
initialized.resolve(scope);
167+
yield* suspend();
128168
});
169+
170+
adapter.destroy = () => run(task.halt);
171+
172+
return yield* initialized.operation;
129173
},
130-
destroy,
131174
};
132175

133176
return adapter;
134177
}
178+
179+
function* box<T>(op: Operation<T>): Operation<Result<T>> {
180+
try {
181+
return Ok(yield* op);
182+
} catch (error) {
183+
return Err(error as Error);
184+
}
185+
}

test-adapter/test/adapter.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it } from "@std/testing/bdd";
22
import { expect } from "@std/expect";
3+
import { createContext, resource } from "effection";
34
import { createTestAdapter } from "../mod.ts";
4-
import { createContext } from "effection";
55

66
describe("TestAdapter", () => {
77
it("can run a test", async () => {
@@ -32,4 +32,64 @@ describe("TestAdapter", () => {
3232
expect(yield* count.expect()).toEqual(8);
3333
});
3434
});
35+
36+
it("has one time setup", async () => {
37+
let grandparent = createTestAdapter({ name: "grandparent" });
38+
39+
let context = createContext<string>("context", "uninitialized");
40+
41+
let sequence: string[] = [];
42+
let contexts: Record<string, string> = {};
43+
44+
grandparent.addOnetimeSetup(function* () {
45+
yield* context.set("initialized");
46+
47+
yield* resource<void>(function* (provide) {
48+
try {
49+
sequence.push("grandparent/setup:once");
50+
yield* provide();
51+
} finally {
52+
sequence.push("grandparent/teardown:once");
53+
}
54+
});
55+
});
56+
57+
grandparent.addSetup(function* () {
58+
sequence.push("grandparent/setup:each");
59+
});
60+
61+
let parent = createTestAdapter({ name: "parent", parent: grandparent });
62+
63+
parent.addOnetimeSetup(() =>
64+
resource<void>(function* (provide) {
65+
try {
66+
sequence.push("parent/setup:once");
67+
contexts["parent"] = yield* context.expect();
68+
yield* provide();
69+
} finally {
70+
sequence.push("parent/teardown:once");
71+
}
72+
})
73+
);
74+
75+
let child = createTestAdapter({ name: "child", parent });
76+
77+
await child.runTest(function* () {
78+
sequence.push("child/run");
79+
contexts["child"] = yield* context.expect();
80+
});
81+
82+
await grandparent.destroy();
83+
84+
expect(sequence).toEqual([
85+
"grandparent/setup:once",
86+
"parent/setup:once",
87+
"grandparent/setup:each",
88+
"child/run",
89+
"parent/teardown:once",
90+
"grandparent/teardown:once",
91+
]);
92+
93+
expect(contexts).toEqual({ parent: "initialized", child: "initialized" });
94+
});
3595
});

0 commit comments

Comments
 (0)