Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/testcontainers/src/common/time.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toNanos, toSeconds } from "./time";
import { toNanos, toSeconds, toUnixSeconds } from "./time";

test.for([
[0, 0],
Expand All @@ -23,3 +23,11 @@ test.for([
])("should convert %i ms to %i ns", ([ms, ns]) => {
expect(toNanos(ms)).toEqual(ns);
});

test("toUnixSeconds leaves a Unix-seconds number unchanged", () => {
expect(toUnixSeconds(1_700_000_000)).toEqual(1_700_000_000);
});

test("toUnixSeconds converts a Date to Unix seconds", () => {
expect(toUnixSeconds(new Date(1_700_000_000_123))).toEqual(1_700_000_000);
});
4 changes: 4 additions & 0 deletions packages/testcontainers/src/common/time.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const toSeconds = (ms: number) => Math.trunc(ms * 1e-3);

export const toNanos = (ms: number) => ms * 1e6;

/** Unix seconds for Docker log filters. Numbers are already seconds; Dates are converted. */
export const toUnixSeconds = (value: number | Date): number =>
value instanceof Date ? Math.trunc(value.getTime() * 1e-3) : value;
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type ComposeExecutableOptions =
};

export type ComposeDownOptions = {
/** Compose down timeout in milliseconds. */
timeout: number;
removeVolumes: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class AbstractStartedContainer implements StartedTestContainer {
return this.startedTestContainer.exec(command, opts);
}

public logs(opts?: { since?: number; tail?: number }): Promise<Readable> {
public logs(opts?: { since?: number | Date; tail?: number }): Promise<Readable> {
return this.startedTestContainer.logs(opts);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ describe("GenericContainer logs", { timeout: 180_000 }, () => {
expect(log).toBe(logAfterSleep);
});

it("should stream logs with since as a Date from a started container", async () => {
const pauseMs = 5 * 1000;
const logBeforeSleep = "first";
const logAfterSleep = "second";
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
.withEntrypoint([
"/bin/sh",
"-c",
`echo ${logBeforeSleep} && sleep ${pauseMs / 1000} && echo ${logAfterSleep} && sleep infinity`,
])
.withWaitStrategy(Wait.forLogMessage(logBeforeSleep))
.start();

await new Promise((resolve) => setTimeout(resolve, pauseMs));

const since = new Date(Date.now() - pauseMs + 1000);

const stream = await container.logs({ since });
const log: string = await new Promise((resolve) => stream.on("data", (line) => resolve(line.trim())));

expect(log).toBe(logAfterSleep);
});

it("should stream logs from a running container after restart", async () => {
const containerLogTraceSpy = vi.spyOn(containerLog, "trace");
await using container = await new GenericContainer("cristianrgreco/testcontainer:1.1.14")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,12 +558,14 @@ export class GenericContainer implements TestContainer {
return this;
}

/** Resource limits. `memory` is gigabytes; `cpu` is CPUs. */
public withResourcesQuota({ memory, cpu }: ResourcesQuota): this {
this.hostConfig.Memory = memory !== undefined ? Math.ceil(memory * 1024 ** 3) : undefined;
this.hostConfig.NanoCpus = cpu !== undefined ? Math.ceil(cpu * 10 ** 9) : undefined;
return this;
}

/** `/dev/shm` size in bytes. */
public withSharedMemorySize(bytes: number): this {
this.hostConfig.ShmSize = bytes;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import archiver from "archiver";
import AsyncLock from "async-lock";
import type Dockerode from "dockerode";
import type { ContainerInspectInfo } from "dockerode";
import { containerLog, log } from "../common";
import { containerLog, log, toUnixSeconds } from "../common";
import { type ContainerRuntimeClient, getContainerRuntimeClient } from "../container-runtime";
// biome-ignore lint/suspicious/noImportCycles: Ryuk and sshd run as GenericContainers; only dereferenced at runtime
import { getReaper } from "../reaper/reaper";
Expand Down Expand Up @@ -257,10 +257,11 @@ export class StartedGenericContainer implements StartedTestContainer {
return output;
}

public async logs(opts?: { since?: number; tail?: number }): Promise<Readable> {
public async logs(opts?: { since?: number | Date; tail?: number }): Promise<Readable> {
const client = await getContainerRuntimeClient();
const since = opts?.since !== undefined ? toUnixSeconds(opts.since) : undefined;

return client.container.logs(this.container, opts);
return client.container.logs(this.container, { ...opts, since });
}

async [Symbol.asyncDispose]() {
Expand Down
12 changes: 11 additions & 1 deletion packages/testcontainers/src/test-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface TestContainer {
withExposedPorts(...ports: PortWithOptionalBinding[]): this;
withBindMounts(bindMounts: BindMount[]): this;
withWaitStrategy(waitStrategy: WaitStrategy): this;
/** Container startup timeout in milliseconds. */
withStartupTimeout(startupTimeoutMs: number): this;
withNetwork(network: StartedNetwork): this;
withNetworkMode(networkMode: string): this;
Expand All @@ -52,17 +53,21 @@ export interface TestContainer {
withCopyArchivesToContainer(archivesToCopy: ArchiveToCopy[]): this;
withCopyToContainerOptions(copyToContainerOptions: CopyToContainerOptions): this;
withWorkingDir(workingDir: string): this;
/** Resource limits. `memory` is gigabytes; `cpu` is CPUs. */
withResourcesQuota(resourcesQuota: ResourcesQuota): this;
/** `/dev/shm` size in bytes. */
withSharedMemorySize(bytes: number): this;
withLogConsumer(logConsumer: (stream: Readable) => unknown): this;
withHostname(hostname: string): this;
}

export interface RestartOptions {
/** Stop timeout before restart, in milliseconds. */
timeout: number;
}

export interface StopOptions {
/** Stop timeout in milliseconds. */
timeout: number;
remove: boolean;
removeVolumes: boolean;
Expand All @@ -89,7 +94,12 @@ export interface StartedTestContainer extends AsyncDisposable {
copyFilesToContainer(filesToCopy: FileToCopy[]): Promise<void>;
copyContentToContainer(contentsToCopy: ContentToCopy[]): Promise<void>;
exec(command: string | string[], opts?: Partial<ExecOptions>): Promise<ExecResult>;
logs(opts?: { since?: number; tail?: number }): Promise<Readable>;
/**
* Stream container logs.
* @param opts.since Unix timestamp in **seconds**, or a `Date` (converted to seconds). Millisecond values such as `Date.now()` are far in the future for Docker.
* @param opts.tail Number of lines from the end of the logs, or omit for the full stream.
*/
logs(opts?: { since?: number | Date; tail?: number }): Promise<Readable>;
}

export interface StoppedTestContainer {
Expand Down
10 changes: 8 additions & 2 deletions packages/testcontainers/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,21 @@ export type TmpFs = { [dir in string]: string };
export type Ulimits = { [name: string]: { hard: number | undefined; soft: number | undefined } };

export type ResourcesQuota = {
memory?: number; // Memory limit in Gigabytes
cpu?: number; // CPU quota in units of CPUs
/** Memory limit in gigabytes (e.g. `0.5` is 512 MiB). */
memory?: number;
/** CPU quota in CPUs (e.g. `1` is one CPU). */
cpu?: number;
};

export type HealthCheck = {
test: ["CMD-SHELL", string] | ["CMD", ...string[]];
/** Interval between checks, in milliseconds. */
interval?: number;
/** Maximum time to allow one check to run, in milliseconds. */
timeout?: number;
/** Consecutive failures before the container is marked unhealthy. */
retries?: number;
/** Grace period before failed checks count, in milliseconds. */
startPeriod?: number;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class CompositeWaitStrategy extends AbstractWaitStrategy {
return this;
}

/** Overall deadline for all composed strategies, in milliseconds. */
public withDeadline(deadline: number): this {
this.deadline = deadline;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class HttpWaitStrategy extends AbstractWaitStrategy {
return this;
}

/** HTTP read timeout in milliseconds. */
public withReadTimeout(readTimeoutMs: number): this {
this.readTimeoutMs = readTimeoutMs;
return this;
Expand Down
2 changes: 2 additions & 0 deletions packages/testcontainers/src/wait-strategies/wait-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { BoundPorts } from "../utils/bound-ports";
export interface WaitStrategy {
waitUntilReady(container: Dockerode.Container, boundPorts: BoundPorts, startTime?: Date): Promise<void>;

/** Wait-strategy timeout in milliseconds. */
withStartupTimeout(startupTimeoutMs: number): WaitStrategy;

isStartupTimeoutSet(): boolean;
Expand All @@ -21,6 +22,7 @@ export abstract class AbstractWaitStrategy implements WaitStrategy {
startTime?: Date
): Promise<void>;

/** Wait-strategy timeout in milliseconds. */
public withStartupTimeout(startupTimeoutMs: number): this {
this.startupTimeoutMs = startupTimeoutMs;
this.startupTimeoutSet = true;
Expand Down