Skip to content

Commit 253e49c

Browse files
committed
chore: Refactor the integration test set up so it's easier to maintain
Now, there is a new class, MongoDBClusterProcess, that knows how to start and stop a cluster based on the provided configuration. This decouples the test setup and simplifies any future approaches to consolidate running strategies into one.
1 parent fd5bb03 commit 253e49c

File tree

5 files changed

+123
-99
lines changed

5 files changed

+123
-99
lines changed

tests/accuracy/sdk/describeAccuracyTests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function describeAccuracyTests(accuracyTestConfigs: AccuracyTestConfig[])
6363
eachModel(`$displayName`, function (model) {
6464
const configsWithDescriptions = getConfigsWithDescriptions(accuracyTestConfigs);
6565
const accuracyRunId = `${process.env.MDB_ACCURACY_RUN_ID}`;
66-
const mdbIntegration = setupMongoDBIntegrationTest({}, []);
66+
const mdbIntegration = setupMongoDBIntegrationTest();
6767
const { populateTestData, cleanupTestDatabases } = prepareTestData(mdbIntegration);
6868

6969
let commitSHA: string;

tests/integration/common/connectionManager.oidc.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ describe.skipIf(process.platform !== "linux")("ConnectionManager OIDC Tests", as
144144
defaults: {},
145145
}),
146146
}),
147-
{ enterprise: true, version: mongodbVersion },
148-
serverArgs
147+
{ runner: true, downloadOptions: { enterprise: true, version: mongodbVersion }, serverArgs }
149148
);
150149
}
151150

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import fs from "fs/promises";
2+
import path from "path";
3+
import type { MongoClusterOptions } from "mongodb-runner";
4+
import { GenericContainer } from "testcontainers";
5+
import { MongoCluster } from "mongodb-runner";
6+
import { ShellWaitStrategy } from "testcontainers/build/wait-strategies/shell-wait-strategy.js";
7+
8+
export type MongoRunnerConfiguration = {
9+
runner: true;
10+
downloadOptions: MongoClusterOptions["downloadOptions"];
11+
serverArgs: string[];
12+
};
13+
14+
export type MongoSearchConfiguration = { search: true; image?: string };
15+
export type MongoClusterConfiguration = MongoRunnerConfiguration | MongoSearchConfiguration;
16+
17+
const DOWNLOAD_RETRIES = 10;
18+
19+
export class MongoDBClusterProcess {
20+
static async spinUp(config: MongoClusterConfiguration): Promise<MongoDBClusterProcess> {
21+
if (MongoDBClusterProcess.isSearchOptions(config)) {
22+
const runningContainer = await new GenericContainer(config.image ?? "mongodb/mongodb-atlas-local:8")
23+
.withExposedPorts(27017)
24+
.withCommand(["/usr/local/bin/runner", "server"])
25+
.withWaitStrategy(new ShellWaitStrategy(`mongosh --eval 'db.test.getSearchIndexes()'`))
26+
.start();
27+
28+
return new MongoDBClusterProcess(
29+
() => runningContainer.stop(),
30+
() => `mongodb://${runningContainer.getHost()}:${runningContainer.getMappedPort(27017)}`
31+
);
32+
} else if (MongoDBClusterProcess.isMongoRunnerOptions(config)) {
33+
const { downloadOptions, serverArgs } = config;
34+
35+
const tmpDir = path.join(__dirname, "..", "..", "..", "tmp");
36+
await fs.mkdir(tmpDir, { recursive: true });
37+
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
38+
for (let i = 0; i < DOWNLOAD_RETRIES; i++) {
39+
try {
40+
const mongoCluster = await MongoCluster.start({
41+
tmpDir: dbsDir,
42+
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
43+
topology: "standalone",
44+
version: downloadOptions?.version ?? "8.0.12",
45+
downloadOptions,
46+
args: serverArgs,
47+
});
48+
49+
return new MongoDBClusterProcess(
50+
() => mongoCluster.close(),
51+
() => mongoCluster.connectionString
52+
);
53+
} catch (err) {
54+
if (i < 5) {
55+
// Just wait a little bit and retry
56+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
57+
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
58+
await new Promise((resolve) => setTimeout(resolve, 1000));
59+
} else {
60+
// If we still fail after 5 seconds, try another db dir
61+
console.error(
62+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
63+
`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
64+
);
65+
dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
66+
}
67+
}
68+
}
69+
throw new Error(`Could not download cluster with configuration: ${JSON.stringify(config)}`);
70+
} else {
71+
throw new Error(`Unsupported configuration: ${JSON.stringify(config)}`);
72+
}
73+
}
74+
75+
private constructor(
76+
private readonly tearDownFunction: () => Promise<unknown>,
77+
private readonly connectionStringFunction: () => string
78+
) {}
79+
80+
connectionString(): string {
81+
return this.connectionStringFunction();
82+
}
83+
84+
async close(): Promise<void> {
85+
await this.tearDownFunction();
86+
return;
87+
}
88+
89+
static isConfigurationSupportedInCurrentEnv(config: MongoClusterConfiguration): boolean {
90+
if (MongoDBClusterProcess.isSearchOptions(config) && process.env.GITHUB_ACTIONS === "true") {
91+
return process.platform === "linux";
92+
}
93+
94+
return true;
95+
}
96+
97+
private static isSearchOptions(opt: MongoClusterConfiguration): opt is MongoSearchConfiguration {
98+
return (opt as MongoSearchConfiguration)?.search === true;
99+
}
100+
101+
private static isMongoRunnerOptions(opt: MongoClusterConfiguration): opt is MongoRunnerConfiguration {
102+
return (opt as MongoRunnerConfiguration)?.runner === true;
103+
}
104+
}

tests/integration/tools/mongodb/mongodbHelpers.ts

Lines changed: 16 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { MongoClusterOptions } from "mongodb-runner";
2-
import { MongoCluster } from "mongodb-runner";
31
import path from "path";
42
import { fileURLToPath } from "url";
53
import fs from "fs/promises";
@@ -16,9 +14,8 @@ import {
1614
import type { UserConfig, DriverOptions } from "../../../../src/common/config.js";
1715
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
1816
import { EJSON } from "bson";
19-
import { GenericContainer } from "testcontainers";
20-
import type { StartedTestContainer } from "testcontainers";
21-
import { ShellWaitStrategy } from "testcontainers/build/wait-strategies/shell-wait-strategy.js";
17+
import { MongoDBClusterProcess } from "./mongodbClusterProcess.js";
18+
import type { MongoClusterConfiguration } from "./mongodbClusterProcess.js";
2219

2320
const __dirname = path.dirname(fileURLToPath(import.meta.url));
2421

@@ -52,6 +49,12 @@ const testDataPaths = [
5249
},
5350
];
5451

52+
const DEFAULT_MONGODB_PROCESS_OPTIONS: MongoClusterConfiguration = {
53+
runner: true,
54+
downloadOptions: { enterprise: false },
55+
serverArgs: [],
56+
};
57+
5558
interface MongoDBIntegrationTest {
5659
mongoClient: () => MongoClient;
5760
connectionString: () => string;
@@ -68,11 +71,10 @@ export function describeWithMongoDB(
6871
fn: (integration: MongoDBIntegrationTestCase) => void,
6972
getUserConfig: (mdbIntegration: MongoDBIntegrationTest) => UserConfig = () => defaultTestConfig,
7073
getDriverOptions: (mdbIntegration: MongoDBIntegrationTest) => DriverOptions = () => defaultDriverOptions,
71-
downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration = { enterprise: false },
72-
serverArgs: string[] = []
74+
downloadOptions: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS
7375
): void {
74-
describe.skipIf(!isMongoDBEnvSupported(downloadOptions))(name, () => {
75-
const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions, serverArgs);
76+
describe.skipIf(!MongoDBClusterProcess.isConfigurationSupportedInCurrentEnv(downloadOptions))(name, () => {
77+
const mdbIntegration = setupMongoDBIntegrationTest(downloadOptions);
7678
const integration = setupIntegrationTest(
7779
() => ({
7880
...getUserConfig(mdbIntegration),
@@ -98,35 +100,10 @@ export function describeWithMongoDB(
98100
});
99101
}
100102

101-
function isSearchOptions(
102-
opt: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration
103-
): opt is MongoSearchConfiguration {
104-
return (opt as MongoSearchConfiguration)?.search === true;
105-
}
106-
107-
function isTestContainersCluster(
108-
cluster: MongoCluster | StartedTestContainer | undefined
109-
): cluster is StartedTestContainer {
110-
return !!(cluster as StartedTestContainer)?.stop;
111-
}
112-
113-
function isMongoDBEnvSupported(
114-
downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration
115-
): boolean {
116-
// on GHA, OSX does not support nested virtualisation and we don't release
117-
// windows containers, so for now we can only run these tests in Linux.
118-
if (isSearchOptions(downloadOptions) && process.env.GITHUB_ACTIONS === "true") {
119-
return process.platform === "linux";
120-
}
121-
122-
return true;
123-
}
124-
125103
export function setupMongoDBIntegrationTest(
126-
downloadOptions: MongoClusterOptions["downloadOptions"] | MongoSearchConfiguration,
127-
serverArgs: string[]
104+
configuration: MongoClusterConfiguration = DEFAULT_MONGODB_PROCESS_OPTIONS
128105
): MongoDBIntegrationTest {
129-
let mongoCluster: MongoCluster | StartedTestContainer | undefined;
106+
let mongoCluster: MongoDBClusterProcess | undefined;
130107
let mongoClient: MongoClient | undefined;
131108
let randomDbName: string;
132109

@@ -140,61 +117,11 @@ export function setupMongoDBIntegrationTest(
140117
});
141118

142119
beforeAll(async function () {
143-
if (isSearchOptions(downloadOptions)) {
144-
mongoCluster = await new GenericContainer(downloadOptions.image ?? "mongodb/mongodb-atlas-local:8")
145-
.withExposedPorts(27017)
146-
.withCommand(["/usr/local/bin/runner", "server"])
147-
.withWaitStrategy(new ShellWaitStrategy(`mongosh --eval 'db.test.getSearchIndexes()'`))
148-
.start();
149-
return;
150-
}
151-
152-
// Downloading Windows executables in CI takes a long time because
153-
// they include debug symbols...
154-
const tmpDir = path.join(__dirname, "..", "..", "..", "tmp");
155-
await fs.mkdir(tmpDir, { recursive: true });
156-
157-
// On Windows, we may have a situation where mongod.exe is not fully released by the OS
158-
// before we attempt to run it again, so we add a retry.
159-
let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs");
160-
for (let i = 0; i < 10; i++) {
161-
try {
162-
mongoCluster = await MongoCluster.start({
163-
tmpDir: dbsDir,
164-
logDir: path.join(tmpDir, "mongodb-runner", "logs"),
165-
topology: "standalone",
166-
version: downloadOptions?.version ?? "8.0.12",
167-
downloadOptions,
168-
args: serverArgs,
169-
});
170-
171-
return;
172-
} catch (err) {
173-
if (i < 5) {
174-
// Just wait a little bit and retry
175-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
176-
console.error(`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}`);
177-
await new Promise((resolve) => setTimeout(resolve, 1000));
178-
} else {
179-
// If we still fail after 5 seconds, try another db dir
180-
console.error(
181-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
182-
`Failed to start cluster in ${dbsDir}, attempt ${i}: ${err}. Retrying with a new db dir.`
183-
);
184-
dbsDir = path.join(tmpDir, "mongodb-runner", `dbs${i - 5}`);
185-
}
186-
}
187-
}
188-
189-
throw new Error("Failed to start cluster after 10 attempts");
120+
mongoCluster = await MongoDBClusterProcess.spinUp(configuration);
190121
}, 120_000);
191122

192123
afterAll(async function () {
193-
if (isTestContainersCluster(mongoCluster)) {
194-
await mongoCluster.stop();
195-
} else {
196-
await mongoCluster?.close();
197-
}
124+
await mongoCluster?.close();
198125
mongoCluster = undefined;
199126
});
200127

@@ -203,12 +130,7 @@ export function setupMongoDBIntegrationTest(
203130
throw new Error("beforeAll() hook not ran yet");
204131
}
205132

206-
if (isTestContainersCluster(mongoCluster)) {
207-
const mappedPort = mongoCluster.getMappedPort(27017);
208-
return `mongodb://${mongoCluster.getHost()}:${mappedPort}`;
209-
}
210-
211-
return mongoCluster.connectionString;
133+
return mongoCluster.connectionString();
212134
};
213135

214136
return {
@@ -219,7 +141,6 @@ export function setupMongoDBIntegrationTest(
219141
return mongoClient;
220142
},
221143
connectionString: getConnectionString,
222-
223144
randomDbName: () => randomDbName,
224145
};
225146
}

tests/integration/tools/mongodb/mongodbTool.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const injectedErrorHandler: ConnectionErrorHandler = (error) => {
5252
};
5353

5454
describe("MongoDBTool implementations", () => {
55-
const mdbIntegration = setupMongoDBIntegrationTest({ enterprise: false }, []);
55+
const mdbIntegration = setupMongoDBIntegrationTest();
5656
const executeStub: MockedFunction<() => Promise<CallToolResult>> = vi
5757
.fn()
5858
.mockResolvedValue({ content: [{ type: "text", text: "Something" }] });

0 commit comments

Comments
 (0)