diff --git a/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.test.ts b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.test.ts new file mode 100644 index 000000000..0a18e03c9 --- /dev/null +++ b/packages/testcontainers/src/container-runtime/clients/container/docker-container-client.test.ts @@ -0,0 +1,48 @@ +import { PassThrough, Readable } from "stream"; +import { DockerContainerClient } from "./docker-container-client"; + +describe("DockerContainerClient", () => { + describe("exec", () => { + it("should not truncate output when the demuxed streams flush after the raw stream ends", async () => { + const payload = "the-final-line-that-must-not-be-truncated\n"; + + const rawStream = new PassThrough(); + + const exec = { + start: vi.fn(async () => { + process.nextTick(() => { + rawStream.write(payload); + rawStream.end(); + }); + return rawStream; + }), + inspect: vi.fn(async () => ({ ExitCode: 0 })), + }; + + const container = { + id: "container-id", + exec: vi.fn(async () => exec), + }; + + const dockerode = { + modem: { + demuxStream: (raw: Readable, stdout: PassThrough) => { + stdout.cork(); + raw.on("data", (chunk) => stdout.write(chunk)); + setImmediate(() => stdout.uncork()); + }, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + const client = new DockerContainerClient(dockerode); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await client.exec(container as any, ["echo", "hi"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(payload); + expect(result.output).toBe(payload); + }); + }); +}); 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 4ddef30e1..ada9def7a 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 @@ -9,6 +9,7 @@ import Dockerode, { } from "dockerode"; import { IncomingMessage } from "http"; import { PassThrough, Readable } from "stream"; +import { finished } from "stream/promises"; import { execLog, log, streamToString, toSeconds } from "../../../common"; import { CopyToContainerOptions } from "../../../types"; import { ContainerClient } from "./container-client"; @@ -253,11 +254,18 @@ export class DockerContainerClient implements ContainerClient { processStream(stdoutStream, stdoutChunks); processStream(stderrStream, stderrChunks); - await new Promise((res, rej) => { - stream.on("end", res); - stream.on("error", rej); - }); - stream.destroy(); + try { + await new Promise((res, rej) => { + stream.on("end", res); + stream.on("error", rej); + }); + + stdoutStream.end(); + stderrStream.end(); + await Promise.all([finished(stdoutStream), finished(stderrStream)]); + } finally { + stream.destroy(); + } const inspectResult = await exec.inspect(); const exitCode = inspectResult.ExitCode ?? -1;