From 989ecaac49185227bd5cf5791cc50c4a02fee6b0 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Sat, 22 Mar 2025 12:00:03 +0000 Subject: [PATCH 1/3] Fix flaky port forwarder tests --- .../port-forwarder-reuse.test.ts | 62 ++++++++ .../src/port-forwarder/port-forwarder.test.ts | 149 ++++-------------- .../src/utils/port-generator.ts | 4 +- .../testcontainers/src/utils/test-helper.ts | 10 ++ 4 files changed, 108 insertions(+), 117 deletions(-) create mode 100644 packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts b/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts new file mode 100644 index 000000000..86178a19e --- /dev/null +++ b/packages/testcontainers/src/port-forwarder/port-forwarder-reuse.test.ts @@ -0,0 +1,62 @@ +import { GenericContainer } from "../generic-container/generic-container"; +import { RandomUniquePortGenerator } from "../utils/port-generator"; +import { createTestServer } from "../utils/test-helper"; + +describe("Port Forwarder reuse", { timeout: 180_000 }, () => { + it("should expose additional ports", async () => { + const portGen = new RandomUniquePortGenerator(); + + const { TestContainers: TC1 } = await import("../test-containers"); + const { PortForwarderInstance: PFI1 } = await import("../port-forwarder/port-forwarder"); + const port1 = await portGen.generatePort(); + const server1 = await createTestServer(port1); + await TC1.exposeHostPorts(port1); + const portForwarder1ContainerId = (await PFI1.getInstance()).getContainerId(); + + vi.resetModules(); + const { TestContainers: TC2 } = await import("../test-containers"); + const { PortForwarderInstance: PFI2 } = await import("../port-forwarder/port-forwarder"); + const port2 = await portGen.generatePort(); + const server2 = await createTestServer(port2); + await TC2.exposeHostPorts(port2); + const portForwarder2ContainerId = (await PFI2.getInstance()).getContainerId(); + + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); + + expect(portForwarder1ContainerId).toEqual(portForwarder2ContainerId); + const { output: output1 } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${port1}`]); + expect(output1).toEqual(expect.stringContaining("hello world")); + const { output: output2 } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${port2}`]); + expect(output2).toEqual(expect.stringContaining("hello world")); + + await new Promise((resolve) => server1.close(resolve)); + await new Promise((resolve) => server2.close(resolve)); + await container.stop(); + }); + + it("should reuse same ports", async () => { + const portGen = new RandomUniquePortGenerator(); + const port = await portGen.generatePort(); + const server = await createTestServer(port); + + const { TestContainers: TC1 } = await import("../test-containers"); + const { PortForwarderInstance: PFI1 } = await import("../port-forwarder/port-forwarder"); + await TC1.exposeHostPorts(port); + const portForwarder1ContainerId = (await PFI1.getInstance()).getContainerId(); + + vi.resetModules(); + const { TestContainers: TC2 } = await import("../test-containers"); + const { PortForwarderInstance: PFI2 } = await import("../port-forwarder/port-forwarder"); + await TC2.exposeHostPorts(port); + const portForwarder2ContainerId = (await PFI2.getInstance()).getContainerId(); + + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); + + expect(portForwarder1ContainerId).toEqual(portForwarder2ContainerId); + const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${port}`]); + expect(output).toEqual(expect.stringContaining("hello world")); + + await new Promise((resolve) => server.close(resolve)); + await container.stop(); + }); +}); diff --git a/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts b/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts index 96fe11b42..c7e263e36 100644 --- a/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts +++ b/packages/testcontainers/src/port-forwarder/port-forwarder.test.ts @@ -1,141 +1,60 @@ -import { createServer, Server } from "http"; +import { Server } from "http"; import { GenericContainer } from "../generic-container/generic-container"; import { Network } from "../network/network"; import { TestContainers } from "../test-containers"; import { RandomUniquePortGenerator } from "../utils/port-generator"; +import { createTestServer } from "../utils/test-helper"; describe("PortForwarder", { timeout: 180_000 }, () => { let randomPort: number; let server: Server; - afterEach(() => { - server.close(); + beforeEach(async () => { + randomPort = await new RandomUniquePortGenerator().generatePort(); + server = await createTestServer(randomPort); }); - describe("Behaviour", () => { - beforeEach(async () => { - randomPort = await new RandomUniquePortGenerator().generatePort(); - server = await createTestServer(randomPort); - }); - - it("should expose host ports to the container", async () => { - await TestContainers.exposeHostPorts(randomPort); - - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); - - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); - - await container.stop(); - }); - - it("should expose host ports to the container with custom network", async () => { - await TestContainers.exposeHostPorts(randomPort); - - const network = await new Network().start(); - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withNetwork(network).start(); - - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); - - await container.stop(); - await network.stop(); - }); - - it("should expose host ports to the container with custom network and network alias", async () => { - await TestContainers.exposeHostPorts(randomPort); - - const network = await new Network().start(); - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") - .withNetwork(network) - .withNetworkAliases("foo") - .start(); - - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); - - await container.stop(); - await network.stop(); - }); + afterEach(async () => { + await new Promise((resolve) => server.close(resolve)); }); - describe("Reuse", () => { - afterEach(() => { - vi.resetModules(); - }); + it("should expose host ports to the container", async () => { + await TestContainers.exposeHostPorts(randomPort); - describe("Different host ports", () => { - beforeEach(async () => { - randomPort = await new RandomUniquePortGenerator().generatePort(); - server = await createTestServer(randomPort); - }); + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); - it("1", async () => { - const { TestContainers } = await import("../test-containers"); - await TestContainers.exposeHostPorts(randomPort); + const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); + expect(output).toEqual(expect.stringContaining("hello world")); - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); - - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); - - await container.stop(); - }); - - it("2", async () => { - const { TestContainers } = await import("../test-containers"); - await TestContainers.exposeHostPorts(randomPort); - - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); - - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); - - await container.stop(); - }); - }); - - describe("Same host ports", () => { - beforeAll(async () => { - randomPort = await new RandomUniquePortGenerator().generatePort(); - }); - - beforeEach(async () => { - server = await createTestServer(randomPort); - }); + await container.stop(); + }); - it("1", async () => { - const { TestContainers } = await import("../test-containers"); - await TestContainers.exposeHostPorts(randomPort); + it("should expose host ports to the container with custom network", async () => { + await TestContainers.exposeHostPorts(randomPort); - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); + const network = await new Network().start(); + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").withNetwork(network).start(); - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); + const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); + expect(output).toEqual(expect.stringContaining("hello world")); - await container.stop(); - }); + await container.stop(); + await network.stop(); + }); - it("2", async () => { - const { TestContainers } = await import("../test-containers"); - await TestContainers.exposeHostPorts(randomPort); + it("should expose host ports to the container with custom network and network alias", async () => { + await TestContainers.exposeHostPorts(randomPort); - const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14").start(); + const network = await new Network().start(); + const container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14") + .withNetwork(network) + .withNetworkAliases("foo") + .start(); - const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); - expect(output).toEqual(expect.stringContaining("hello world")); + const { output } = await container.exec(["curl", "-s", `http://host.testcontainers.internal:${randomPort}`]); + expect(output).toEqual(expect.stringContaining("hello world")); - await container.stop(); - }); - }); + await container.stop(); + await network.stop(); }); }); - -async function createTestServer(port: number): Promise { - const server = createServer((req, res) => { - res.writeHead(200); - res.end("hello world"); - }); - await new Promise((resolve) => server.listen(port, resolve)); - return server; -} diff --git a/packages/testcontainers/src/utils/port-generator.ts b/packages/testcontainers/src/utils/port-generator.ts index 6078b09f2..f441500f6 100644 --- a/packages/testcontainers/src/utils/port-generator.ts +++ b/packages/testcontainers/src/utils/port-generator.ts @@ -4,8 +4,8 @@ export interface PortGenerator { class RandomPortGenerator { public async generatePort(): Promise { - const { default: getPort, portNumbers } = await import("get-port"); - return getPort({ port: portNumbers(10000, 65535) }); + const { default: getPort } = await import("get-port"); + return getPort(); } } diff --git a/packages/testcontainers/src/utils/test-helper.ts b/packages/testcontainers/src/utils/test-helper.ts index 16210782f..e3a132c30 100644 --- a/packages/testcontainers/src/utils/test-helper.ts +++ b/packages/testcontainers/src/utils/test-helper.ts @@ -1,4 +1,5 @@ import { GetEventsOptions, ImageInspectInfo } from "dockerode"; +import { Server, createServer } from "http"; import { Readable } from "stream"; import { Agent } from "undici"; import { IntervalRetry } from "../common"; @@ -143,3 +144,12 @@ export async function stopStartingContainer(container: GenericContainer, name: s await client.container.getById(name).stop(); await containerStartPromise; } + +export async function createTestServer(port: number): Promise { + const server = createServer((req, res) => { + res.writeHead(200); + res.end("hello world"); + }); + await new Promise((resolve) => server.listen(port, resolve)); + return server; +} From 1aa037a9e1c1fa891dc35d4e5687de7f7d0bf5d2 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Sat, 22 Mar 2025 12:03:25 +0000 Subject: [PATCH 2/3] Fix lint --- packages/modules/kafka/src/kafka-container.test.ts | 2 +- .../modules/localstack/src/localstack-container.test.ts | 2 +- packages/modules/selenium/src/selenium-container.test.ts | 2 +- packages/testcontainers/src/common/index.ts | 2 +- .../src/container-runtime/clients/compose/compose-client.ts | 2 +- packages/testcontainers/src/index.ts | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/modules/kafka/src/kafka-container.test.ts b/packages/modules/kafka/src/kafka-container.test.ts index f71c654ec..d68799d71 100644 --- a/packages/modules/kafka/src/kafka-container.test.ts +++ b/packages/modules/kafka/src/kafka-container.test.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import { Kafka, KafkaConfig, logLevel } from "kafkajs"; import * as path from "path"; import { GenericContainer, Network, StartedTestContainer } from "testcontainers"; -import { KafkaContainer, KAFKA_IMAGE } from "./kafka-container"; +import { KAFKA_IMAGE, KafkaContainer } from "./kafka-container"; describe("KafkaContainer", { timeout: 240_000 }, () => { // connectBuiltInZK { diff --git a/packages/modules/localstack/src/localstack-container.test.ts b/packages/modules/localstack/src/localstack-container.test.ts index c690bf04b..1b8957ac4 100644 --- a/packages/modules/localstack/src/localstack-container.test.ts +++ b/packages/modules/localstack/src/localstack-container.test.ts @@ -1,6 +1,6 @@ import { CreateBucketCommand, HeadBucketCommand, S3Client } from "@aws-sdk/client-s3"; import { GenericContainer, LABEL_TESTCONTAINERS_SESSION_ID, log, Network, StartedTestContainer } from "testcontainers"; -import { LocalstackContainer, LOCALSTACK_PORT } from "./localstack-container"; +import { LOCALSTACK_PORT, LocalstackContainer } from "./localstack-container"; const runAwsCliAgainstDockerNetworkContainer = async ( command: string, diff --git a/packages/modules/selenium/src/selenium-container.test.ts b/packages/modules/selenium/src/selenium-container.test.ts index d7a131963..921bfe803 100644 --- a/packages/modules/selenium/src/selenium-container.test.ts +++ b/packages/modules/selenium/src/selenium-container.test.ts @@ -2,7 +2,7 @@ import path from "path"; import { Browser, Builder } from "selenium-webdriver"; import { GenericContainer } from "testcontainers"; import tmp from "tmp"; -import { SeleniumContainer, SELENIUM_VIDEO_IMAGE } from "./selenium-container"; +import { SELENIUM_VIDEO_IMAGE, SeleniumContainer } from "./selenium-container"; describe("SeleniumContainer", { timeout: 180_000 }, () => { const browsers = [ diff --git a/packages/testcontainers/src/common/index.ts b/packages/testcontainers/src/common/index.ts index e3c453f75..264aa58f0 100644 --- a/packages/testcontainers/src/common/index.ts +++ b/packages/testcontainers/src/common/index.ts @@ -1,6 +1,6 @@ export { withFileLock } from "./file-lock"; export { hash } from "./hash"; -export { buildLog, composeLog, containerLog, execLog, log, Logger, pullLog } from "./logger"; +export { Logger, buildLog, composeLog, containerLog, execLog, log, pullLog } from "./logger"; export { IntervalRetry, Retry } from "./retry"; export { streamToString } from "./streams"; export * from "./type-guards"; diff --git a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts index 70cbea015..e73b71109 100644 --- a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts @@ -1,4 +1,4 @@ -import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose"; +import { default as dockerComposeV1, v2 as dockerComposeV2, default as v1, v2 } from "docker-compose"; import { log, pullLog } from "../../../common"; import { ComposeInfo } from "../types"; import { defaultComposeOptions } from "./default-compose-options"; diff --git a/packages/testcontainers/src/index.ts b/packages/testcontainers/src/index.ts index 08a4e910d..8942cdb25 100644 --- a/packages/testcontainers/src/index.ts +++ b/packages/testcontainers/src/index.ts @@ -1,5 +1,5 @@ -export { IntervalRetry, log, RandomUuid, Retry, Uuid } from "./common"; -export { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "./container-runtime"; +export { IntervalRetry, RandomUuid, Retry, Uuid, log } from "./common"; +export { ContainerRuntimeClient, ImageName, getContainerRuntimeClient } from "./container-runtime"; export { DockerComposeEnvironment } from "./docker-compose-environment/docker-compose-environment"; export { DownedDockerComposeEnvironment } from "./docker-compose-environment/downed-docker-compose-environment"; export { StartedDockerComposeEnvironment } from "./docker-compose-environment/started-docker-compose-environment"; @@ -21,7 +21,7 @@ export { TestContainers } from "./test-containers"; export { CommitOptions, Content, ExecOptions, ExecResult, InspectResult } from "./types"; export { BoundPorts } from "./utils/bound-ports"; export { LABEL_TESTCONTAINERS_SESSION_ID } from "./utils/labels"; -export { getContainerPort, hasHostBinding, PortWithBinding, PortWithOptionalBinding } from "./utils/port"; +export { PortWithBinding, PortWithOptionalBinding, getContainerPort, hasHostBinding } from "./utils/port"; export { PortGenerator, RandomUniquePortGenerator } from "./utils/port-generator"; export { ImagePullPolicy, PullPolicy } from "./utils/pull-policy"; export { HttpWaitStrategyOptions } from "./wait-strategies/http-wait-strategy"; From caa35afc9787299964106ac3ed99dc5e7671e16d Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Sat, 22 Mar 2025 12:06:49 +0000 Subject: [PATCH 3/3] Fix lint --- packages/modules/kafka/src/kafka-container.test.ts | 2 +- .../modules/localstack/src/localstack-container.test.ts | 2 +- packages/modules/selenium/src/selenium-container.test.ts | 2 +- packages/testcontainers/src/common/index.ts | 2 +- .../src/container-runtime/clients/compose/compose-client.ts | 2 +- packages/testcontainers/src/index.ts | 6 +++--- packages/testcontainers/src/utils/test-helper.ts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/modules/kafka/src/kafka-container.test.ts b/packages/modules/kafka/src/kafka-container.test.ts index d68799d71..f71c654ec 100644 --- a/packages/modules/kafka/src/kafka-container.test.ts +++ b/packages/modules/kafka/src/kafka-container.test.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import { Kafka, KafkaConfig, logLevel } from "kafkajs"; import * as path from "path"; import { GenericContainer, Network, StartedTestContainer } from "testcontainers"; -import { KAFKA_IMAGE, KafkaContainer } from "./kafka-container"; +import { KafkaContainer, KAFKA_IMAGE } from "./kafka-container"; describe("KafkaContainer", { timeout: 240_000 }, () => { // connectBuiltInZK { diff --git a/packages/modules/localstack/src/localstack-container.test.ts b/packages/modules/localstack/src/localstack-container.test.ts index 1b8957ac4..c690bf04b 100644 --- a/packages/modules/localstack/src/localstack-container.test.ts +++ b/packages/modules/localstack/src/localstack-container.test.ts @@ -1,6 +1,6 @@ import { CreateBucketCommand, HeadBucketCommand, S3Client } from "@aws-sdk/client-s3"; import { GenericContainer, LABEL_TESTCONTAINERS_SESSION_ID, log, Network, StartedTestContainer } from "testcontainers"; -import { LOCALSTACK_PORT, LocalstackContainer } from "./localstack-container"; +import { LocalstackContainer, LOCALSTACK_PORT } from "./localstack-container"; const runAwsCliAgainstDockerNetworkContainer = async ( command: string, diff --git a/packages/modules/selenium/src/selenium-container.test.ts b/packages/modules/selenium/src/selenium-container.test.ts index 921bfe803..d7a131963 100644 --- a/packages/modules/selenium/src/selenium-container.test.ts +++ b/packages/modules/selenium/src/selenium-container.test.ts @@ -2,7 +2,7 @@ import path from "path"; import { Browser, Builder } from "selenium-webdriver"; import { GenericContainer } from "testcontainers"; import tmp from "tmp"; -import { SELENIUM_VIDEO_IMAGE, SeleniumContainer } from "./selenium-container"; +import { SeleniumContainer, SELENIUM_VIDEO_IMAGE } from "./selenium-container"; describe("SeleniumContainer", { timeout: 180_000 }, () => { const browsers = [ diff --git a/packages/testcontainers/src/common/index.ts b/packages/testcontainers/src/common/index.ts index 264aa58f0..e3c453f75 100644 --- a/packages/testcontainers/src/common/index.ts +++ b/packages/testcontainers/src/common/index.ts @@ -1,6 +1,6 @@ export { withFileLock } from "./file-lock"; export { hash } from "./hash"; -export { Logger, buildLog, composeLog, containerLog, execLog, log, pullLog } from "./logger"; +export { buildLog, composeLog, containerLog, execLog, log, Logger, pullLog } from "./logger"; export { IntervalRetry, Retry } from "./retry"; export { streamToString } from "./streams"; export * from "./type-guards"; diff --git a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts index e73b71109..70cbea015 100644 --- a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts @@ -1,4 +1,4 @@ -import { default as dockerComposeV1, v2 as dockerComposeV2, default as v1, v2 } from "docker-compose"; +import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose"; import { log, pullLog } from "../../../common"; import { ComposeInfo } from "../types"; import { defaultComposeOptions } from "./default-compose-options"; diff --git a/packages/testcontainers/src/index.ts b/packages/testcontainers/src/index.ts index 8942cdb25..08a4e910d 100644 --- a/packages/testcontainers/src/index.ts +++ b/packages/testcontainers/src/index.ts @@ -1,5 +1,5 @@ -export { IntervalRetry, RandomUuid, Retry, Uuid, log } from "./common"; -export { ContainerRuntimeClient, ImageName, getContainerRuntimeClient } from "./container-runtime"; +export { IntervalRetry, log, RandomUuid, Retry, Uuid } from "./common"; +export { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "./container-runtime"; export { DockerComposeEnvironment } from "./docker-compose-environment/docker-compose-environment"; export { DownedDockerComposeEnvironment } from "./docker-compose-environment/downed-docker-compose-environment"; export { StartedDockerComposeEnvironment } from "./docker-compose-environment/started-docker-compose-environment"; @@ -21,7 +21,7 @@ export { TestContainers } from "./test-containers"; export { CommitOptions, Content, ExecOptions, ExecResult, InspectResult } from "./types"; export { BoundPorts } from "./utils/bound-ports"; export { LABEL_TESTCONTAINERS_SESSION_ID } from "./utils/labels"; -export { PortWithBinding, PortWithOptionalBinding, getContainerPort, hasHostBinding } from "./utils/port"; +export { getContainerPort, hasHostBinding, PortWithBinding, PortWithOptionalBinding } from "./utils/port"; export { PortGenerator, RandomUniquePortGenerator } from "./utils/port-generator"; export { ImagePullPolicy, PullPolicy } from "./utils/pull-policy"; export { HttpWaitStrategyOptions } from "./wait-strategies/http-wait-strategy"; diff --git a/packages/testcontainers/src/utils/test-helper.ts b/packages/testcontainers/src/utils/test-helper.ts index e3a132c30..dc1ff07b9 100644 --- a/packages/testcontainers/src/utils/test-helper.ts +++ b/packages/testcontainers/src/utils/test-helper.ts @@ -1,5 +1,5 @@ import { GetEventsOptions, ImageInspectInfo } from "dockerode"; -import { Server, createServer } from "http"; +import { createServer, Server } from "http"; import { Readable } from "stream"; import { Agent } from "undici"; import { IntervalRetry } from "../common";