Skip to content

Commit b19e0c8

Browse files
Add missing reaper tests (#947)
1 parent 42a522b commit b19e0c8

File tree

4 files changed

+100
-15
lines changed

4 files changed

+100
-15
lines changed

docs/supported-container-runtimes.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,38 @@ Works out of the box.
88

99
### Usage
1010

11-
MacOS:
11+
#### MacOS:
12+
1213
```bash
1314
{% raw %}
1415
export DOCKER_HOST=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')
1516
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock
1617
{% endraw %}
1718
```
1819

19-
Linux:
20-
```bash
21-
export DOCKER_HOST=unix://${XDG_RUNTIME_DIR}/podman/podman.sock
22-
```
20+
#### Linux:
21+
22+
1. Ensure the Podman socket is exposed:
23+
24+
Rootless:
25+
26+
```bash
27+
systemctl --user status podman.socket
28+
```
29+
30+
Rootful:
31+
32+
```bash
33+
sudo systemctl enable --now podman.socket
34+
```
35+
36+
2. Export the `DOCKER_HOST`:
37+
38+
```bash
39+
{% raw %}
40+
export DOCKER_HOST="unix://$(podman info --format '{{.Host.RemoteSocket.Path}}')"
41+
{% endraw %}
42+
```
2343

2444
### Known issues
2545

@@ -71,10 +91,7 @@ You can use a composite wait strategy to additionally wait for a port to be boun
7191
const { GenericContainer, Wait } = require("testcontainers");
7292
7393
const container = await new GenericContainer("redis")
74-
.withWaitStrategy(Wait.forAll([
75-
Wait.forListeningPorts(),
76-
Wait.forLogMessage("Ready to accept connections")
77-
]))
94+
.withWaitStrategy(Wait.forAll([Wait.forListeningPorts(), Wait.forLogMessage("Ready to accept connections")]))
7895
.start();
7996
```
8097

packages/testcontainers/src/port-forwarder/port-forwarder.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,13 @@ export class PortForwarderInstance {
9696
}
9797

9898
log.debug(`Connecting to Port Forwarder on "${host}:${port}"...`);
99-
const connection = await createSshConnection({ host, port, username: "root", password: "root" });
99+
const connection = await createSshConnection({
100+
host,
101+
port,
102+
username: "root",
103+
password: "root",
104+
readyTimeout: 100_000,
105+
});
100106
log.debug(`Connected to Port Forwarder on "${host}:${port}"`);
101107
connection.unref();
102108

packages/testcontainers/src/reaper/reaper.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime";
2+
import { RandomUniquePortGenerator } from "../utils/port-generator";
23

34
describe("Reaper", { timeout: 120_000 }, () => {
45
let client: ContainerRuntimeClient;
@@ -7,9 +8,64 @@ describe("Reaper", { timeout: 120_000 }, () => {
78

89
beforeEach(async () => {
910
vi.resetModules();
11+
vi.stubEnv("TESTCONTAINERS_RYUK_TEST_LABEL", "true");
1012
client = await getContainerRuntimeClient();
1113
});
1214

15+
it("should create disabled reaper when TESTCONTAINERS_RYUK_DISABLED=true", async () => {
16+
vi.stubEnv("TESTCONTAINERS_RYUK_DISABLED", "true");
17+
vi.spyOn(client.container, "list").mockResolvedValue([]);
18+
19+
const reaper = await getReaper();
20+
21+
expect(() => reaper.addSession("test-session")).not.toThrow();
22+
expect(() => reaper.addComposeProject("test-project")).not.toThrow();
23+
});
24+
25+
it("should return cached reaper instance", async () => {
26+
vi.spyOn(client.container, "list").mockResolvedValue([]);
27+
28+
const reaper = await getReaper();
29+
const reaper2 = await getReaper();
30+
31+
expect(reaper2.containerId).toBe(reaper.containerId);
32+
});
33+
34+
it("should create new reaper container if one is not running", async () => {
35+
vi.spyOn(client.container, "list").mockResolvedValue([]);
36+
const reaper = await getReaper();
37+
vi.resetModules();
38+
39+
const reaper2 = await getReaper();
40+
41+
expect(reaper2.containerId).not.toBe(reaper.containerId);
42+
});
43+
44+
it("should reuse existing reaper container if one is already running", async () => {
45+
const reaper = await getReaper();
46+
vi.resetModules();
47+
const reaperContainerInfo = (await client.container.list()).filter((c) => c.Id === reaper.containerId)[0];
48+
reaperContainerInfo.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] = "false";
49+
vi.spyOn(client.container, "list").mockResolvedValue([reaperContainerInfo]);
50+
51+
const reaper2 = await getReaper();
52+
53+
expect(reaper2.containerId).toBe(reaper.containerId);
54+
});
55+
56+
it("should use custom port when TESTCONTAINERS_RYUK_PORT is set", async () => {
57+
const customPort = (await new RandomUniquePortGenerator().generatePort()).toString();
58+
vi.stubEnv("TESTCONTAINERS_RYUK_PORT", customPort);
59+
vi.spyOn(client.container, "list").mockResolvedValue([]);
60+
61+
const reaper = await getReaper();
62+
63+
const reaperContainer = client.container.getById(reaper.containerId);
64+
const ports = (await reaperContainer.inspect()).HostConfig.PortBindings;
65+
const port = ports["8080"] || ports["8080/tcp"];
66+
expect(port[0].HostPort).toBe(customPort);
67+
});
68+
1369
it("should create Reaper container without RYUK_VERBOSE env var by default", async () => {
1470
vi.spyOn(client.container, "list").mockResolvedValue([]);
1571
const reaper = await getReaper();
@@ -22,8 +78,8 @@ describe("Reaper", { timeout: 120_000 }, () => {
2278

2379
it("should propagate TESTCONTAINERS_RYUK_VERBOSE into Reaper container", async () => {
2480
vi.stubEnv("TESTCONTAINERS_RYUK_VERBOSE", "true");
25-
2681
vi.spyOn(client.container, "list").mockResolvedValue([]);
82+
2783
const reaper = await getReaper();
2884

2985
const reaperContainer = client.container.getById(reaper.containerId);

packages/testcontainers/src/reaper/reaper.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ export async function getReaper(client: ContainerRuntimeClient): Promise<Reaper>
4848
async function findReaperContainer(client: ContainerRuntimeClient): Promise<ContainerInfo | undefined> {
4949
const containers = await client.container.list();
5050
return containers.find(
51-
(container) => container.State === "running" && container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true"
51+
(container) =>
52+
container.State === "running" &&
53+
container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true" &&
54+
container.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] !== "true"
5255
);
5356
}
5457

@@ -78,12 +81,15 @@ async function createNewReaper(sessionId: string, remoteSocketPath: string): Pro
7881
.withBindMounts([{ source: remoteSocketPath, target: "/var/run/docker.sock" }])
7982
.withLabels({ [LABEL_TESTCONTAINERS_SESSION_ID]: sessionId })
8083
.withWaitStrategy(Wait.forLogMessage(/.*Started.*/));
81-
if (process.env["TESTCONTAINERS_RYUK_VERBOSE"])
84+
if (process.env["TESTCONTAINERS_RYUK_VERBOSE"]) {
8285
container.withEnvironment({ RYUK_VERBOSE: process.env["TESTCONTAINERS_RYUK_VERBOSE"] });
83-
84-
if (process.env.TESTCONTAINERS_RYUK_PRIVILEGED === "true") {
86+
}
87+
if (process.env["TESTCONTAINERS_RYUK_PRIVILEGED"] === "true") {
8588
container.withPrivilegedMode();
8689
}
90+
if (process.env["TESTCONTAINERS_RYUK_TEST_LABEL"] === "true") {
91+
container.withLabels({ TESTCONTAINERS_RYUK_TEST_LABEL: "true" });
92+
}
8793

8894
const startedContainer = await container.start();
8995

0 commit comments

Comments
 (0)