Skip to content

Commit 601a66c

Browse files
authored
New test framework, extracted from #474 (#506)
2 parents 3e6b870 + 7ef1242 commit 601a66c

18 files changed

+171
-112
lines changed

.github/workflows/ci.yml

+6-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ jobs:
4040
with:
4141
node-version: ${{ matrix.node-version }}
4242
- run: yarn --frozen-lockfile
43-
- run: yarn node --experimental-vm-modules node_modules/.bin/jest -i --ci
43+
- run: yarn prepack
44+
- run: yarn test:setupdb
45+
- run: yarn test:only --ci
4446

4547
lint:
4648
runs-on: ubuntu-latest
@@ -89,8 +91,9 @@ jobs:
8991
with:
9092
node-version: ${{ matrix.node-version }}
9193
- run: yarn --frozen-lockfile
92-
# - run: yarn lint # No need to lint altschema
93-
- run: yarn node --experimental-vm-modules node_modules/.bin/jest -i --ci
94+
- run: yarn prepack
95+
- run: yarn test:setupdb
96+
- run: yarn test:only --ci
9497

9598
database_updated:
9699
runs-on: ubuntu-latest

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/.git
12
dist
23
npm-debug.log*
34
yarn-debug.log*

__tests__/events.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import deferred, { Deferred } from "../src/deferred";
66
import { Task, TaskList, WorkerSharedOptions } from "../src/interfaces";
77
import {
88
ESCAPED_GRAPHILE_WORKER_SCHEMA,
9-
jobCount,
9+
expectJobCount,
1010
reset,
1111
sleep,
1212
sleepUntil,
@@ -132,7 +132,7 @@ test("emits the expected events", () =>
132132
expect(eventCount("worker:release")).toEqual(CONCURRENCY);
133133
expect(eventCount("worker:stop")).toEqual(CONCURRENCY);
134134
expect(eventCount("pool:release")).toEqual(1);
135-
expect(await jobCount(pgPool)).toEqual(0);
135+
await expectJobCount(pgPool, 0);
136136
} finally {
137137
Object.values(jobPromises).forEach((p) => p?.resolve());
138138
}

__tests__/forbiddenFlags.test.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,14 @@ import {
55
TaskList,
66
WorkerSharedOptions,
77
} from "../src/index";
8-
import {
9-
getJobs,
10-
reset,
11-
TEST_CONNECTION_STRING,
12-
withPgClient,
13-
withPgPool,
14-
} from "./helpers";
8+
import { getJobs, reset, withPgClient, withPgPool } from "./helpers";
159

1610
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
1711

1812
const options: WorkerSharedOptions = {};
1913

2014
test("supports the flags API", () =>
21-
withPgClient(async (pgClient) => {
15+
withPgClient(async (pgClient, { TEST_CONNECTION_STRING }) => {
2216
await reset(pgClient, options);
2317

2418
// Schedule a job

__tests__/helpers.ts

+99-24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { randomBytes } from "crypto";
12
import { EventEmitter } from "events";
3+
import {
4+
setupFakeTimers as realSetupFakeTimers,
5+
sleep,
6+
sleepUntil as baseSleepUntil,
7+
} from "jest-time-helpers";
28
import * as pg from "pg";
3-
import { parse } from "pg-connection-string";
49

510
import defer from "../src/deferred";
611
import {
@@ -16,6 +21,8 @@ import { processSharedOptions } from "../src/lib";
1621
import { _allWorkerPools } from "../src/main";
1722
import { migrate } from "../src/migrate";
1823

24+
export { DAY, HOUR, MINUTE, SECOND, sleep, WEEK } from "jest-time-helpers";
25+
1926
declare global {
2027
namespace GraphileWorker {
2128
interface Tasks {
@@ -25,39 +32,65 @@ declare global {
2532
}
2633
}
2734

28-
export {
29-
DAY,
30-
HOUR,
31-
MINUTE,
32-
SECOND,
33-
sleep,
34-
sleepUntil,
35-
WEEK,
36-
} from "jest-time-helpers";
37-
import {
38-
setupFakeTimers as realSetupFakeTimers,
39-
sleepUntil,
40-
} from "jest-time-helpers";
41-
4235
let fakeTimers: ReturnType<typeof realSetupFakeTimers> | null = null;
4336
export function setupFakeTimers() {
4437
fakeTimers = realSetupFakeTimers();
4538
return fakeTimers;
4639
}
4740

41+
export function sleepUntil(condition: () => boolean, ms?: number) {
42+
// Bump the default timeout from 2000ms for CI
43+
return baseSleepUntil(condition, ms ?? 5000);
44+
}
45+
4846
// Sometimes CI's clock can get interrupted (it is shared infra!) so this
4947
// extends the default timeout just in case.
50-
jest.setTimeout(15000);
48+
jest.setTimeout(20000);
5149

5250
// process.env.GRAPHILE_LOGGER_DEBUG = "1";
5351

54-
export const TEST_CONNECTION_STRING =
55-
process.env.TEST_CONNECTION_STRING || "postgres:///graphile_worker_test";
52+
async function createTestDatabase() {
53+
const id = randomBytes(8).toString("hex");
54+
const PGDATABASE = `graphile_worker_test_${id}`;
55+
{
56+
const client = new pg.Client({ connectionString: `postgres:///template1` });
57+
await client.connect();
58+
await client.query(
59+
`create database ${pg.Client.prototype.escapeIdentifier(
60+
PGDATABASE,
61+
)} with template = graphile_worker_testtemplate;`,
62+
);
63+
await client.end();
64+
}
65+
const TEST_CONNECTION_STRING = `postgres:///${PGDATABASE}`;
66+
const PGHOST = process.env.PGHOST;
67+
async function release() {
68+
const client = new pg.Client({ connectionString: `postgres:///template1` });
69+
await client.connect();
70+
await client.query(
71+
`drop database ${pg.Client.prototype.escapeIdentifier(PGDATABASE)};`,
72+
);
73+
await client.end();
74+
}
5675

57-
const parsed = parse(TEST_CONNECTION_STRING);
76+
return {
77+
TEST_CONNECTION_STRING,
78+
PGHOST,
79+
PGDATABASE,
80+
release,
81+
};
82+
}
83+
84+
export let databaseDetails: Awaited<
85+
ReturnType<typeof createTestDatabase>
86+
> | null = null;
5887

59-
export const PGHOST = parsed.host || process.env.PGHOST;
60-
export const PGDATABASE = parsed.database || undefined;
88+
beforeAll(async () => {
89+
databaseDetails = await createTestDatabase();
90+
});
91+
afterAll(async () => {
92+
databaseDetails?.release();
93+
});
6194

6295
export const GRAPHILE_WORKER_SCHEMA =
6396
process.env.GRAPHILE_WORKER_SCHEMA || "graphile_worker";
@@ -67,6 +100,7 @@ export const ESCAPED_GRAPHILE_WORKER_SCHEMA =
67100
export async function withPgPool<T>(
68101
cb: (pool: pg.Pool) => Promise<T>,
69102
): Promise<T> {
103+
const { TEST_CONNECTION_STRING } = databaseDetails!;
70104
const pool = new pg.Pool({
71105
connectionString: TEST_CONNECTION_STRING,
72106
max: 100,
@@ -85,12 +119,17 @@ afterEach(() => {
85119
});
86120

87121
export async function withPgClient<T>(
88-
cb: (client: pg.PoolClient) => Promise<T>,
122+
cb: (
123+
client: pg.PoolClient,
124+
extra: {
125+
TEST_CONNECTION_STRING: string;
126+
},
127+
) => Promise<T>,
89128
): Promise<T> {
90129
return withPgPool(async (pool) => {
91130
const client = await pool.connect();
92131
try {
93-
return await cb(client);
132+
return await cb(client, databaseDetails!);
94133
} finally {
95134
client.release();
96135
}
@@ -156,7 +195,18 @@ async function _reset(
156195
}
157196
}
158197

159-
export async function jobCount(
198+
/**
199+
* Counts the number of jobs currently in DB.
200+
*
201+
* If you have a pool, you may hit race conditions with this method, instead
202+
* use `expectJobCount()` which will try multiple times to give time for
203+
* multiple clients to synchronize.
204+
*/
205+
export async function jobCount(pgClient: pg.PoolClient): Promise<number> {
206+
return _jobCount(pgClient);
207+
}
208+
209+
async function _jobCount(
160210
pgPoolOrClient: pg.Pool | pg.PoolClient,
161211
): Promise<number> {
162212
const {
@@ -327,3 +377,28 @@ export function withOptions<T>(
327377
}),
328378
);
329379
}
380+
381+
/**
382+
* Wait for the job count to match the expected count, handles
383+
* issues with different connections to the database not
384+
* reflecting the same data by retrying.
385+
*/
386+
export async function expectJobCount(
387+
// NOTE: if you have a pgClient then you shouldn't need to
388+
// use this - just call `jobCount()` directly since you're
389+
// in the same client
390+
pool: pg.Pool,
391+
expectedCount: number,
392+
) {
393+
let count: number = Infinity;
394+
for (let i = 0; i < 8; i++) {
395+
if (i > 0) {
396+
await sleep(i * 50);
397+
}
398+
count = await _jobCount(pool);
399+
if (count === expectedCount) {
400+
break;
401+
}
402+
}
403+
expect(count).toEqual(expectedCount);
404+
}

__tests__/jobsView.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ import {
33
ESCAPED_GRAPHILE_WORKER_SCHEMA,
44
makeSelectionOfJobs,
55
reset,
6-
TEST_CONNECTION_STRING,
76
withPgClient,
87
} from "./helpers";
98

109
const options: WorkerSharedOptions = {};
1110

1211
test("jobs view renders jobs", () =>
13-
withPgClient(async (pgClient) => {
12+
withPgClient(async (pgClient, { TEST_CONNECTION_STRING }) => {
1413
await reset(pgClient, options);
1514

1615
const utils = await makeWorkerUtils({

__tests__/main.runTaskList.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Task, TaskList, WorkerSharedOptions } from "../src/interfaces";
66
import { runTaskList } from "../src/main";
77
import {
88
ESCAPED_GRAPHILE_WORKER_SCHEMA,
9-
jobCount,
9+
expectJobCount,
1010
reset,
1111
sleep,
1212
sleepUntil,
@@ -70,7 +70,7 @@ test("main will execute jobs as they come up, and exits cleanly", () =>
7070
await sleep(1);
7171
expect(finished).toBeTruthy();
7272
await workerPool.promise;
73-
expect(await jobCount(pgPool)).toEqual(0);
73+
await expectJobCount(pgPool, 0);
7474
expect(process.listeners("SIGTERM")).toHaveLength(0);
7575
} finally {
7676
Object.values(jobPromises).forEach((p) => p?.resolve());

__tests__/runner.helpers.getTaskName.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { Pool, PoolClient } from "pg";
33
import { DbJobSpec, Runner, RunnerOptions } from "../src/interfaces";
44
import { run } from "../src/runner";
55
import {
6+
databaseDetails,
67
ESCAPED_GRAPHILE_WORKER_SCHEMA,
78
sleepUntil,
8-
TEST_CONNECTION_STRING,
99
withPgClient,
1010
} from "./helpers";
1111

@@ -15,7 +15,7 @@ let runner: Runner | null = null;
1515
const JOB_COUNT = 10;
1616
beforeAll(() => {
1717
pgPool = new Pool({
18-
connectionString: TEST_CONNECTION_STRING,
18+
connectionString: databaseDetails!.TEST_CONNECTION_STRING,
1919
max: JOB_COUNT * 2 + 5,
2020
});
2121
});

0 commit comments

Comments
 (0)