From 5ed1cebaf6e96f564cd61bd5856be8e02ad020ab Mon Sep 17 00:00:00 2001 From: digital88 Date: Wed, 26 Mar 2025 00:36:23 +0300 Subject: [PATCH 1/6] refactor timeouts --- packages/testcontainers/src/common/index.ts | 1 + packages/testcontainers/src/common/ms.test.ts | 26 +++++++++++++++ packages/testcontainers/src/common/ms.ts | 17 ++++++++++ packages/testcontainers/src/common/retry.ts | 12 +++---- .../clients/compose/compose-client.ts | 28 ++++++++-------- .../container/docker-container-client.ts | 8 +++-- .../docker-compose-environment.ts | 10 +++--- .../generic-container/generic-container.ts | 26 +++++++-------- packages/testcontainers/src/index.ts | 2 +- packages/testcontainers/src/test-container.ts | 2 +- .../composite-wait-strategy.ts | 4 +-- .../health-check-wait-strategy.ts | 4 +-- .../host-port-wait-strategy.ts | 4 +-- .../src/wait-strategies/http-wait-strategy.ts | 32 +++++++++---------- .../src/wait-strategies/log-wait-strategy.ts | 4 +-- .../wait-strategies/shell-wait-strategy.ts | 4 +-- .../wait-strategies/startup-check-strategy.ts | 4 +-- .../src/wait-strategies/wait-strategy.ts | 10 +++--- 18 files changed, 121 insertions(+), 77 deletions(-) create mode 100644 packages/testcontainers/src/common/ms.test.ts create mode 100644 packages/testcontainers/src/common/ms.ts diff --git a/packages/testcontainers/src/common/index.ts b/packages/testcontainers/src/common/index.ts index e3c453f75..a0afc4807 100644 --- a/packages/testcontainers/src/common/index.ts +++ b/packages/testcontainers/src/common/index.ts @@ -1,6 +1,7 @@ export { withFileLock } from "./file-lock"; export { hash } from "./hash"; export { buildLog, composeLog, containerLog, execLog, log, Logger, pullLog } from "./logger"; +export { Ms } from "./ms"; export { IntervalRetry, Retry } from "./retry"; export { streamToString } from "./streams"; export * from "./type-guards"; diff --git a/packages/testcontainers/src/common/ms.test.ts b/packages/testcontainers/src/common/ms.test.ts new file mode 100644 index 000000000..38b2da4dd --- /dev/null +++ b/packages/testcontainers/src/common/ms.test.ts @@ -0,0 +1,26 @@ +import { Ms } from "./ms"; + +describe("Ms", () => { + it.for([[10_000, 10_000]])("should return %i ms from %i ms", ([ms1, ms2]) => { + const t = new Ms(ms1); + expect(t.value()).toEqual(ms2); + }); + it.for([ + [0, 0], + [10, 0], + [999, 0], + [1010, 1], + [1999, 1], + [10_000, 10], + ])("should convert %i ms to %i seconds", ([ms, s]) => { + const t = new Ms(ms); + expect(t.seconds()).toEqual(s); + }); + it.for([ + [0, 0], + [1, 1_000_000], + ])("should convert %i ms to %i ns", ([ms, ns]) => { + const t = new Ms(ms); + expect(t.nanos()).toEqual(ns); + }); +}); diff --git a/packages/testcontainers/src/common/ms.ts b/packages/testcontainers/src/common/ms.ts new file mode 100644 index 000000000..13937542d --- /dev/null +++ b/packages/testcontainers/src/common/ms.ts @@ -0,0 +1,17 @@ +/** + * Represents time interval in milliseconds. + */ +export class Ms { + constructor(private readonly milliseconds: number) { + if (milliseconds < 0) throw new Error("Negative interval is not supported in this context."); + } + public seconds(): number { + return Math.trunc(this.milliseconds * 1e-3); + } + public value(): number { + return this.milliseconds; + } + public nanos(): number { + return this.milliseconds * 1e6; + } +} diff --git a/packages/testcontainers/src/common/retry.ts b/packages/testcontainers/src/common/retry.ts index 42c22db3e..870f06fa1 100644 --- a/packages/testcontainers/src/common/retry.ts +++ b/packages/testcontainers/src/common/retry.ts @@ -5,7 +5,7 @@ export interface Retry { fn: () => Promise, predicate: (result: T) => boolean | Promise, onTimeout: () => U, - timeout: number + timeoutMs: number ): Promise; } @@ -16,11 +16,11 @@ abstract class AbstractRetry implements Retry { fn: () => Promise, predicate: (result: T) => boolean | Promise, onTimeout: () => U, - timeout: number + timeoutMs: number ): Promise; - protected hasTimedOut(timeout: number, startTime: Time): boolean { - return this.clock.getTime() - startTime > timeout; + protected hasTimedOut(timeoutMs: number, startTime: Time): boolean { + return this.clock.getTime() - startTime > timeoutMs; } protected wait(duration: number): Promise { @@ -37,7 +37,7 @@ export class IntervalRetry extends AbstractRetry { fn: (attempt: number) => Promise, predicate: (result: T) => boolean | Promise, onTimeout: () => U, - timeout: number + timeoutMs: number ): Promise { const startTime = this.clock.getTime(); @@ -45,7 +45,7 @@ export class IntervalRetry extends AbstractRetry { let result = await fn(attemptNumber++); while (!(await predicate(result))) { - if (this.hasTimedOut(timeout, startTime)) { + if (this.hasTimedOut(timeoutMs, startTime)) { return onTimeout(); } await this.wait(this.interval); 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..af1f4ee42 100644 --- a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts @@ -1,5 +1,5 @@ import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose"; -import { log, pullLog } from "../../../common"; +import { log, Ms, pullLog } from "../../../common"; import { ComposeInfo } from "../types"; import { defaultComposeOptions } from "./default-compose-options"; import { ComposeDownOptions, ComposeOptions } from "./types"; @@ -53,10 +53,10 @@ class ComposeV1Client implements ComposeClient { try { if (services) { log.info(`Upping Compose environment services ${services.join(", ")}...`); - await v1.upMany(services, await defaultComposeOptions(this.environment, options)); + await v1.upMany(services, defaultComposeOptions(this.environment, options)); } else { log.info(`Upping Compose environment...`); - await v1.upAll(await defaultComposeOptions(this.environment, options)); + await v1.upAll(defaultComposeOptions(this.environment, options)); } log.info(`Upped Compose environment`); } catch (err) { @@ -75,10 +75,10 @@ class ComposeV1Client implements ComposeClient { try { if (services) { log.info(`Pulling Compose environment images "${services.join('", "')}"...`); - await v1.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog })); + await v1.pullMany(services, defaultComposeOptions(this.environment, { ...options, logger: pullLog })); } else { log.info(`Pulling Compose environment images...`); - await v1.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog })); + await v1.pullAll(defaultComposeOptions(this.environment, { ...options, logger: pullLog })); } log.info(`Pulled Compose environment`); } catch (err) { @@ -91,7 +91,7 @@ class ComposeV1Client implements ComposeClient { async stop(options: ComposeOptions): Promise { try { log.info(`Stopping Compose environment...`); - await v1.stop(await defaultComposeOptions(this.environment, options)); + await v1.stop(defaultComposeOptions(this.environment, options)); log.info(`Stopped Compose environment`); } catch (err) { await handleAndRethrow(err, async (error: Error) => @@ -104,7 +104,7 @@ class ComposeV1Client implements ComposeClient { try { log.info(`Downing Compose environment...`); await v1.down({ - ...(await defaultComposeOptions(this.environment, options)), + ...defaultComposeOptions(this.environment, options), commandOptions: composeDownCommandOptions(downOptions), }); log.info(`Downed Compose environment`); @@ -126,10 +126,10 @@ class ComposeV2Client implements ComposeClient { try { if (services) { log.info(`Upping Compose environment services ${services.join(", ")}...`); - await v2.upMany(services, await defaultComposeOptions(this.environment, options)); + await v2.upMany(services, defaultComposeOptions(this.environment, options)); } else { log.info(`Upping Compose environment...`); - await v2.upAll(await defaultComposeOptions(this.environment, options)); + await v2.upAll(defaultComposeOptions(this.environment, options)); } log.info(`Upped Compose environment`); } catch (err) { @@ -148,10 +148,10 @@ class ComposeV2Client implements ComposeClient { try { if (services) { log.info(`Pulling Compose environment images "${services.join('", "')}"...`); - await v2.pullMany(services, await defaultComposeOptions(this.environment, { ...options, logger: pullLog })); + await v2.pullMany(services, defaultComposeOptions(this.environment, { ...options, logger: pullLog })); } else { log.info(`Pulling Compose environment images...`); - await v2.pullAll(await defaultComposeOptions(this.environment, { ...options, logger: pullLog })); + await v2.pullAll(defaultComposeOptions(this.environment, { ...options, logger: pullLog })); } log.info(`Pulled Compose environment`); } catch (err) { @@ -164,7 +164,7 @@ class ComposeV2Client implements ComposeClient { async stop(options: ComposeOptions): Promise { try { log.info(`Stopping Compose environment...`); - await v2.stop(await defaultComposeOptions(this.environment, options)); + await v2.stop(defaultComposeOptions(this.environment, options)); log.info(`Stopped Compose environment`); } catch (err) { await handleAndRethrow(err, async (error: Error) => @@ -177,7 +177,7 @@ class ComposeV2Client implements ComposeClient { try { log.info(`Downing Compose environment...`); await v2.down({ - ...(await defaultComposeOptions(this.environment, options)), + ...defaultComposeOptions(this.environment, options), commandOptions: composeDownCommandOptions(downOptions), }); log.info(`Downed Compose environment`); @@ -222,7 +222,7 @@ function composeDownCommandOptions(options: ComposeDownOptions): string[] { result.push("-v"); } if (options.timeout) { - result.push("-t", `${options.timeout / 1000}`); + result.push("-t", `${new Ms(options.timeout).seconds()}`); } return result; } 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 1b855642b..4f9ac106b 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,7 +9,7 @@ import Dockerode, { } from "dockerode"; import { IncomingMessage } from "http"; import { PassThrough, Readable } from "stream"; -import { execLog, log, streamToString } from "../../../common"; +import { execLog, log, Ms, streamToString } from "../../../common"; import { ContainerClient } from "./container-client"; import { ContainerCommitOptions, ContainerStatus, ExecOptions, ExecResult } from "./types"; @@ -131,7 +131,8 @@ export class DockerContainerClient implements ContainerClient { async stop(container: Container, opts?: { timeout: number }): Promise { try { log.debug(`Stopping container...`, { containerId: container.id }); - await container.stop({ t: opts?.timeout }); + const t = new Ms(opts?.timeout ?? 0).seconds(); + await container.stop({ t }); log.debug(`Stopped container`, { containerId: container.id }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { @@ -256,7 +257,8 @@ export class DockerContainerClient implements ContainerClient { async restart(container: Container, opts?: { timeout: number }): Promise { try { log.debug(`Restarting container...`, { containerId: container.id }); - await container.restart({ t: opts?.timeout }); + const t = new Ms(opts?.timeout ?? 0).seconds(); + await container.restart({ t }); log.debug(`Restarted container`, { containerId: container.id }); } catch (err) { log.error(`Failed to restart container: ${err}`, { containerId: container.id }); diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts index 83dd0a2cb..04015ff1b 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts @@ -24,7 +24,7 @@ export class DockerComposeEnvironment { private environment: Environment = {}; private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy(); private waitStrategy: { [containerName: string]: WaitStrategy } = {}; - private startupTimeout?: number; + private startupTimeoutMs?: number; constructor(composeFilePath: string, composeFiles: string | string[], uuid: Uuid = new RandomUuid()) { this.composeFilePath = composeFilePath; @@ -68,8 +68,8 @@ export class DockerComposeEnvironment { return this; } - public withStartupTimeout(startupTimeout: number): this { - this.startupTimeout = startupTimeout; + public withStartupTimeout(ms: number): this { + this.startupTimeoutMs = ms; return this; } @@ -141,8 +141,8 @@ export class DockerComposeEnvironment { const waitStrategy = this.waitStrategy[containerName] ? this.waitStrategy[containerName] : Wait.forListeningPorts(); - if (this.startupTimeout !== undefined) { - waitStrategy.withStartupTimeout(this.startupTimeout); + if (this.startupTimeoutMs !== undefined) { + waitStrategy.withStartupTimeout(this.startupTimeoutMs); } if (containerLog.enabled()) { diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 5b50bc91c..7afd5d5ad 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -2,7 +2,7 @@ import archiver from "archiver"; import AsyncLock from "async-lock"; import { Container, ContainerCreateOptions, HostConfig } from "dockerode"; import { Readable } from "stream"; -import { containerLog, hash, log } from "../common"; +import { containerLog, hash, log, Ms } from "../common"; import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types"; import { StartedNetwork } from "../network/network"; @@ -46,7 +46,7 @@ export class GenericContainer implements TestContainer { protected hostConfig: HostConfig; protected imageName: ImageName; - protected startupTimeout?: number; + protected startupTimeoutMs?: number; protected waitStrategy: WaitStrategy = Wait.forListeningPorts(); protected environment: Record = {}; protected exposedPorts: PortWithOptionalBinding[] = []; @@ -149,8 +149,8 @@ export class GenericContainer implements TestContainer { const boundPorts = BoundPorts.fromInspectResult(client.info.containerRuntime.hostIps, mappedInspectResult).filter( this.exposedPorts ); - if (this.startupTimeout !== undefined) { - this.waitStrategy.withStartupTimeout(this.startupTimeout); + if (this.startupTimeoutMs !== undefined) { + this.waitStrategy.withStartupTimeout(this.startupTimeoutMs); } await waitForContainer(client, container, this.waitStrategy, boundPorts); @@ -202,8 +202,8 @@ export class GenericContainer implements TestContainer { this.exposedPorts ); - if (this.startupTimeout !== undefined) { - this.waitStrategy.withStartupTimeout(this.startupTimeout); + if (this.startupTimeoutMs !== undefined) { + this.waitStrategy.withStartupTimeout(this.startupTimeoutMs); } if (containerLog.enabled() || this.logConsumer !== undefined) { @@ -395,22 +395,20 @@ export class GenericContainer implements TestContainer { } public withHealthCheck(healthCheck: HealthCheck): this { - const toNanos = (duration: number): number => duration * 1e6; - this.healthCheck = healthCheck; this.createOpts.Healthcheck = { Test: healthCheck.test, - Interval: healthCheck.interval ? toNanos(healthCheck.interval) : 0, - Timeout: healthCheck.timeout ? toNanos(healthCheck.timeout) : 0, - Retries: healthCheck.retries || 0, - StartPeriod: healthCheck.startPeriod ? toNanos(healthCheck.startPeriod) : 0, + Interval: healthCheck.interval ? new Ms(healthCheck.interval).nanos() : 0, + Timeout: healthCheck.timeout ? new Ms(healthCheck.timeout).nanos() : 0, + Retries: healthCheck.retries ?? 0, + StartPeriod: healthCheck.startPeriod ? new Ms(healthCheck.startPeriod).nanos() : 0, }; return this; } - public withStartupTimeout(startupTimeoutMs: number): this { - this.startupTimeout = startupTimeoutMs; + public withStartupTimeout(ms: number): this { + this.startupTimeoutMs = ms; return this; } diff --git a/packages/testcontainers/src/index.ts b/packages/testcontainers/src/index.ts index 08a4e910d..1ec780354 100644 --- a/packages/testcontainers/src/index.ts +++ b/packages/testcontainers/src/index.ts @@ -1,4 +1,4 @@ -export { IntervalRetry, log, RandomUuid, Retry, Uuid } from "./common"; +export { IntervalRetry, log, Ms, 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"; diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index f4154ca78..af74e760c 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -32,7 +32,7 @@ export interface TestContainer { withExposedPorts(...ports: PortWithOptionalBinding[]): this; withBindMounts(bindMounts: BindMount[]): this; withWaitStrategy(waitStrategy: WaitStrategy): this; - withStartupTimeout(startupTimeoutMs: number): this; + withStartupTimeout(ms: number): this; withNetwork(network: StartedNetwork): this; withNetworkMode(networkMode: string): this; withExtraHosts(extraHosts: ExtraHost[]): this; diff --git a/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts index 99dc3c2c5..de90a4323 100644 --- a/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts @@ -39,10 +39,10 @@ export class CompositeWaitStrategy extends AbstractWaitStrategy { }); } - public override withStartupTimeout(startupTimeout: number): this { + public override withStartupTimeout(ms: number): this { this.waitStrategies .filter((waitStrategy) => !waitStrategy.isStartupTimeoutSet()) - .forEach((waitStrategy) => waitStrategy.withStartupTimeout(startupTimeout)); + .forEach((waitStrategy) => waitStrategy.withStartupTimeout(ms)); return this; } diff --git a/packages/testcontainers/src/wait-strategies/health-check-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/health-check-wait-strategy.ts index 47d97d8df..7beca859d 100644 --- a/packages/testcontainers/src/wait-strategies/health-check-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/health-check-wait-strategy.ts @@ -12,12 +12,12 @@ export class HealthCheckWaitStrategy extends AbstractWaitStrategy { async () => (await client.container.inspect(container)).State.Health?.Status, (healthCheckStatus) => healthCheckStatus === "healthy" || healthCheckStatus === "unhealthy", () => { - const timeout = this.startupTimeout; + const timeout = this.startupTimeoutMs; const message = `Health check not healthy after ${timeout}ms`; log.error(message, { containerId: container.id }); throw new Error(message); }, - this.startupTimeout + this.startupTimeoutMs ); if (status !== "healthy") { diff --git a/packages/testcontainers/src/wait-strategies/host-port-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/host-port-wait-strategy.ts index be5b8639d..7224e8889 100644 --- a/packages/testcontainers/src/wait-strategies/host-port-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/host-port-wait-strategy.ts @@ -47,11 +47,11 @@ export class HostPortWaitStrategy extends AbstractWaitStrategy { () => portCheck.isBound(port), (isBound) => isBound, () => { - const message = `Port ${port} not bound after ${this.startupTimeout}ms`; + const message = `Port ${port} not bound after ${this.startupTimeoutMs}ms`; log.error(message, { containerId: container.id }); throw new Error(message); }, - this.startupTimeout + this.startupTimeoutMs ); } } diff --git a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts index ec9ab840f..3b1aa3f83 100644 --- a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts @@ -13,9 +13,9 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { private protocol = "http"; private method = "GET"; private headers: { [key: string]: string } = {}; - private predicates: Array<(response: Response) => Promise> = []; + private readonly predicates: Array<(response: Response) => Promise> = []; private _allowInsecure = false; - private readTimeout = 1000; + private readTimeoutMs = 1000; constructor( private readonly path: string, @@ -25,48 +25,48 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { super(); } - public forStatusCode(statusCode: number): HttpWaitStrategy { + public forStatusCode(statusCode: number): this { this.predicates.push(async (response: Response) => response.status === statusCode); return this; } - public forStatusCodeMatching(predicate: (statusCode: number) => boolean): HttpWaitStrategy { + public forStatusCodeMatching(predicate: (statusCode: number) => boolean): this { this.predicates.push(async (response: Response) => predicate(response.status)); return this; } - public forResponsePredicate(predicate: (response: string) => boolean): HttpWaitStrategy { + public forResponsePredicate(predicate: (response: string) => boolean): this { this.predicates.push(async (response: Response) => predicate(await response.text())); return this; } - public withMethod(method: string): HttpWaitStrategy { + public withMethod(method: string): this { this.method = method; return this; } - public withHeaders(headers: { [key: string]: string }): HttpWaitStrategy { + public withHeaders(headers: { [key: string]: string }): this { this.headers = { ...this.headers, ...headers }; return this; } - public withBasicCredentials(username: string, password: string): HttpWaitStrategy { + public withBasicCredentials(username: string, password: string): this { const base64Encoded = Buffer.from(`${username}:${password}`).toString("base64"); this.headers = { ...this.headers, Authorization: `Basic ${base64Encoded}` }; return this; } - public withReadTimeout(readTimeout: number): HttpWaitStrategy { - this.readTimeout = readTimeout; + public withReadTimeout(ms: number): this { + this.readTimeoutMs = ms; return this; } - public usingTls(): HttpWaitStrategy { + public usingTls(): this { this.protocol = "https"; return this; } - public allowInsecure(): HttpWaitStrategy { + public allowInsecure(): this { this._allowInsecure = true; return this; } @@ -79,7 +79,7 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { const client = await getContainerRuntimeClient(); const { abortOnContainerExit } = this.options; - await new IntervalRetry(this.readTimeout).retryUntil( + await new IntervalRetry(this.readTimeoutMs).retryUntil( async () => { try { const url = `${this.protocol}://${client.info.containerRuntime.host}:${boundPorts.getBinding(this.port)}${ @@ -98,7 +98,7 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { return await fetch(url, { method: this.method, - signal: AbortSignal.timeout(this.readTimeout), + signal: AbortSignal.timeout(this.readTimeoutMs), headers: this.headers, dispatcher: this.getAgent(), }); @@ -126,11 +126,11 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { } }, () => { - const message = `URL ${this.path} not accessible after ${this.startupTimeout}ms`; + const message = `URL ${this.path} not accessible after ${this.startupTimeoutMs}ms`; log.error(message, { containerId: container.id }); throw new Error(message); }, - this.startupTimeout + this.startupTimeoutMs ); if (abortOnContainerExit && containerExited) { diff --git a/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts index 005ef113b..6ccf1f88f 100644 --- a/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/log-wait-strategy.ts @@ -21,10 +21,10 @@ export class LogWaitStrategy extends AbstractWaitStrategy { const stream = await client.container.logs(container, { since: startTime ? startTime.getTime() / 1000 : 0 }); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - const message = `Log message "${this.message}" not received after ${this.startupTimeout}ms`; + const message = `Log message "${this.message}" not received after ${this.startupTimeoutMs}ms`; log.error(message, { containerId: container.id }); reject(new Error(message)); - }, this.startupTimeout); + }, this.startupTimeoutMs); const comparisonFn: (line: string) => boolean = (line: string) => { if (this.message instanceof RegExp) { diff --git a/packages/testcontainers/src/wait-strategies/shell-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/shell-wait-strategy.ts index 7ae2be886..d62ab80a5 100644 --- a/packages/testcontainers/src/wait-strategies/shell-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/shell-wait-strategy.ts @@ -21,11 +21,11 @@ export class ShellWaitStrategy extends AbstractWaitStrategy { }, (exitCode) => exitCode === 0, () => { - const message = `Shell command "${this.command}" not successful after ${this.startupTimeout}ms`; + const message = `Shell command "${this.command}" not successful after ${this.startupTimeoutMs}ms`; log.error(message, { containerId: container.id }); throw new Error(message); }, - this.startupTimeout + this.startupTimeoutMs ); log.debug(`Shell wait strategy complete`, { containerId: container.id }); diff --git a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts index 1604221fb..14db883aa 100644 --- a/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/startup-check-strategy.ts @@ -19,11 +19,11 @@ export abstract class StartupCheckStrategy extends AbstractWaitStrategy { async () => await this.checkStartupState(client.container.dockerode, container.id), (startupStatus) => startupStatus === "SUCCESS" || startupStatus === "FAIL", () => { - const message = `Container not accessible after ${this.startupTimeout}ms`; + const message = `Container not accessible after ${this.startupTimeoutMs}ms`; log.error(message, { containerId: container.id }); return new Error(message); }, - this.startupTimeout + this.startupTimeoutMs ); if (startupStatus instanceof Error) { diff --git a/packages/testcontainers/src/wait-strategies/wait-strategy.ts b/packages/testcontainers/src/wait-strategies/wait-strategy.ts index 241e44ecf..49335f3fb 100644 --- a/packages/testcontainers/src/wait-strategies/wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/wait-strategy.ts @@ -4,7 +4,7 @@ import { BoundPorts } from "../utils/bound-ports"; export interface WaitStrategy { waitUntilReady(container: Dockerode.Container, boundPorts: BoundPorts, startTime?: Date): Promise; - withStartupTimeout(startupTimeout: number): WaitStrategy; + withStartupTimeout(ms: number): WaitStrategy; isStartupTimeoutSet(): boolean; @@ -12,7 +12,7 @@ export interface WaitStrategy { } export abstract class AbstractWaitStrategy implements WaitStrategy { - protected startupTimeout = 60_000; + protected startupTimeoutMs = 60_000; private startupTimeoutSet = false; public abstract waitUntilReady( @@ -21,8 +21,8 @@ export abstract class AbstractWaitStrategy implements WaitStrategy { startTime?: Date ): Promise; - public withStartupTimeout(startupTimeout: number): this { - this.startupTimeout = startupTimeout; + public withStartupTimeout(ms: number): this { + this.startupTimeoutMs = ms; this.startupTimeoutSet = true; return this; } @@ -32,6 +32,6 @@ export abstract class AbstractWaitStrategy implements WaitStrategy { } public getStartupTimeout(): number { - return this.startupTimeout; + return this.startupTimeoutMs; } } From 6069d7b63da8e5196476659eeecb4d6d15f0725c Mon Sep 17 00:00:00 2001 From: digital88 Date: Wed, 26 Mar 2025 16:34:24 +0300 Subject: [PATCH 2/6] add section in docs describing timeouts --- docs/features/advanced.md | 30 +++++++++++++++++++ docs/features/compose.md | 4 +-- docs/features/containers.md | 4 +-- docs/features/wait-strategies.md | 30 +++++++++---------- packages/testcontainers/src/common/ms.test.ts | 12 +++++++- packages/testcontainers/src/common/ms.ts | 4 +-- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/docs/features/advanced.md b/docs/features/advanced.md index c2af420cb..7f43b8efb 100644 --- a/docs/features/advanced.md +++ b/docs/features/advanced.md @@ -1,5 +1,35 @@ # Advanced +## Timeout parameter of APIs + +Testcontainers library provides a set of classes and functions, and some of them expect a parameter named `timeout`, `ms`, or similar to be passed from the caller. This parameters are of type `number`, and the unit of measurement for this parameters across all public APIs is the same: millisecond. For example: + +```js +export interface TestContainer { + ... + withStartupTimeout(ms: number): this; // timeout expected to be passed as milliseconds +} +``` + +The underlying docker APIs may expect different units for timeouts and intervals, and testcontainers library will do the needed conversion under the hood automatically. For example, consider the `stop` method of a container: + +```javascript +const container = await new GenericContainer("alpine").start(); +await container.stop({ timeout: 10_000 }); // testcontainers library expects the timeout to be passed as milliseconds +``` + +The Docker API [expects seconds](https://docs.docker.com/reference/api/engine/version/v1.48/#tag/Container/operation/ContainerStop) to be passed to this API call. The 10_000 ms value will be converted to seconds by testontainers library. + +Keep in mind that conversion from ms to seconds uses truncation to integer, for example: + +``` +5000ms = 5s +3800ms = 3s +500ms = 0s +``` + +You may also pass a *negative* value to function parameters, but this may lead to unexpedted results. + ## Container Runtime Client Testcontainers configures an underlying container runtime to perform its tasks. This runtime works automatically with several providers like Docker, Podman, Colima, Rancher Desktop and Testcontainers Desktop. There are too many usage examples to list here, but here are some common examples: diff --git a/docs/features/compose.md b/docs/features/compose.md index 9fbc066fe..328def0b7 100644 --- a/docs/features/compose.md +++ b/docs/features/compose.md @@ -140,11 +140,11 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF await environment.down(); ``` -If you need to wait for the environment to be downed, you can provide a timeout. The unit of timeout here is **second**: +If you need to wait for the environment to be downed, you can provide a timeout: ```javascript const environment = await new DockerComposeEnvironment(composeFilePath, composeFile).up(); -await environment.down({ timeout: 10 }); // timeout after 10 seconds +await environment.down({ timeout: 10_000 }); // 10 seconds ``` Volumes created by the environment are removed when stopped. This is configurable: diff --git a/docs/features/containers.md b/docs/features/containers.md index 98ac5b95c..7c848dea5 100644 --- a/docs/features/containers.md +++ b/docs/features/containers.md @@ -334,11 +334,11 @@ const container = await new GenericContainer("alpine").start(); await container.stop(); ``` -If you need to wait for the container to be stopped, you can provide a timeout. The unit of timeout option here is **second**: +If you need to wait for the container to be stopped, you can provide a timeout: ```javascript const container = await new GenericContainer("alpine").start(); -await container.stop({ timeout: 10 }); // 10 seconds +await container.stop({ timeout: 10_000 }); // 10 seconds ``` You can disable automatic removal of the container, which is useful for debugging, or if for example you want to copy content from the container once it has stopped: diff --git a/docs/features/wait-strategies.md b/docs/features/wait-strategies.md index 5f6bfa9d7..a44e4fc58 100644 --- a/docs/features/wait-strategies.md +++ b/docs/features/wait-strategies.md @@ -1,12 +1,12 @@ # Wait Strategies -Note that the startup timeout of all wait strategies is configurable. The unit of timeout of wait strategies is **millisecond**: +Note that the startup timeout of all wait strategies is configurable: ```javascript const { GenericContainer } = require("testcontainers"); const container = await new GenericContainer("alpine") - .withStartupTimeout(120000) // wait 120 seconds + .withStartupTimeout(120_000) // 120 seconds .start(); ``` @@ -73,7 +73,7 @@ const { GenericContainer, Wait } = require("testcontainers"); const container = await new GenericContainer("alpine").withWaitStrategy(Wait.forHealthCheck()).start(); ``` -Define your own health check. The unit of timeouts and intervals here is **millisecond**: +Define your own health check: ```javascript const { GenericContainer, Wait } = require("testcontainers"); @@ -81,10 +81,10 @@ const { GenericContainer, Wait } = require("testcontainers"); const container = await new GenericContainer("alpine") .withHealthCheck({ test: ["CMD-SHELL", "curl -f http://localhost || exit 1"], - interval: 1000, - timeout: 3000, + interval: 1000, // 1 second + timeout: 3000, // 3 seconds retries: 5, - startPeriod: 1000, + startPeriod: 1000, // 1 second }) .withWaitStrategy(Wait.forHealthCheck()) .start(); @@ -148,7 +148,7 @@ const container = await new GenericContainer("redis") .withMethod("POST") .withHeaders({ X_CUSTOM_VALUE: "custom" }) .withBasicCredentials("username", "password") - .withReadTimeout(10000)) // timeout after 10 seconds + .withReadTimeout(10_000)) // 10 seconds ``` ### Use TLS @@ -186,7 +186,7 @@ This strategy is intended for use with containers that only run briefly and exit const { GenericContainer, Wait } = require("testcontainers"); const container = await new GenericContainer("alpine") - .withWaitStrategy(Wait.forOneShotStartup())) + .withWaitStrategy(Wait.forOneShotStartup()) .start(); ``` @@ -202,11 +202,11 @@ const container = await new GenericContainer("alpine") .start(); ``` -The composite wait strategy by default will respect each individual wait strategy's startup timeout. The unit of timeouts here is **millisecond**. For example: +The composite wait strategy by default will respect each individual wait strategy's startup timeout. For example: ```javascript -const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // wait 1 second -const w2 = Wait.forLogMessage("READY").withStartupTimeout(2000); // wait 2 seconds +const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // 1 second +const w2 = Wait.forLogMessage("READY").withStartupTimeout(2000); // 2 seconds const composite = Wait.forAll([w1, w2]); @@ -217,21 +217,21 @@ expect(w2.getStartupTimeout()).toBe(2000); The startup timeout of inner wait strategies that have not defined their own startup timeout can be set by setting the startup timeout on the composite: ```javascript -const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // wait 1 second +const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // 1 second const w2 = Wait.forLogMessage("READY"); -const composite = Wait.forAll([w1, w2]).withStartupTimeout(2000); // wait 2 seconds +const composite = Wait.forAll([w1, w2]).withStartupTimeout(2000); // 2 seconds expect(w1.getStartupTimeout()).toBe(1000); expect(w2.getStartupTimeout()).toBe(2000); ``` -The startup timeout of all wait strategies can be controlled by setting a deadline on the composite. In this case, the composite will throw unless all inner wait strategies have resolved before the deadline. The unit of deadline timeout is **millisecond**. +The startup timeout of all wait strategies can be controlled by setting a deadline on the composite. In this case, the composite will throw unless all inner wait strategies have resolved before the deadline. ```javascript const w1 = Wait.forListeningPorts(); const w2 = Wait.forLogMessage("READY"); -const composite = Wait.forAll([w1, w2]).withDeadline(2000); // wait 2 seconds +const composite = Wait.forAll([w1, w2]).withDeadline(2000); // 2 seconds ``` ## Other startup strategies diff --git a/packages/testcontainers/src/common/ms.test.ts b/packages/testcontainers/src/common/ms.test.ts index 38b2da4dd..0c9b2e9e1 100644 --- a/packages/testcontainers/src/common/ms.test.ts +++ b/packages/testcontainers/src/common/ms.test.ts @@ -1,7 +1,11 @@ import { Ms } from "./ms"; describe("Ms", () => { - it.for([[10_000, 10_000]])("should return %i ms from %i ms", ([ms1, ms2]) => { + it.for([ + [10_000, 10_000], + [-1000, -1000], + [0, 0], + ])("should return %i ms from %i ms", ([ms1, ms2]) => { const t = new Ms(ms1); expect(t.value()).toEqual(ms2); }); @@ -12,6 +16,11 @@ describe("Ms", () => { [1010, 1], [1999, 1], [10_000, 10], + [-10, -0], + [-999, -0], + [-1010, -1], + [-1999, -1], + [-10_000, -10], ])("should convert %i ms to %i seconds", ([ms, s]) => { const t = new Ms(ms); expect(t.seconds()).toEqual(s); @@ -19,6 +28,7 @@ describe("Ms", () => { it.for([ [0, 0], [1, 1_000_000], + [-1, -1_000_000], ])("should convert %i ms to %i ns", ([ms, ns]) => { const t = new Ms(ms); expect(t.nanos()).toEqual(ns); diff --git a/packages/testcontainers/src/common/ms.ts b/packages/testcontainers/src/common/ms.ts index 13937542d..1f0b3a7b5 100644 --- a/packages/testcontainers/src/common/ms.ts +++ b/packages/testcontainers/src/common/ms.ts @@ -2,9 +2,7 @@ * Represents time interval in milliseconds. */ export class Ms { - constructor(private readonly milliseconds: number) { - if (milliseconds < 0) throw new Error("Negative interval is not supported in this context."); - } + constructor(private readonly milliseconds: number) {} public seconds(): number { return Math.trunc(this.milliseconds * 1e-3); } From b44b362d5f17ff7c95d4a117c59627191cabf76d Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Fri, 23 May 2025 16:54:12 +0100 Subject: [PATCH 3/6] Updates --- docs/features/advanced.md | 32 +---------------- packages/testcontainers/src/common/index.ts | 2 +- packages/testcontainers/src/common/ms.test.ts | 36 ------------------- packages/testcontainers/src/common/ms.ts | 15 -------- .../testcontainers/src/common/time.test.ts | 25 +++++++++++++ packages/testcontainers/src/common/time.ts | 3 ++ .../clients/compose/compose-client.ts | 5 +-- .../container/docker-container-client.ts | 11 +++--- .../docker-compose-environment.ts | 4 +-- .../generic-container/generic-container.ts | 12 +++---- packages/testcontainers/src/index.ts | 2 +- packages/testcontainers/src/test-container.ts | 2 +- .../composite-wait-strategy.ts | 4 +-- .../src/wait-strategies/http-wait-strategy.ts | 4 +-- .../src/wait-strategies/wait-strategy.ts | 6 ++-- 15 files changed, 54 insertions(+), 109 deletions(-) delete mode 100644 packages/testcontainers/src/common/ms.test.ts delete mode 100644 packages/testcontainers/src/common/ms.ts create mode 100644 packages/testcontainers/src/common/time.test.ts create mode 100644 packages/testcontainers/src/common/time.ts diff --git a/docs/features/advanced.md b/docs/features/advanced.md index 7f43b8efb..5e480ce8b 100644 --- a/docs/features/advanced.md +++ b/docs/features/advanced.md @@ -1,35 +1,5 @@ # Advanced -## Timeout parameter of APIs - -Testcontainers library provides a set of classes and functions, and some of them expect a parameter named `timeout`, `ms`, or similar to be passed from the caller. This parameters are of type `number`, and the unit of measurement for this parameters across all public APIs is the same: millisecond. For example: - -```js -export interface TestContainer { - ... - withStartupTimeout(ms: number): this; // timeout expected to be passed as milliseconds -} -``` - -The underlying docker APIs may expect different units for timeouts and intervals, and testcontainers library will do the needed conversion under the hood automatically. For example, consider the `stop` method of a container: - -```javascript -const container = await new GenericContainer("alpine").start(); -await container.stop({ timeout: 10_000 }); // testcontainers library expects the timeout to be passed as milliseconds -``` - -The Docker API [expects seconds](https://docs.docker.com/reference/api/engine/version/v1.48/#tag/Container/operation/ContainerStop) to be passed to this API call. The 10_000 ms value will be converted to seconds by testontainers library. - -Keep in mind that conversion from ms to seconds uses truncation to integer, for example: - -``` -5000ms = 5s -3800ms = 3s -500ms = 0s -``` - -You may also pass a *negative* value to function parameters, but this may lead to unexpedted results. - ## Container Runtime Client Testcontainers configures an underlying container runtime to perform its tasks. This runtime works automatically with several providers like Docker, Podman, Colima, Rancher Desktop and Testcontainers Desktop. There are too many usage examples to list here, but here are some common examples: @@ -69,4 +39,4 @@ const environment = await containerRuntimeClient.compose.up({ ... }) ```js const network = await containerRuntimeClient.network.create({ ... }) -``` +```** diff --git a/packages/testcontainers/src/common/index.ts b/packages/testcontainers/src/common/index.ts index a0afc4807..e927a1f22 100644 --- a/packages/testcontainers/src/common/index.ts +++ b/packages/testcontainers/src/common/index.ts @@ -1,8 +1,8 @@ export { withFileLock } from "./file-lock"; export { hash } from "./hash"; export { buildLog, composeLog, containerLog, execLog, log, Logger, pullLog } from "./logger"; -export { Ms } from "./ms"; export { IntervalRetry, Retry } from "./retry"; export { streamToString } from "./streams"; +export * from "./time"; export * from "./type-guards"; export { RandomUuid, Uuid } from "./uuid"; diff --git a/packages/testcontainers/src/common/ms.test.ts b/packages/testcontainers/src/common/ms.test.ts deleted file mode 100644 index 0c9b2e9e1..000000000 --- a/packages/testcontainers/src/common/ms.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Ms } from "./ms"; - -describe("Ms", () => { - it.for([ - [10_000, 10_000], - [-1000, -1000], - [0, 0], - ])("should return %i ms from %i ms", ([ms1, ms2]) => { - const t = new Ms(ms1); - expect(t.value()).toEqual(ms2); - }); - it.for([ - [0, 0], - [10, 0], - [999, 0], - [1010, 1], - [1999, 1], - [10_000, 10], - [-10, -0], - [-999, -0], - [-1010, -1], - [-1999, -1], - [-10_000, -10], - ])("should convert %i ms to %i seconds", ([ms, s]) => { - const t = new Ms(ms); - expect(t.seconds()).toEqual(s); - }); - it.for([ - [0, 0], - [1, 1_000_000], - [-1, -1_000_000], - ])("should convert %i ms to %i ns", ([ms, ns]) => { - const t = new Ms(ms); - expect(t.nanos()).toEqual(ns); - }); -}); diff --git a/packages/testcontainers/src/common/ms.ts b/packages/testcontainers/src/common/ms.ts deleted file mode 100644 index 1f0b3a7b5..000000000 --- a/packages/testcontainers/src/common/ms.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Represents time interval in milliseconds. - */ -export class Ms { - constructor(private readonly milliseconds: number) {} - public seconds(): number { - return Math.trunc(this.milliseconds * 1e-3); - } - public value(): number { - return this.milliseconds; - } - public nanos(): number { - return this.milliseconds * 1e6; - } -} diff --git a/packages/testcontainers/src/common/time.test.ts b/packages/testcontainers/src/common/time.test.ts new file mode 100644 index 000000000..7eefc96d8 --- /dev/null +++ b/packages/testcontainers/src/common/time.test.ts @@ -0,0 +1,25 @@ +import { toNanos, toSeconds } from "./time"; + +test.for([ + [0, 0], + [10, 0], + [999, 0], + [1010, 1], + [1999, 1], + [10_000, 10], + [-10, -0], + [-999, -0], + [-1010, -1], + [-1999, -1], + [-10_000, -10], +])("should convert %i ms to %i seconds", ([ms, s]) => { + expect(toSeconds(ms)).toEqual(s); +}); + +test.for([ + [0, 0], + [1, 1_000_000], + [-1, -1_000_000], +])("should convert %i ms to %i ns", ([ms, ns]) => { + expect(toNanos(ms)).toEqual(ns); +}); diff --git a/packages/testcontainers/src/common/time.ts b/packages/testcontainers/src/common/time.ts new file mode 100644 index 000000000..a3342d2b0 --- /dev/null +++ b/packages/testcontainers/src/common/time.ts @@ -0,0 +1,3 @@ +export const toSeconds = (ms: number) => Math.trunc(ms * 1e-3); + +export const toNanos = (ms: number) => ms * 1e6; 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 af1f4ee42..c89a160a0 100644 --- a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts @@ -1,5 +1,6 @@ import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose"; -import { log, Ms, pullLog } from "../../../common"; +import { log, pullLog } from "../../../common"; +import { toSeconds } from "../../../common/time"; import { ComposeInfo } from "../types"; import { defaultComposeOptions } from "./default-compose-options"; import { ComposeDownOptions, ComposeOptions } from "./types"; @@ -222,7 +223,7 @@ function composeDownCommandOptions(options: ComposeDownOptions): string[] { result.push("-v"); } if (options.timeout) { - result.push("-t", `${new Ms(options.timeout).seconds()}`); + result.push("-t", `${toSeconds(options.timeout)}`); } return result; } 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 4f9ac106b..fb7894dfa 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,7 +9,7 @@ import Dockerode, { } from "dockerode"; import { IncomingMessage } from "http"; import { PassThrough, Readable } from "stream"; -import { execLog, log, Ms, streamToString } from "../../../common"; +import { execLog, log, streamToString, toSeconds } from "../../../common"; import { ContainerClient } from "./container-client"; import { ContainerCommitOptions, ContainerStatus, ExecOptions, ExecResult } from "./types"; @@ -120,8 +120,7 @@ export class DockerContainerClient implements ContainerClient { async inspect(container: Dockerode.Container): Promise { try { - const inspectInfo = await container.inspect(); - return inspectInfo; + return await container.inspect(); } catch (err) { log.error(`Failed to inspect container: ${err}`, { containerId: container.id }); throw err; @@ -131,8 +130,7 @@ export class DockerContainerClient implements ContainerClient { async stop(container: Container, opts?: { timeout: number }): Promise { try { log.debug(`Stopping container...`, { containerId: container.id }); - const t = new Ms(opts?.timeout ?? 0).seconds(); - await container.stop({ t }); + await container.stop({ t: toSeconds(opts?.timeout ?? 0) }); log.debug(`Stopped container`, { containerId: container.id }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { @@ -257,8 +255,7 @@ export class DockerContainerClient implements ContainerClient { async restart(container: Container, opts?: { timeout: number }): Promise { try { log.debug(`Restarting container...`, { containerId: container.id }); - const t = new Ms(opts?.timeout ?? 0).seconds(); - await container.restart({ t }); + await container.restart({ t: toSeconds(opts?.timeout ?? 0) }); log.debug(`Restarted container`, { containerId: container.id }); } catch (err) { log.error(`Failed to restart container: ${err}`, { containerId: container.id }); diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts index 04015ff1b..cd60e6875 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts @@ -68,8 +68,8 @@ export class DockerComposeEnvironment { return this; } - public withStartupTimeout(ms: number): this { - this.startupTimeoutMs = ms; + public withStartupTimeout(startupTimeoutMs: number): this { + this.startupTimeoutMs = startupTimeoutMs; return this; } diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 7afd5d5ad..ae2baf35d 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -2,7 +2,7 @@ import archiver from "archiver"; import AsyncLock from "async-lock"; import { Container, ContainerCreateOptions, HostConfig } from "dockerode"; import { Readable } from "stream"; -import { containerLog, hash, log, Ms } from "../common"; +import { containerLog, hash, log, toNanos } from "../common"; import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime"; import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types"; import { StartedNetwork } from "../network/network"; @@ -398,17 +398,17 @@ export class GenericContainer implements TestContainer { this.healthCheck = healthCheck; this.createOpts.Healthcheck = { Test: healthCheck.test, - Interval: healthCheck.interval ? new Ms(healthCheck.interval).nanos() : 0, - Timeout: healthCheck.timeout ? new Ms(healthCheck.timeout).nanos() : 0, + Interval: healthCheck.interval ? toNanos(healthCheck.interval) : 0, + Timeout: healthCheck.timeout ? toNanos(healthCheck.timeout) : 0, Retries: healthCheck.retries ?? 0, - StartPeriod: healthCheck.startPeriod ? new Ms(healthCheck.startPeriod).nanos() : 0, + StartPeriod: healthCheck.startPeriod ? toNanos(healthCheck.startPeriod) : 0, }; return this; } - public withStartupTimeout(ms: number): this { - this.startupTimeoutMs = ms; + public withStartupTimeout(startupTimeoutMs: number): this { + this.startupTimeoutMs = startupTimeoutMs; return this; } diff --git a/packages/testcontainers/src/index.ts b/packages/testcontainers/src/index.ts index 1ec780354..08a4e910d 100644 --- a/packages/testcontainers/src/index.ts +++ b/packages/testcontainers/src/index.ts @@ -1,4 +1,4 @@ -export { IntervalRetry, log, Ms, RandomUuid, Retry, Uuid } from "./common"; +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"; diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index af74e760c..f4154ca78 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -32,7 +32,7 @@ export interface TestContainer { withExposedPorts(...ports: PortWithOptionalBinding[]): this; withBindMounts(bindMounts: BindMount[]): this; withWaitStrategy(waitStrategy: WaitStrategy): this; - withStartupTimeout(ms: number): this; + withStartupTimeout(startupTimeoutMs: number): this; withNetwork(network: StartedNetwork): this; withNetworkMode(networkMode: string): this; withExtraHosts(extraHosts: ExtraHost[]): this; diff --git a/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts index de90a4323..6cd3112ed 100644 --- a/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/composite-wait-strategy.ts @@ -39,10 +39,10 @@ export class CompositeWaitStrategy extends AbstractWaitStrategy { }); } - public override withStartupTimeout(ms: number): this { + public override withStartupTimeout(startupTimeoutMs: number): this { this.waitStrategies .filter((waitStrategy) => !waitStrategy.isStartupTimeoutSet()) - .forEach((waitStrategy) => waitStrategy.withStartupTimeout(ms)); + .forEach((waitStrategy) => waitStrategy.withStartupTimeout(startupTimeoutMs)); return this; } diff --git a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts index 3b1aa3f83..389cd14d7 100644 --- a/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/http-wait-strategy.ts @@ -56,8 +56,8 @@ export class HttpWaitStrategy extends AbstractWaitStrategy { return this; } - public withReadTimeout(ms: number): this { - this.readTimeoutMs = ms; + public withReadTimeout(startupTimeoutMs: number): this { + this.readTimeoutMs = startupTimeoutMs; return this; } diff --git a/packages/testcontainers/src/wait-strategies/wait-strategy.ts b/packages/testcontainers/src/wait-strategies/wait-strategy.ts index 49335f3fb..961776111 100644 --- a/packages/testcontainers/src/wait-strategies/wait-strategy.ts +++ b/packages/testcontainers/src/wait-strategies/wait-strategy.ts @@ -4,7 +4,7 @@ import { BoundPorts } from "../utils/bound-ports"; export interface WaitStrategy { waitUntilReady(container: Dockerode.Container, boundPorts: BoundPorts, startTime?: Date): Promise; - withStartupTimeout(ms: number): WaitStrategy; + withStartupTimeout(startupTimeoutMs: number): WaitStrategy; isStartupTimeoutSet(): boolean; @@ -21,8 +21,8 @@ export abstract class AbstractWaitStrategy implements WaitStrategy { startTime?: Date ): Promise; - public withStartupTimeout(ms: number): this { - this.startupTimeoutMs = ms; + public withStartupTimeout(startupTimeoutMs: number): this { + this.startupTimeoutMs = startupTimeoutMs; this.startupTimeoutSet = true; return this; } From 9c7ec001f2b10264ae2c6707eb0aaf637b85f07f Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Fri, 23 May 2025 16:54:59 +0100 Subject: [PATCH 4/6] Revert typo --- docs/features/advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/advanced.md b/docs/features/advanced.md index 5e480ce8b..c2af420cb 100644 --- a/docs/features/advanced.md +++ b/docs/features/advanced.md @@ -39,4 +39,4 @@ const environment = await containerRuntimeClient.compose.up({ ... }) ```js const network = await containerRuntimeClient.network.create({ ... }) -```** +``` From 01645164f4d4e0f8eb63c7a961c3570018cea758 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Fri, 23 May 2025 16:57:44 +0100 Subject: [PATCH 5/6] Combine import --- .../src/container-runtime/clients/compose/compose-client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 c89a160a0..9aead99fd 100644 --- a/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts +++ b/packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts @@ -1,6 +1,5 @@ import { default as dockerComposeV1, default as v1, v2 as dockerComposeV2, v2 } from "docker-compose"; -import { log, pullLog } from "../../../common"; -import { toSeconds } from "../../../common/time"; +import { log, pullLog, toSeconds } from "../../../common"; import { ComposeInfo } from "../types"; import { defaultComposeOptions } from "./default-compose-options"; import { ComposeDownOptions, ComposeOptions } from "./types"; From 7e6f588351a0ddbb7611dd02795bc860e93de83d Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Sun, 25 May 2025 18:48:04 +0100 Subject: [PATCH 6/6] Fix compilation issue --- packages/modules/couchbase/src/couchbase-container.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/modules/couchbase/src/couchbase-container.ts b/packages/modules/couchbase/src/couchbase-container.ts index 30a34ccb7..92fb1873e 100644 --- a/packages/modules/couchbase/src/couchbase-container.ts +++ b/packages/modules/couchbase/src/couchbase-container.ts @@ -448,12 +448,12 @@ export class CouchbaseContainer extends GenericContainer { return jsonResponse.results[0].present; }, () => { - const message = `URL /query/service not accessible after ${this.startupTimeout || 60_000}ms`; + const message = `URL /query/service not accessible after ${this.startupTimeoutMs || 60_000}ms`; log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id }); throw new Error(message); }, - this.startupTimeout || 60_000 + this.startupTimeoutMs || 60_000 ); } @@ -509,12 +509,12 @@ export class CouchbaseContainer extends GenericContainer { return jsonResponse.results[0].online; }, () => { - const message = `URL /query/service not accessible after ${this.startupTimeout || 60_000}ms`; + const message = `URL /query/service not accessible after ${this.startupTimeoutMs || 60_000}ms`; log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id }); throw new Error(message); }, - this.startupTimeout || 60_000 + this.startupTimeoutMs || 60_000 ); } else { log.info(