Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getVolumeNames,
waitForDockerEvent,
} from "../utils/test-helper";
import { HealthCheckWaitStrategy } from "../wait-strategies/health-check-wait-strategy";
import { HostPortWaitStrategy } from "../wait-strategies/host-port-wait-strategy";
import { Wait } from "../wait-strategies/wait";
import { DockerComposeEnvironment } from "./docker-compose-environment";

Expand Down Expand Up @@ -97,6 +99,26 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => {
await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container"));
});

it("should use wait strategy Wait.forHealthCheck() if healthcheck is defined in service", async () => {
await using startedEnvironment = await new DockerComposeEnvironment(
fixtures,
"docker-compose-with-healthcheck.yml"
).up();

await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container"));

const waitStrategy = startedEnvironment.getContainer("container-1").getWaitStrategy();
expect(waitStrategy).toBeInstanceOf(HealthCheckWaitStrategy);
});
it("should use wait strategy Wait.forListeningPorts() if healthcheck is NOT defined in service", async () => {
await using startedEnvironment = await new DockerComposeEnvironment(fixtures, "docker-compose-with-name.yml").up();

await checkEnvironmentContainerIsHealthy(startedEnvironment, "custom_container_name");

const waitStrategy = startedEnvironment.getContainer("custom_container_name").getWaitStrategy();
expect(waitStrategy).toBeInstanceOf(HostPortWaitStrategy);
});

it("should support log message wait strategy", async () => {
await using startedEnvironment = await new DockerComposeEnvironment(fixtures, "docker-compose.yml")
.withWaitStrategy(await composeContainerName("container"), Wait.forLogMessage("Listening on port 8080"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContainerInfo } from "dockerode";
import { ContainerInfo, ContainerInspectInfo, HealthConfig } from "dockerode";
import { containerLog, log, RandomUuid, Uuid } from "../common";
import { ComposeOptions, getContainerRuntimeClient, parseComposeContainerName } from "../container-runtime";
import { StartedGenericContainer } from "../generic-container/started-generic-container";
Expand All @@ -7,6 +7,7 @@ import { Environment } from "../types";
import { BoundPorts } from "../utils/bound-ports";
import { mapInspectResult } from "../utils/map-inspect-result";
import { ImagePullPolicy, PullPolicy } from "../utils/pull-policy";
import { NullWaitStrategy } from "../wait-strategies/null-wait-strategy";
import { Wait } from "../wait-strategies/wait";
import { waitForContainer } from "../wait-strategies/wait-for-container";
import { WaitStrategy } from "../wait-strategies/wait-strategy";
Expand All @@ -23,7 +24,7 @@ export class DockerComposeEnvironment {
private profiles: string[] = [];
private environment: Environment = {};
private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
private defaultWaitStrategy: WaitStrategy = Wait.forListeningPorts();
private defaultWaitStrategy: WaitStrategy = new NullWaitStrategy();
private waitStrategy: { [containerName: string]: WaitStrategy } = {};
private startupTimeoutMs?: number;
private clientOptions: Partial<ComposeOptions> = {};
Expand Down Expand Up @@ -159,9 +160,7 @@ export class DockerComposeEnvironment {
const inspectResult = await client.container.inspect(container);
const mappedInspectResult = mapInspectResult(inspectResult);
const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult);
const waitStrategy = this.waitStrategy[containerName]
? this.waitStrategy[containerName]
: this.defaultWaitStrategy;
const waitStrategy = this.selectWaitStrategy(containerName, inspectResult);
if (this.startupTimeoutMs !== undefined) {
waitStrategy.withStartupTimeout(this.startupTimeoutMs);
}
Expand Down Expand Up @@ -207,4 +206,22 @@ export class DockerComposeEnvironment {
environment: this.environment,
});
}

private selectWaitStrategy(containerName: string, inspectResult: ContainerInspectInfo) {
const containerWaitStrategy = this.waitStrategy[containerName]
? this.waitStrategy[containerName]
: this.defaultWaitStrategy;
if (!(containerWaitStrategy instanceof NullWaitStrategy)) return containerWaitStrategy;
const healthcheck = (
inspectResult as ContainerInspectInfo & {
Config: ContainerInspectInfo["Config"] & {
Healthcheck: HealthConfig | undefined;
};
}
).Config.Healthcheck;
if (healthcheck?.Test) {
return Wait.forHealthCheck();
}
return Wait.forListeningPorts();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export class AbstractStartedContainer implements StartedTestContainer {
return this.startedTestContainer.getIpAddress(networkName);
}

public getWaitStrategy() {
return this.startedTestContainer.getWaitStrategy();
}

public async copyFilesToContainer(filesToCopy: FileToCopy[]): Promise<void> {
return this.startedTestContainer.copyFilesToContainer(filesToCopy);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import path from "path";
import { HealthCheckWaitStrategy } from "../wait-strategies/health-check-wait-strategy";
import { HostPortWaitStrategy } from "../wait-strategies/host-port-wait-strategy";
import { Wait } from "../wait-strategies/wait";
import { GenericContainer } from "./generic-container";

const fixtures = path.resolve(__dirname, "..", "..", "fixtures", "docker");

describe("GenericContainer wait strategy", { timeout: 180_000 }, () => {
it("should use Wait.forListeningPorts if healthcheck is not defined in DOCKERFILE", async () => {
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.start();
expect(container.getWaitStrategy()).toBeInstanceOf(HostPortWaitStrategy);
});
it("should use Wait.forHealthCheck if withHealthCheck() explicitly called", async () => {
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.withHealthCheck({
test: ["CMD-SHELL", "echo 'started' && exit 0"],
})
.start();
expect(container.getWaitStrategy()).toBeInstanceOf(HealthCheckWaitStrategy);
});
it("should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE", async () => {
const context = path.resolve(fixtures, "docker-with-health-check");
const genericContainer = await GenericContainer.fromDockerfile(context).build();
await using startedContainer = await genericContainer.start();
expect(startedContainer.getWaitStrategy()).toBeInstanceOf(HealthCheckWaitStrategy);

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 22.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 22.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 22.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 22.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 24.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 24.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 24.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 24.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 20.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 20.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 20.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48

Check failure on line 29 in packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts

View workflow job for this annotation

GitHub Actions / Tests (testcontainers, 20.x, podman) / Run

packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts > GenericContainer wait strategy > should use Wait.forHealthCheck if healthcheck is defined in DOCKERFILE

AssertionError: expected HostPortWaitStrategy{ …(2) } to be an instance of HealthCheckWaitStrategy ❯ packages/testcontainers/src/generic-container/generic-container-wait-strategy.test.ts:29:48
});
it("should use same WaitStrategy if it's explicitly defined in withWaitStrategy() even if image defines healthcheck", async () => {
const context = path.resolve(fixtures, "docker-with-health-check");
const genericContainer = await GenericContainer.fromDockerfile(context).build();
await using container = await genericContainer
.withExposedPorts(8080)
.withWaitStrategy(Wait.forListeningPorts())
.start();
expect(container.getWaitStrategy()).toBeInstanceOf(HostPortWaitStrategy);
});
it("should use same WaitStrategy if it's explicitly defined in withWaitStrategy() even if withHealthCheck() is called", async () => {
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withExposedPorts(8080)
.withHealthCheck({
test: ["CMD-SHELL", "echo 'started' && exit 0"],
})
.withWaitStrategy(Wait.forListeningPorts())
.start();
expect(container.getWaitStrategy()).toBeInstanceOf(HostPortWaitStrategy);
});
});
23 changes: 21 additions & 2 deletions packages/testcontainers/src/generic-container/generic-container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import archiver from "archiver";
import AsyncLock from "async-lock";
import { Container, ContainerCreateOptions, HostConfig } from "dockerode";
import { Container, ContainerCreateOptions, HealthConfig, HostConfig, ImageInspectInfo } from "dockerode";
import { Readable } from "stream";
import { containerLog, hash, log, toNanos } from "../common";
import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime";
Expand Down Expand Up @@ -29,6 +29,7 @@ import { createLabels, LABEL_TESTCONTAINERS_CONTAINER_HASH, LABEL_TESTCONTAINERS
import { mapInspectResult } from "../utils/map-inspect-result";
import { getContainerPort, getProtocol, hasHostBinding, PortWithOptionalBinding } from "../utils/port";
import { ImagePullPolicy, PullPolicy } from "../utils/pull-policy";
import { NullWaitStrategy } from "../wait-strategies/null-wait-strategy";
import { Wait } from "../wait-strategies/wait";
import { waitForContainer } from "../wait-strategies/wait-for-container";
import { WaitStrategy } from "../wait-strategies/wait-strategy";
Expand All @@ -48,7 +49,7 @@ export class GenericContainer implements TestContainer {

protected imageName: ImageName;
protected startupTimeoutMs?: number;
protected waitStrategy: WaitStrategy = Wait.forListeningPorts();
protected waitStrategy: WaitStrategy = new NullWaitStrategy();
protected environment: Record<string, string> = {};
protected exposedPorts: PortWithOptionalBinding[] = [];
protected reuse = false;
Expand Down Expand Up @@ -94,6 +95,8 @@ export class GenericContainer implements TestContainer {
await this.beforeContainerCreated();
}

this.waitStrategy = await this.selectWaitStrategy(client);

if (!this.isHelperContainer() && PortForwarderInstance.isRunning()) {
const portForwarder = await PortForwarderInstance.getInstance();
this.hostConfig.ExtraHosts = [
Expand All @@ -117,6 +120,22 @@ export class GenericContainer implements TestContainer {
return this.startContainer(client);
}

private async selectWaitStrategy(client: ContainerRuntimeClient): Promise<WaitStrategy> {
if (!(this.waitStrategy instanceof NullWaitStrategy)) return this.waitStrategy;
if (this.healthCheck) {
return Wait.forHealthCheck();
}
const imageInfo = (await client.image.inspect(this.imageName)) as ImageInspectInfo & {
Config: ImageInspectInfo["Config"] & {
Healthcheck: HealthConfig | undefined;
};
};
if (imageInfo.Config.Healthcheck?.Test) {
return Wait.forHealthCheck();
}
return Wait.forListeningPorts();
}

private async reuseOrStartContainer(client: ContainerRuntimeClient) {
const containerHash = hash(JSON.stringify(this.createOpts));
this.createOpts.Labels = { ...this.createOpts.Labels, [LABEL_TESTCONTAINERS_CONTAINER_HASH]: containerHash };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ export class StartedGenericContainer implements StartedTestContainer {
return this.getNetworkSettings()[networkName].ipAddress;
}

public getWaitStrategy() {
return this.waitStrategy;
}

private getNetworkSettings() {
return Object.entries(this.inspectResult.NetworkSettings.Networks)
.map(([networkName, network]) => ({
Expand Down
1 change: 1 addition & 0 deletions packages/testcontainers/src/test-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export interface StartedTestContainer extends AsyncDisposable {
getNetworkNames(): string[];
getNetworkId(networkName: string): string;
getIpAddress(networkName: string): string;
getWaitStrategy(): WaitStrategy;
copyArchiveFromContainer(path: string): Promise<NodeJS.ReadableStream>;
copyArchiveToContainer(tar: Readable, target?: string): Promise<void>;
copyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AbstractWaitStrategy } from "./wait-strategy";

export class NullWaitStrategy extends AbstractWaitStrategy {
public override waitUntilReady(): Promise<void> {
return Promise.resolve();
}
}
Loading