Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion internal-packages/testcontainers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"tinyexec": "^0.3.0"
},
"scripts": {
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "vitest --sequence.concurrent=false --no-file-parallelism"
}
}
170 changes: 121 additions & 49 deletions internal-packages/testcontainers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,44 @@ const prismaFromContainer = async (
}
};

export const postgresTest = test.extend<PostgresTestContext>({
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
});
const CONTAINER_WARMUP_TIMEOUT_MS = 300_000;

type WarmableTestApi = {
beforeAll: (fn: (context: any) => Promise<void>, timeout?: number) => void;
};

const withWarmup = <T extends WarmableTestApi>(
api: T,
warmUp: (context: any) => Promise<void>
): T => {
const register = () => {
api.beforeAll(warmUp, CONTAINER_WARMUP_TIMEOUT_MS);
};

return new Proxy(api, {
apply(target, thisArg, args) {
register();
return Reflect.apply(target as unknown as (...a: unknown[]) => unknown, thisArg, args);
},
get(target, prop, receiver) {
if (prop !== "then") {
// awaiting the module is not use
register();
}
return Reflect.get(target, prop, receiver);
},
}) as T;
Comment thread
nicktrn marked this conversation as resolved.
};

export const postgresTest = withWarmup(
test.extend<PostgresTestContext>({
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
}),
async () => {
await getWorkerPostgresContainer();
}
);

type HeteroPostgresTestContext = {
// PG14 (legacy / control-plane DB analog)
Expand Down Expand Up @@ -609,11 +643,16 @@ type RedisTestContext = {

// Worker-scoped redis (boots once, FLUSHALL between tests). Use isolatedRedisTest for tests that run
// background redis work (redis-worker Workers, BatchQueue) past the test body - see its note + README.
export const redisTest = test.extend<RedisTestContext>({
redisContainer: [bootWorkerRedis, { scope: "worker" }],
resetRedis: [flushRedis, { auto: true }],
redisOptions,
});
export const redisTest = withWarmup(
test.extend<RedisTestContext>({
redisContainer: [bootWorkerRedis, { scope: "worker" }],
resetRedis: [flushRedis, { auto: true }],
redisOptions,
}),
async ({ redisContainer }) => {
void redisContainer;
}
Comment thread
nicktrn marked this conversation as resolved.
);

// Per-test redis for tests with background redis work (redis-worker Workers, BatchQueue) that can
// outlive the test body - a shared redis would let leaked work hit a closed connection / next test
Expand Down Expand Up @@ -723,11 +762,16 @@ const scopedClickhouseClient = async (
}
};

export const clickhouseTest = test.extend<ClickhouseTestContext>({
clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }],
resetClickhouse: [truncateClickhouseFixture, { auto: true }],
clickhouseClient: scopedClickhouseClient,
});
export const clickhouseTest = withWarmup(
test.extend<ClickhouseTestContext>({
clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }],
resetClickhouse: [truncateClickhouseFixture, { auto: true }],
clickhouseClient: scopedClickhouseClient,
}),
async ({ clickhouseContainer }) => {
void clickhouseContainer;
}
);

// NOTE: per-test containers (not worker-scoped) - the replication package does logical replication
// (slots/publications/REPLICA IDENTITY), which doesn't play nicely with a shared container +
Expand Down Expand Up @@ -755,17 +799,24 @@ type ContainerTestContext = {
// The workhorse fixture (~36 files). Postgres (template-clone), Redis (FLUSHALL) and ClickHouse
// (truncate) all boot once per worker - no per-test container boots. Use containerTestWithIsolatedRedis
// for tests that run background redis work (BatchQueue, redis-worker Workers) past the test body.
export const containerTest = test.extend<ContainerTestContext>({
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
schemaOnlyPrisma: schemaOnlyPrismaFixture,
redisContainer: [bootWorkerRedis, { scope: "worker" }],
resetRedis: [flushRedis, { auto: true }],
redisOptions,
clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }],
resetClickhouse: [truncateClickhouseFixture, { auto: true }],
clickhouseClient: scopedClickhouseClient,
});
export const containerTest = withWarmup(
test.extend<ContainerTestContext>({
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
schemaOnlyPrisma: schemaOnlyPrismaFixture,
redisContainer: [bootWorkerRedis, { scope: "worker" }],
resetRedis: [flushRedis, { auto: true }],
redisOptions,
clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }],
resetClickhouse: [truncateClickhouseFixture, { auto: true }],
clickhouseClient: scopedClickhouseClient,
}),
async ({ redisContainer, clickhouseContainer }) => {
void redisContainer;
void clickhouseContainer;
await getWorkerPostgresContainer();
}
Comment thread
nicktrn marked this conversation as resolved.
);

type ContainerWithIsolatedRedisContext = {
network: StartedNetwork;
Expand All @@ -780,16 +831,22 @@ type ContainerWithIsolatedRedisContext = {

// Same as containerTest but Redis is PER-TEST - for tests whose background redis work (BatchQueue,
// Workers) outlives the test body and would otherwise hit a closed/shared connection.
export const containerTestWithIsolatedRedis = test.extend<ContainerWithIsolatedRedisContext>({
network,
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
redisContainer,
redisOptions,
clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }],
resetClickhouse: [truncateClickhouseFixture, { auto: true }],
clickhouseClient: scopedClickhouseClient,
});
export const containerTestWithIsolatedRedis = withWarmup(
test.extend<ContainerWithIsolatedRedisContext>({
network,
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
redisContainer,
redisOptions,
clickhouseContainer: [bootWorkerClickhouse, { scope: "worker" }],
resetClickhouse: [truncateClickhouseFixture, { auto: true }],
clickhouseClient: scopedClickhouseClient,
}),
async ({ clickhouseContainer }) => {
void clickhouseContainer;
await getWorkerPostgresContainer();
}
);

type ContainerWithIsolatedRedisNoClickhouseContext = {
network: StartedNetwork;
Expand All @@ -801,14 +858,18 @@ type ContainerWithIsolatedRedisNoClickhouseContext = {

// Like containerTestWithIsolatedRedis (template-clone Postgres + per-test Redis) but with no
// ClickHouse - for suites that touch Postgres + Redis but never ClickHouse, avoiding its boot+migrate.
export const containerTestWithIsolatedRedisNoClickhouse =
export const containerTestWithIsolatedRedisNoClickhouse = withWarmup(
test.extend<ContainerWithIsolatedRedisNoClickhouseContext>({
network,
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
redisContainer,
redisOptions,
});
}),
async () => {
await getWorkerPostgresContainer();
}
);

// For tests that exercise the Postgres -> ClickHouse logical-replication pipeline (WAL slots,
// publications, REPLICA IDENTITY). These need a dedicated Postgres per test - the worker-scoped +
Expand Down Expand Up @@ -887,11 +948,16 @@ type MinioTestContext = {
minioConfig: MinIOConnectionConfig;
};

export const minioTest = test.extend<MinioTestContext>({
minioContainer: [bootWorkerMinio, { scope: "worker" }],
resetMinio: [minioReset, { auto: true }],
minioConfig,
});
export const minioTest = withWarmup(
test.extend<MinioTestContext>({
minioContainer: [bootWorkerMinio, { scope: "worker" }],
resetMinio: [minioReset, { auto: true }],
minioConfig,
}),
async ({ minioContainer }) => {
void minioContainer;
}
);

type PostgresAndMinioTestContext = {
postgresContainer: StartedPostgreSqlContainer;
Expand All @@ -901,10 +967,16 @@ type PostgresAndMinioTestContext = {
minioConfig: MinIOConnectionConfig;
};

export const postgresAndMinioTest = test.extend<PostgresAndMinioTestContext>({
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
minioContainer: [bootWorkerMinio, { scope: "worker" }],
resetMinio: [minioReset, { auto: true }],
minioConfig,
});
export const postgresAndMinioTest = withWarmup(
test.extend<PostgresAndMinioTestContext>({
postgresContainer: clonedPostgresContainer,
prisma: prismaFromContainer,
minioContainer: [bootWorkerMinio, { scope: "worker" }],
resetMinio: [minioReset, { auto: true }],
minioConfig,
}),
async ({ minioContainer }) => {
void minioContainer;
await getWorkerPostgresContainer();
}
);
33 changes: 33 additions & 0 deletions internal-packages/testcontainers/src/warmup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, vi } from "vitest";
import { clickhouseTest, containerTest } from "./index";

vi.setConfig({ testTimeout: 10_000 });

describe.skip("a skipped suite that touches the fixture first", () => {
containerTest("never runs", async ({ prisma }) => {
expect(prisma).toBeDefined();
});
});

describe("container fixture warmup", () => {
containerTest("the first test is not billed for the container boot", async ({ prisma }) => {
const rows = await prisma.$queryRawUnsafe<Array<{ ok: number }>>("SELECT 1 as ok");

expect(rows[0]?.ok).toBe(1);
});

containerTest("later tests still get a working fixture", async ({ prisma }) => {
const rows = await prisma.$queryRawUnsafe<Array<{ ok: number }>>("SELECT 2 as ok");

expect(rows[0]?.ok).toBe(2);
});
});

describe("worker-scoped fixtures are warmed too", () => {
clickhouseTest("clickhouse is up before the first test", async ({ clickhouseClient }) => {
const rs = await clickhouseClient.query({ query: "SELECT 1 AS ok", format: "JSONEachRow" });
const rows = await rs.json<{ ok: number }>();

expect(rows[0]?.ok).toBe(1);
});
});