Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/features/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,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:
Expand Down
4 changes: 2 additions & 2 deletions docs/features/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 15 additions & 15 deletions docs/features/wait-strategies.md
Original file line number Diff line number Diff line change
@@ -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();
```

Expand Down Expand Up @@ -73,18 +73,18 @@ 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");

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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
```

Expand All @@ -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]);

Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/modules/couchbase/src/couchbase-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/testcontainers/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { hash } from "./hash";
export { Logger, buildLog, composeLog, containerLog, execLog, log, pullLog } from "./logger";
export { IntervalRetry, Retry } from "./retry";
export { streamToString } from "./streams";
export * from "./time";
export * from "./type-guards";
export { RandomUuid, Uuid } from "./uuid";
12 changes: 6 additions & 6 deletions packages/testcontainers/src/common/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface Retry<T, U> {
fn: () => Promise<T>,
predicate: (result: T) => boolean | Promise<boolean>,
onTimeout: () => U,
timeout: number
timeoutMs: number
): Promise<T | U>;
}

Expand All @@ -16,11 +16,11 @@ abstract class AbstractRetry<T, U> implements Retry<T, U> {
fn: () => Promise<T>,
predicate: (result: T) => boolean | Promise<boolean>,
onTimeout: () => U,
timeout: number
timeoutMs: number
): Promise<T | U>;

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<void> {
Expand All @@ -37,15 +37,15 @@ export class IntervalRetry<T, U> extends AbstractRetry<T, U> {
fn: (attempt: number) => Promise<T>,
predicate: (result: T) => boolean | Promise<boolean>,
onTimeout: () => U,
timeout: number
timeoutMs: number
): Promise<T | U> {
const startTime = this.clock.getTime();

let attemptNumber = 0;
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);
Expand Down
25 changes: 25 additions & 0 deletions packages/testcontainers/src/common/time.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
3 changes: 3 additions & 0 deletions packages/testcontainers/src/common/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const toSeconds = (ms: number) => Math.trunc(ms * 1e-3);

export const toNanos = (ms: number) => ms * 1e6;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import compose from "docker-compose";
import { log, pullLog } from "../../../common";
import { log, pullLog, toSeconds } from "../../../common";
import { defaultComposeOptions } from "./default-compose-options";
import { ComposeDownOptions, ComposeOptions } from "./types";

Expand Down Expand Up @@ -126,7 +126,7 @@ function composeDownCommandOptions(options: ComposeDownOptions): string[] {
result.push("-v");
}
if (options.timeout) {
result.push("-t", `${options.timeout / 1000}`);
result.push("-t", `${toSeconds(options.timeout)}`);
}
return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, streamToString, toSeconds } from "../../../common";
import { ContainerClient } from "./container-client";
import { ContainerCommitOptions, ContainerStatus, ExecOptions, ExecResult } from "./types";

Expand Down Expand Up @@ -120,8 +120,7 @@ export class DockerContainerClient implements ContainerClient {

async inspect(container: Dockerode.Container): Promise<ContainerInspectInfo> {
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;
Expand All @@ -131,7 +130,7 @@ export class DockerContainerClient implements ContainerClient {
async stop(container: Container, opts?: { timeout: number }): Promise<void> {
try {
log.debug(`Stopping container...`, { containerId: container.id });
await container.stop({ t: opts?.timeout });
await container.stop({ t: toSeconds(opts?.timeout ?? 0) });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: these are the reasons why this is a breaking change

log.debug(`Stopped container`, { containerId: container.id });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
Expand Down Expand Up @@ -267,7 +266,7 @@ export class DockerContainerClient implements ContainerClient {
async restart(container: Container, opts?: { timeout: number }): Promise<void> {
try {
log.debug(`Restarting container...`, { containerId: container.id });
await container.restart({ t: opts?.timeout });
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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class DockerComposeEnvironment {
private pullPolicy: ImagePullPolicy = PullPolicy.defaultPolicy();
private defaultWaitStrategy: WaitStrategy = Wait.forListeningPorts();
private waitStrategy: { [containerName: string]: WaitStrategy } = {};
private startupTimeout?: number;
private startupTimeoutMs?: number;

constructor(composeFilePath: string, composeFiles: string | string[], uuid: Uuid = new RandomUuid()) {
this.composeFilePath = composeFilePath;
Expand Down Expand Up @@ -74,8 +74,8 @@ export class DockerComposeEnvironment {
return this;
}

public withStartupTimeout(startupTimeout: number): this {
this.startupTimeout = startupTimeout;
public withStartupTimeout(startupTimeoutMs: number): this {
this.startupTimeoutMs = startupTimeoutMs;
return this;
}

Expand Down Expand Up @@ -147,8 +147,8 @@ export class DockerComposeEnvironment {
const waitStrategy = this.waitStrategy[containerName]
? this.waitStrategy[containerName]
: this.defaultWaitStrategy;
if (this.startupTimeout !== undefined) {
waitStrategy.withStartupTimeout(this.startupTimeout);
if (this.startupTimeoutMs !== undefined) {
waitStrategy.withStartupTimeout(this.startupTimeoutMs);
}

if (containerLog.enabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, toNanos } from "../common";
import { ContainerRuntimeClient, getContainerRuntimeClient, ImageName } from "../container-runtime";
import { CONTAINER_STATUSES } from "../container-runtime/clients/container/types";
import { StartedNetwork } from "../network/network";
Expand Down Expand Up @@ -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<string, string> = {};
protected exposedPorts: PortWithOptionalBinding[] = [];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -395,8 +395,6 @@ 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,
Expand All @@ -410,7 +408,7 @@ export class GenericContainer implements TestContainer {
}

public withStartupTimeout(startupTimeoutMs: number): this {
this.startupTimeout = startupTimeoutMs;
this.startupTimeoutMs = startupTimeoutMs;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ export class CompositeWaitStrategy extends AbstractWaitStrategy {
});
}

public override withStartupTimeout(startupTimeout: number): this {
public override withStartupTimeout(startupTimeoutMs: number): this {
this.waitStrategies
.filter((waitStrategy) => !waitStrategy.isStartupTimeoutSet())
.forEach((waitStrategy) => waitStrategy.withStartupTimeout(startupTimeout));
.forEach((waitStrategy) => waitStrategy.withStartupTimeout(startupTimeoutMs));
return this;
}

Expand Down
Loading