Skip to content

Commit 6465ab0

Browse files
committed
add withRemove and tests
1 parent a00bbb6 commit 6465ab0

File tree

6 files changed

+88
-6
lines changed

6 files changed

+88
-6
lines changed

packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export class DockerComposeEnvironment {
169169
boundPorts,
170170
containerName,
171171
waitStrategy
172+
// not sure how to control 'remove' option for the whole compose stack, will use default value here (true)
172173
);
173174
})
174175
)

packages/testcontainers/src/generic-container/generic-container-reuse.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,27 @@ describe("GenericContainer reuse", { timeout: 180_000 }, () => {
153153
await container2.stop();
154154
});
155155

156+
it("should reuse container when an existing reusable container created using withRemove(false) has stopped", async () => {
157+
const name = `there_can_only_be_one_${randomUuid()}`;
158+
const container1 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
159+
.withName(name)
160+
.withExposedPorts(8080)
161+
.withReuse()
162+
.withRemove(false)
163+
.start();
164+
await container1.stop({ timeout: 10000 });
165+
166+
const container2 = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
167+
.withName(name)
168+
.withExposedPorts(8080)
169+
.withReuse()
170+
.start();
171+
await checkContainerIsHealthy(container2);
172+
173+
expect(container1.getId()).toBe(container2.getId());
174+
await container2.stop();
175+
});
176+
156177
it("should keep the labels passed in when a new reusable container is created", async () => {
157178
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
158179
.withName(`there_can_only_be_one_${randomUuid()}`)

packages/testcontainers/src/generic-container/generic-container.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
checkContainerIsHealthy,
88
getDockerEventStream,
99
getRunningContainerNames,
10+
getStoppedContainerNames,
1011
waitForDockerEvent,
1112
} from "../utils/test-helper";
1213
import { GenericContainer } from "./generic-container";
@@ -503,6 +504,45 @@ describe("GenericContainer", { timeout: 180_000 }, () => {
503504
expect(await getRunningContainerNames()).not.toContain(container.getName());
504505
});
505506

507+
it("should stop the container and NOT remove it if withRemove(false) set", async () => {
508+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
509+
.withName(`container-${new RandomUuid().nextUuid()}`)
510+
.withRemove(false)
511+
.start();
512+
513+
await container.stop();
514+
515+
const containerName = container.getName()?.slice(1); // remove first '/'
516+
expect(await getStoppedContainerNames()).toContain(containerName);
517+
518+
// ryuk will clean up this container after test anyway
519+
// unless env var TESTCONTAINERS_RYUK_DISABLED=true is set
520+
});
521+
522+
it("should stop the container and remove it if withRemove(true) set", async () => {
523+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
524+
.withName(`container-${new RandomUuid().nextUuid()}`)
525+
.withRemove(true)
526+
.start();
527+
528+
await container.stop();
529+
530+
const containerName = container.getName()?.slice(1); // remove first '/'
531+
expect(await getStoppedContainerNames()).not.toContain(containerName);
532+
});
533+
534+
it("should stop the container and remove it when explicitly overriding withRemove(false)", async () => {
535+
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
536+
.withName(`container-${new RandomUuid().nextUuid()}`)
537+
.withRemove(false)
538+
.start();
539+
540+
await container.stop({ remove: true }); // remove: true here should override withRemove(false)
541+
542+
const containerName = container.getName()?.slice(1); // remove first '/'
543+
expect(await getStoppedContainerNames()).not.toContain(containerName);
544+
});
545+
506546
it("should stop the container idempotently", async () => {
507547
const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
508548
.withName(`container-${new RandomUuid().nextUuid()}`)

packages/testcontainers/src/generic-container/generic-container.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class GenericContainer implements TestContainer {
5050
protected environment: Record<string, string> = {};
5151
protected exposedPorts: PortWithOptionalBinding[] = [];
5252
protected reuse = false;
53+
protected remove = true;
5354
protected networkMode?: string;
5455
protected networkAliases: string[] = [];
5556
protected pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
@@ -158,7 +159,8 @@ export class GenericContainer implements TestContainer {
158159
inspectResult,
159160
boundPorts,
160161
inspectResult.Name,
161-
this.waitStrategy
162+
this.waitStrategy,
163+
this.remove
162164
);
163165
}
164166

@@ -222,7 +224,8 @@ export class GenericContainer implements TestContainer {
222224
inspectResult,
223225
boundPorts,
224226
inspectResult.Name,
225-
this.waitStrategy
227+
this.waitStrategy,
228+
this.remove
226229
);
227230

228231
if (this.containerStarted) {
@@ -433,6 +436,11 @@ export class GenericContainer implements TestContainer {
433436
return this;
434437
}
435438

439+
public withRemove(remove: boolean): this {
440+
this.remove = remove;
441+
return this;
442+
}
443+
436444
public withPullPolicy(pullPolicy: ImagePullPolicy): this {
437445
this.pullPolicy = pullPolicy;
438446
return this;

packages/testcontainers/src/generic-container/started-generic-container.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import { StoppedGenericContainer } from "./stopped-generic-container";
1616

1717
export class StartedGenericContainer implements StartedTestContainer {
1818
private stoppedContainer?: StoppedTestContainer;
19-
private stopContainerLock = new AsyncLock();
19+
private readonly stopContainerLock = new AsyncLock();
2020

2121
constructor(
2222
private readonly container: Dockerode.Container,
2323
private readonly host: string,
2424
private inspectResult: ContainerInspectInfo,
2525
private boundPorts: BoundPorts,
2626
private readonly name: string,
27-
private readonly waitStrategy: WaitStrategy
27+
private readonly waitStrategy: WaitStrategy,
28+
private readonly removeOnStop: boolean = true
2829
) {}
2930

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

108-
const resolvedOptions: StopOptions = { remove: true, timeout: 0, removeVolumes: true, ...options };
109+
// use this.removeOnStop value here (true by default)
110+
// but allow to override it explicitly from options argument
111+
const resolvedOptions: StopOptions = { remove: this.removeOnStop, timeout: 0, removeVolumes: true, ...options };
109112
await client.container.stop(this.container, { timeout: resolvedOptions.timeout });
110113
if (resolvedOptions.remove) {
114+
log.info("StopOptions.remove=true, removing container.");
111115
await client.container.remove(this.container, { removeVolumes: resolvedOptions.removeVolumes });
112116
}
113117
log.info(`Stopped container`, { containerId: this.container.id });

packages/testcontainers/src/utils/test-helper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,16 @@ export const getDockerEventStream = async (opts: GetEventsOptions = {}): Promise
3636
};
3737

3838
export const getRunningContainerNames = async (): Promise<string[]> => {
39+
return getContainerNames(false);
40+
};
41+
42+
export const getStoppedContainerNames = async (): Promise<string[]> => {
43+
return getContainerNames(true, { status: ["paused", "exited"] });
44+
};
45+
46+
const getContainerNames = async (all: boolean, filters?: string | { [key: string]: string[] }): Promise<string[]> => {
3947
const dockerode = (await getContainerRuntimeClient()).container.dockerode;
40-
const containers = await dockerode.listContainers();
48+
const containers = await dockerode.listContainers({ all, filters });
4149
return containers
4250
.map((container) => container.Names)
4351
.reduce((result, containerNames) => [...result, ...containerNames], [])

0 commit comments

Comments
 (0)