From a7f30f053344a7ada302c7129476bf5c4853fc6e Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 17:14:26 +0100 Subject: [PATCH] Replace hand-rolled stream handling with Node stream built-ins Follows the k3s change in #1503. --- .../modules/selenium/src/selenium-container.ts | 7 ++----- packages/testcontainers/src/common/index.ts | 1 - packages/testcontainers/src/common/streams.ts | 15 --------------- .../container/docker-container-client.ts | 18 +++++++----------- .../utils/run-in-container.ts | 5 +++-- .../src/wait-strategies/http-wait-strategy.ts | 10 +++------- 6 files changed, 15 insertions(+), 41 deletions(-) delete mode 100644 packages/testcontainers/src/common/streams.ts diff --git a/packages/modules/selenium/src/selenium-container.ts b/packages/modules/selenium/src/selenium-container.ts index 13b9db952..dffc20b92 100644 --- a/packages/modules/selenium/src/selenium-container.ts +++ b/packages/modules/selenium/src/selenium-container.ts @@ -1,5 +1,6 @@ import { copyFile } from "node:fs/promises"; import path from "node:path"; +import { pipeline } from "node:stream/promises"; import tar from "tar-fs"; import { AbstractStartedContainer, @@ -140,10 +141,6 @@ export class StoppedSeleniumRecordingContainer extends StoppedSeleniumContainer } private async extractTarStreamToDest(tarStream: NodeJS.ReadableStream, dest: string): Promise { - await new Promise((resolve) => { - const destination = tar.extract(dest); - tarStream.pipe(destination); - destination.on("finish", resolve); - }); + await pipeline(tarStream, tar.extract(dest)); } } diff --git a/packages/testcontainers/src/common/index.ts b/packages/testcontainers/src/common/index.ts index f821ac5b4..f9cd5d29f 100644 --- a/packages/testcontainers/src/common/index.ts +++ b/packages/testcontainers/src/common/index.ts @@ -2,7 +2,6 @@ export { withFileLock } from "./file-lock"; export { hash } from "./hash"; export { buildLog, composeLog, containerLog, execLog, Logger, log, pullLog } from "./logger"; export { IntervalRetry, Retry } from "./retry"; -export { streamToString } from "./streams"; export * from "./time"; export * from "./type-guards"; export { RandomUuid, randomUuid, Uuid } from "./uuid"; diff --git a/packages/testcontainers/src/common/streams.ts b/packages/testcontainers/src/common/streams.ts deleted file mode 100644 index 5f7ef4e92..000000000 --- a/packages/testcontainers/src/common/streams.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Readable } from "node:stream"; - -type Options = { trim: boolean }; - -export const streamToString = async (stream: Readable, options: Options = { trim: false }): Promise => { - const chunks = []; - - for await (const chunk of stream) { - chunks.push(Buffer.from(chunk)); - } - - const str = Buffer.concat(chunks).toString("utf-8"); - - return options.trim ? str.trim() : str; -}; diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts index 6fe7c503b..2f462f31d 100644 --- a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts @@ -1,5 +1,5 @@ import type { IncomingMessage } from "node:http"; -import { PassThrough, Readable } from "node:stream"; +import { PassThrough, type Readable } from "node:stream"; import { finished } from "node:stream/promises"; import type Dockerode from "dockerode"; import type { @@ -11,7 +11,7 @@ import type { ExecCreateOptions, Network, } from "dockerode"; -import { execLog, log, streamToString, toSeconds } from "../../../common"; +import { execLog, log, toSeconds } from "../../../common"; import type { CopyToContainerOptions } from "../../../types"; import type { ContainerClient } from "./container-client"; import type { ContainerCommitOptions, ContainerStatus, ExecOptions, ExecResult } from "./types"; @@ -83,15 +83,11 @@ export class DockerContainerClient implements ContainerClient { ): Promise { try { log.debug(`Putting archive to container...`, { containerId: container.id }); - await streamToString( - Readable.from( - await container.putArchive(stream, { - path, - noOverwriteDirNonDir: options?.noOverwriteDirNonDir, - copyUIDGID: options?.copyUIDGID, - }) - ) - ); + await container.putArchive(stream, { + path, + noOverwriteDirNonDir: options?.noOverwriteDirNonDir, + copyUIDGID: options?.copyUIDGID, + }); log.debug(`Put archive to container`, { containerId: container.id }); } catch (err) { log.error(`Failed to put archive to container: ${err}`, { containerId: container.id }); diff --git a/packages/testcontainers/src/container-runtime/utils/run-in-container.ts b/packages/testcontainers/src/container-runtime/utils/run-in-container.ts index 65c4e7d27..0acbeeedd 100644 --- a/packages/testcontainers/src/container-runtime/utils/run-in-container.ts +++ b/packages/testcontainers/src/container-runtime/utils/run-in-container.ts @@ -1,5 +1,6 @@ +import { text } from "node:stream/consumers"; import type Dockerode from "dockerode"; -import { log, streamToString } from "../../common"; +import { log } from "../../common"; import { ImageName } from "../image-name"; import { attachContainer } from "./attach-container"; import { pullImage } from "./pull-image"; @@ -24,7 +25,7 @@ export const runInContainer = async ( await startContainer(container); log.debug(`Waiting for container output...`, { containerId: container.id }); - const output = await streamToString(stream, { trim: true }); + const output = (await text(stream)).trim(); log.debug(`Removing container...`, { containerId: container.id }); await container.remove({ force: true, v: true }); diff --git a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts index b707ba564..5ff1e3096 100644 --- a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts @@ -1,3 +1,4 @@ +import { text } from "node:stream/consumers"; import type Dockerode from "dockerode"; import { Agent, request } from "undici"; import { IntervalRetry, log } from "../common"; @@ -153,18 +154,13 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { private async handleContainerExit(container: Dockerode.Container) { const tail = 50; - const lastLogs: string[] = []; const client = await getContainerRuntimeClient(); let message: string; try { - const stream = await client.container.logs(container, { tail }); + const lastLogs = await text(await client.container.logs(container, { tail })); - await new Promise((res) => { - stream.on("data", (d) => lastLogs.push(d.trim())).on("end", res); - }); - - message = `Container exited during HTTP healthCheck, last ${tail} logs: ${lastLogs.join("\n")}`; + message = `Container exited during HTTP healthCheck, last ${tail} logs: ${lastLogs.trim()}`; } catch (_err) { message = "Container exited during HTTP healthCheck, failed to get last logs"; }