Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export class DockerComposeEnvironment {
boundPorts,
containerName,
waitStrategy
// not sure how to control 'remove' option for the whole compose stack, will use default value here (true)
);
})
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,27 @@ describe("GenericContainer reuse", { timeout: 180_000 }, () => {
await container2.stop();
});

it("should reuse container when an existing reusable container created using withRemove(false) has stopped", async () => {
const name = `there_can_only_be_one_${randomUuid()}`;
const container1 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(name)
.withExposedPorts(8080)
.withReuse()
.withRemove(false)
.start();
await container1.stop({ timeout: 10000 });

const container2 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(name)
.withExposedPorts(8080)
.withReuse()
.start();
await checkContainerIsHealthy(container2);

expect(container1.getId()).toBe(container2.getId());
await container2.stop();
});

it("should keep the labels passed in when a new reusable container is created", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(`there_can_only_be_one_${randomUuid()}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
checkContainerIsHealthy,
getDockerEventStream,
getRunningContainerNames,
getStoppedContainerNames,
waitForDockerEvent,
} from "../utils/test-helper";
import { GenericContainer } from "./generic-container";
Expand Down Expand Up @@ -503,6 +504,45 @@
expect(await getRunningContainerNames()).not.toContain(container.getName());
});

it("should stop the container and NOT remove it if withRemove(false) set", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(`container-${new RandomUuid().nextUuid()}`)
.withRemove(false)
.start();

await container.stop();

const containerName = container.getName()?.slice(1); // remove first '/'
expect(await getStoppedContainerNames()).toContain(containerName);

Check failure on line 516 in packages/testcontainers/src/generic-container/generic-container.test.ts

View workflow job for this annotation

GitHub Actions / Docker tests (testcontainers, 20.x) / Runner (ubuntu-22.04) / Node (20.x) / Runtime (docker) / Workspace (testcontainers)

AssertionError: expected [ …(3) ] to include 'container-825caf940651' ❯ packages/testcontainers/src/generic-container/generic-container.test.ts:516:46

Check failure on line 516 in packages/testcontainers/src/generic-container/generic-container.test.ts

View workflow job for this annotation

GitHub Actions / Docker tests (testcontainers, 22.x) / Runner (ubuntu-22.04) / Node (22.x) / Runtime (docker) / Workspace (testcontainers)

AssertionError: expected [ …(3) ] to include 'container-53388233049b' ❯ packages/testcontainers/src/generic-container/generic-container.test.ts:516:46

Check failure on line 516 in packages/testcontainers/src/generic-container/generic-container.test.ts

View workflow job for this annotation

GitHub Actions / Docker tests (testcontainers, 18.x) / Runner (ubuntu-22.04) / Node (18.x) / Runtime (docker) / Workspace (testcontainers)

AssertionError: expected [ …(3) ] to include 'container-d33cc6900f35' ❯ packages/testcontainers/src/generic-container/generic-container.test.ts:516:46

// ryuk will clean up this container after test anyway
// unless env var TESTCONTAINERS_RYUK_DISABLED=true is set
});

it("should stop the container and remove it if withRemove(true) set", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(`container-${new RandomUuid().nextUuid()}`)
.withRemove(true)
.start();

await container.stop();

const containerName = container.getName()?.slice(1); // remove first '/'
expect(await getStoppedContainerNames()).not.toContain(containerName);
});

it("should stop the container and remove it when explicitly overriding withRemove(false)", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(`container-${new RandomUuid().nextUuid()}`)
.withRemove(false)
.start();

await container.stop({ remove: true }); // remove: true here should override withRemove(false)

const containerName = container.getName()?.slice(1); // remove first '/'
expect(await getStoppedContainerNames()).not.toContain(containerName);
});

it("should stop the container idempotently", async () => {
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withName(`container-${new RandomUuid().nextUuid()}`)
Expand Down
12 changes: 10 additions & 2 deletions packages/testcontainers/src/generic-container/generic-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class GenericContainer implements TestContainer {
protected environment: Record<string, string> = {};
protected exposedPorts: PortWithOptionalBinding[] = [];
protected reuse = false;
protected remove = true;
protected networkMode?: string;
protected networkAliases: string[] = [];
protected pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
Expand Down Expand Up @@ -158,7 +159,8 @@ export class GenericContainer implements TestContainer {
inspectResult,
boundPorts,
inspectResult.Name,
this.waitStrategy
this.waitStrategy,
this.remove
);
}

Expand Down Expand Up @@ -222,7 +224,8 @@ export class GenericContainer implements TestContainer {
inspectResult,
boundPorts,
inspectResult.Name,
this.waitStrategy
this.waitStrategy,
this.remove
);

if (this.containerStarted) {
Expand Down Expand Up @@ -433,6 +436,11 @@ export class GenericContainer implements TestContainer {
return this;
}

public withRemove(remove: boolean): this {
this.remove = remove;
return this;
}

public withPullPolicy(pullPolicy: ImagePullPolicy): this {
this.pullPolicy = pullPolicy;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import { StoppedGenericContainer } from "./stopped-generic-container";

export class StartedGenericContainer implements StartedTestContainer {
private stoppedContainer?: StoppedTestContainer;
private stopContainerLock = new AsyncLock();
private readonly stopContainerLock = new AsyncLock();

constructor(
private readonly container: Dockerode.Container,
private readonly host: string,
private inspectResult: ContainerInspectInfo,
private boundPorts: BoundPorts,
private readonly name: string,
private readonly waitStrategy: WaitStrategy
private readonly waitStrategy: WaitStrategy,
private readonly removeOnStop: boolean = true
) {}

protected containerIsStopping?(): Promise<void>;
Expand Down Expand Up @@ -105,9 +106,12 @@ export class StartedGenericContainer implements StartedTestContainer {
await this.containerIsStopping();
}

const resolvedOptions: StopOptions = { remove: true, timeout: 0, removeVolumes: true, ...options };
// use this.removeOnStop value here (true by default)
// but allow to override it explicitly from options argument
const resolvedOptions: StopOptions = { remove: this.removeOnStop, timeout: 0, removeVolumes: true, ...options };
await client.container.stop(this.container, { timeout: resolvedOptions.timeout });
if (resolvedOptions.remove) {
log.info("StopOptions.remove=true, removing container.");
await client.container.remove(this.container, { removeVolumes: resolvedOptions.removeVolumes });
}
log.info(`Stopped container`, { containerId: this.container.id });
Expand Down
10 changes: 9 additions & 1 deletion packages/testcontainers/src/utils/test-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ export const getDockerEventStream = async (opts: GetEventsOptions = {}): Promise
};

export const getRunningContainerNames = async (): Promise<string[]> => {
return getContainerNames(false);
};

export const getStoppedContainerNames = async (): Promise<string[]> => {
return getContainerNames(true, { status: ["paused", "exited"] });
};

const getContainerNames = async (all: boolean, filters?: string | { [key: string]: string[] }): Promise<string[]> => {
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
const containers = await dockerode.listContainers();
const containers = await dockerode.listContainers({ all, filters });
return containers
.map((container) => container.Names)
.reduce((result, containerNames) => [...result, ...containerNames], [])
Expand Down
Loading