testcontainers 9.1.2 → 9.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -793,7 +793,7 @@ const { GenericContainer } = require("testcontainers");
793
793
 
794
794
  const container = await new GenericContainer("redis")
795
795
  .withExposedPorts(6379)
796
- .withStartupTimeout(120000)
796
+ .withStartupTimeout(120000) // wait 120s
797
797
  .start();
798
798
  ```
799
799
 
@@ -1,3 +1,3 @@
1
1
  import Dockerode from "dockerode";
2
2
  import { Readable } from "stream";
3
- export declare const containerLogs: (container: Dockerode.Container) => Promise<Readable>;
3
+ export declare const containerLogs: (container: Dockerode.Container, options?: Omit<Dockerode.ContainerLogsOptions, "follow" | "stdout" | "stderr">) => Promise<Readable>;
@@ -13,9 +13,9 @@ exports.containerLogs = void 0;
13
13
  const logger_1 = require("../../../logger");
14
14
  const demux_stream_1 = require("../demux-stream");
15
15
  const docker_client_1 = require("../../docker-client");
16
- const containerLogs = (container) => __awaiter(void 0, void 0, void 0, function* () {
16
+ const containerLogs = (container, options) => __awaiter(void 0, void 0, void 0, function* () {
17
17
  try {
18
- const stream = (yield container.logs({ follow: true, stdout: true, stderr: true }));
18
+ const stream = (yield container.logs(Object.assign({ follow: true, stdout: true, stderr: true }, options)));
19
19
  stream.socket.unref();
20
20
  return (0, demux_stream_1.demuxStream)((yield (0, docker_client_1.dockerClient)()).dockerode, stream);
21
21
  }
@@ -11,6 +11,8 @@ export declare type InspectResult = {
11
11
  state: {
12
12
  status: string;
13
13
  running: boolean;
14
+ startedAt: Date;
15
+ finishedAt: Date | undefined;
14
16
  };
15
17
  labels: Labels;
16
18
  };
@@ -14,13 +14,19 @@ const logger_1 = require("../../../logger");
14
14
  const inspectContainer = (container) => __awaiter(void 0, void 0, void 0, function* () {
15
15
  try {
16
16
  const inspectResult = yield container.inspect();
17
+ const finishedAt = new Date(inspectResult.State.FinishedAt);
17
18
  return {
18
19
  name: inspectResult.Name,
19
20
  hostname: inspectResult.Config.Hostname,
20
21
  ports: getPorts(inspectResult),
21
22
  healthCheckStatus: getHealthCheckStatus(inspectResult),
22
23
  networkSettings: getNetworkSettings(inspectResult),
23
- state: { status: inspectResult.State.Status, running: inspectResult.State.Running },
24
+ state: {
25
+ status: inspectResult.State.Status,
26
+ running: inspectResult.State.Running,
27
+ startedAt: new Date(inspectResult.State.StartedAt),
28
+ finishedAt: finishedAt.getTime() < 0 ? undefined : finishedAt,
29
+ },
24
30
  labels: inspectResult.Config.Labels,
25
31
  };
26
32
  }
@@ -59,7 +59,7 @@ export declare class GenericContainer implements TestContainer {
59
59
  withExposedPorts(...ports: PortWithOptionalBinding[]): this;
60
60
  withBindMounts(bindMounts: BindMount[]): this;
61
61
  withHealthCheck(healthCheck: HealthCheck): this;
62
- withStartupTimeout(startupTimeout: number): this;
62
+ withStartupTimeout(startupTimeoutMs: number): this;
63
63
  withWaitStrategy(waitStrategy: WaitStrategy): this;
64
64
  withDefaultLogDriver(): this;
65
65
  withPrivilegedMode(): this;
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.GenericContainer = void 0;
16
16
  const archiver_1 = __importDefault(require("archiver"));
17
+ const async_lock_1 = __importDefault(require("async-lock"));
17
18
  const bound_ports_1 = require("../bound-ports");
18
19
  const logger_1 = require("../logger");
19
20
  const pull_policy_1 = require("../pull-policy");
@@ -36,6 +37,7 @@ const hash_1 = require("../hash");
36
37
  const get_container_1 = require("../docker/functions/container/get-container");
37
38
  const labels_1 = require("../labels");
38
39
  const wait_for_container_1 = require("../wait-for-container");
40
+ const reusableContainerCreationLock = new async_lock_1.default();
39
41
  class GenericContainer {
40
42
  constructor(image) {
41
43
  this.image = image;
@@ -101,15 +103,20 @@ class GenericContainer {
101
103
  const containerHash = (0, hash_1.hash)(JSON.stringify(createContainerOptions));
102
104
  createContainerOptions.labels = Object.assign(Object.assign({}, createContainerOptions.labels), { [labels_1.LABEL_CONTAINER_HASH]: containerHash });
103
105
  logger_1.log.debug(`Container reuse has been enabled, hash: ${containerHash}`);
104
- const container = yield (0, get_container_1.getContainerByHash)(containerHash);
105
- if (container !== undefined) {
106
- logger_1.log.debug(`Found container to reuse with hash: ${containerHash}`);
107
- return this.reuseContainer(container);
108
- }
109
- else {
110
- logger_1.log.debug("No container found to reuse");
111
- return this.startContainer(createContainerOptions);
112
- }
106
+ // We might have several async processes try to create a reusable container
107
+ // at once, to avoid possibly creating too many of these, use a lock
108
+ // on the containerHash, this ensures that only single reusable instance is created
109
+ return reusableContainerCreationLock.acquire(containerHash, () => __awaiter(this, void 0, void 0, function* () {
110
+ const container = yield (0, get_container_1.getContainerByHash)(containerHash);
111
+ if (container !== undefined) {
112
+ logger_1.log.debug(`Found container to reuse with hash: ${containerHash}`);
113
+ return this.reuseContainer(container);
114
+ }
115
+ else {
116
+ logger_1.log.debug("No container found to reuse");
117
+ return this.startContainer(createContainerOptions);
118
+ }
119
+ }));
113
120
  }
114
121
  else {
115
122
  return this.startContainer(createContainerOptions);
@@ -240,8 +247,8 @@ class GenericContainer {
240
247
  this.healthCheck = healthCheck;
241
248
  return this;
242
249
  }
243
- withStartupTimeout(startupTimeout) {
244
- this.startupTimeout = startupTimeout;
250
+ withStartupTimeout(startupTimeoutMs) {
251
+ this.startupTimeout = startupTimeoutMs;
245
252
  return this;
246
253
  }
247
254
  withWaitStrategy(waitStrategy) {
@@ -81,7 +81,7 @@ class HttpWaitStrategy extends wait_strategy_1.AbstractWaitStrategy {
81
81
  catch (_a) {
82
82
  return undefined;
83
83
  }
84
- }), (response) => {
84
+ }), (response) => __awaiter(this, void 0, void 0, function* () {
85
85
  if (response === undefined) {
86
86
  return false;
87
87
  }
@@ -89,9 +89,15 @@ class HttpWaitStrategy extends wait_strategy_1.AbstractWaitStrategy {
89
89
  return response.ok;
90
90
  }
91
91
  else {
92
- return this.predicates.every((predicate) => predicate(response));
92
+ for (const predicate of this.predicates) {
93
+ const result = yield predicate(response);
94
+ if (!result) {
95
+ return false;
96
+ }
97
+ }
98
+ return true;
93
99
  }
94
- }, () => {
100
+ }), () => {
95
101
  throw new Error(`URL ${this.path} not accessible after ${this.startupTimeout}ms for ${container.id}`);
96
102
  }, this.startupTimeout);
97
103
  });
@@ -1,17 +1,17 @@
1
1
  import { Clock, Time } from "./clock";
2
2
  export interface RetryStrategy<T, U> {
3
- retryUntil(fn: () => Promise<T>, predicate: (result: T) => boolean, onTimeout: () => U, timeout: number): Promise<T | U>;
3
+ retryUntil(fn: () => Promise<T>, predicate: (result: T) => boolean | Promise<boolean>, onTimeout: () => U, timeout: number): Promise<T | U>;
4
4
  }
5
5
  declare abstract class AbstractRetryStrategy<T, U> implements RetryStrategy<T, U> {
6
6
  protected readonly clock: Clock;
7
7
  protected constructor(clock?: Clock);
8
- abstract retryUntil(fn: () => Promise<T>, predicate: (result: T) => boolean, onTimeout: () => U, timeout: number): Promise<T | U>;
8
+ abstract retryUntil(fn: () => Promise<T>, predicate: (result: T) => boolean | Promise<boolean>, onTimeout: () => U, timeout: number): Promise<T | U>;
9
9
  protected hasTimedOut(timeout: number, startTime: Time): boolean;
10
10
  protected wait(duration: number): Promise<void>;
11
11
  }
12
12
  export declare class IntervalRetryStrategy<T, U> extends AbstractRetryStrategy<T, U> {
13
13
  private readonly interval;
14
14
  constructor(interval: number);
15
- retryUntil(fn: (attempt: number) => Promise<T>, predicate: (result: T) => boolean, onTimeout: () => U, timeout: number): Promise<T | U>;
15
+ retryUntil(fn: (attempt: number) => Promise<T>, predicate: (result: T) => boolean | Promise<boolean>, onTimeout: () => U, timeout: number): Promise<T | U>;
16
16
  }
17
17
  export {};
@@ -32,7 +32,7 @@ class IntervalRetryStrategy extends AbstractRetryStrategy {
32
32
  const startTime = this.clock.getTime();
33
33
  let attemptNumber = 0;
34
34
  let result = yield fn(attemptNumber++);
35
- while (!predicate(result)) {
35
+ while (!(yield predicate(result))) {
36
36
  if (this.hasTimedOut(timeout, startTime)) {
37
37
  return onTimeout();
38
38
  }
@@ -17,7 +17,7 @@ export interface TestContainer {
17
17
  withExposedPorts(...ports: PortWithOptionalBinding[]): this;
18
18
  withBindMounts(bindMounts: BindMount[]): this;
19
19
  withWaitStrategy(waitStrategy: WaitStrategy): this;
20
- withStartupTimeout(startupTimeout: number): this;
20
+ withStartupTimeout(startupTimeoutMs: number): this;
21
21
  withNetwork(network: StartedNetwork): this;
22
22
  withNetworkMode(networkMode: string): this;
23
23
  withExtraHosts(extraHosts: ExtraHost[]): this;
@@ -23,6 +23,8 @@ export declare type Log = string;
23
23
  export declare class LogWaitStrategy extends AbstractWaitStrategy {
24
24
  private readonly message;
25
25
  constructor(message: Log | RegExp);
26
+ private containerRestartedLogOptions;
27
+ private hasContainerRestarted;
26
28
  waitUntilReady(container: Dockerode.Container): Promise<void>;
27
29
  }
28
30
  export declare class HealthCheckWaitStrategy extends AbstractWaitStrategy {
@@ -74,10 +74,26 @@ class LogWaitStrategy extends AbstractWaitStrategy {
74
74
  super();
75
75
  this.message = message;
76
76
  }
77
+ containerRestartedLogOptions(inspectResult) {
78
+ const { finishedAt } = inspectResult.state;
79
+ if (finishedAt === undefined) {
80
+ return undefined;
81
+ }
82
+ return {
83
+ since: finishedAt.getTime() / 1000,
84
+ timestamps: true,
85
+ };
86
+ }
87
+ hasContainerRestarted(inspectResult) {
88
+ const { finishedAt, status } = inspectResult.state;
89
+ return finishedAt !== undefined && status === "running";
90
+ }
77
91
  waitUntilReady(container) {
78
92
  return __awaiter(this, void 0, void 0, function* () {
79
93
  logger_1.log.debug(`Waiting for log message "${this.message}"`);
80
- const stream = yield (0, container_logs_1.containerLogs)(container);
94
+ const inspect = yield (0, inspect_container_1.inspectContainer)(container);
95
+ const logsOptions = this.hasContainerRestarted(inspect) ? this.containerRestartedLogOptions(inspect) : undefined;
96
+ const stream = yield (0, container_logs_1.containerLogs)(container, logsOptions);
81
97
  return new Promise((resolve, reject) => {
82
98
  const startupTimeout = this.startupTimeout;
83
99
  const timeout = setTimeout(() => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "testcontainers",
3
3
  "author": "Cristian Greco",
4
- "version": "9.1.2",
4
+ "version": "9.1.3",
5
5
  "main": "dist/index",
6
6
  "types": "dist/index",
7
7
  "keywords": [
@@ -35,26 +35,28 @@
35
35
  "dependencies": {
36
36
  "@balena/dockerignore": "^1.0.2",
37
37
  "@types/archiver": "^5.3.1",
38
- "@types/dockerode": "^3.3.12",
38
+ "@types/dockerode": "^3.3.14",
39
39
  "archiver": "^5.3.1",
40
+ "async-lock": "^1.4.0",
40
41
  "byline": "^5.0.0",
41
42
  "debug": "^4.3.4",
42
- "docker-compose": "^0.23.17",
43
+ "docker-compose": "^0.23.19",
43
44
  "dockerode": "^3.3.4",
44
45
  "get-port": "^5.1.1",
45
- "node-fetch": "^2.6.7",
46
+ "node-fetch": "^2.6.9",
46
47
  "properties-reader": "^2.2.0",
47
48
  "ssh-remote-port-forward": "^1.0.4",
48
49
  "tar-fs": "^2.1.1"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@elastic/elasticsearch": "^7.17.0",
53
+ "@types/async-lock": "^1.4.0",
52
54
  "@types/byline": "^4.2.33",
53
55
  "@types/debug": "4.1.7",
54
- "@types/jest": "^29.2.0",
55
- "@types/node": "^18.11.7",
56
+ "@types/jest": "^29.2.6",
57
+ "@types/node": "^18.11.19",
56
58
  "@types/node-fetch": "^2.6.2",
57
- "@types/pg": "^8.6.5",
59
+ "@types/pg": "^8.6.6",
58
60
  "@types/properties-reader": "^2.1.1",
59
61
  "@types/tar-fs": "^2.0.1",
60
62
  "@typescript-eslint/eslint-plugin": "^5.41.0",
@@ -63,18 +65,18 @@
63
65
  "cross-env": "^7.0.3",
64
66
  "eslint": "^8.26.0",
65
67
  "eslint-config-prettier": "^8.5.0",
66
- "husky": "^8.0.1",
68
+ "husky": "^8.0.3",
67
69
  "jest": "^29.2.2",
68
- "kafkajs": "^2.2.2",
69
- "lint-staged": "^13.0.3",
70
+ "kafkajs": "^2.2.3",
71
+ "lint-staged": "^13.0.4",
72
+ "mongoose": "^6.7.5",
70
73
  "mysql2": "^2.3.3",
71
- "mongoose": "^6.7.0",
72
74
  "nats": "^2.8.0",
73
75
  "neo4j-driver": "^5.2.0",
74
76
  "pg": "^8.8.0",
75
77
  "prettier": "^2.7.1",
76
78
  "rimraf": "^3.0.2",
77
- "ts-jest": "^29.0.3",
79
+ "ts-jest": "^29.0.5",
78
80
  "ts-node": "^10.9.1",
79
81
  "typescript": "^4.8.4",
80
82
  "wait-for-expect": "^3.0.2"