Skip to content

Commit f8cc25f

Browse files
authored
feat(smoke-tests): test MSI installer for on Windows COMPASS-8707 (#6640)
* Add skipCleanup option * Throw ExecuteFailure with status and signal * Add MSI installer * Update CI to run windows_msi
1 parent fa77bbb commit f8cc25f

File tree

6 files changed

+83
-12
lines changed

6 files changed

+83
-12
lines changed

.evergreen/functions.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,9 +668,8 @@ functions:
668668
eval $(.evergreen/print-compass-env.sh)
669669
670670
if [[ "$IS_WINDOWS" == "true" ]]; then
671-
# TODO: windows_setup
672-
# TODO: windows_msi
673671
npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_zip
672+
npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_msi
674673
fi
675674
676675
if [[ "$IS_OSX" == "true" ]]; then

packages/compass-smoke-tests/src/cli.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { type SmokeTestsContext } from './context';
2020
import { installMacDMG } from './installers/mac-dmg';
2121
import { installMacZIP } from './installers/mac-zip';
2222
import { installWindowsZIP } from './installers/windows-zip';
23+
import { installWindowsMSI } from './installers/windows-msi';
2324

2425
const SUPPORTED_PLATFORMS = ['win32', 'darwin', 'linux'] as const;
2526
const SUPPORTED_ARCHS = ['x64', 'arm64'] as const;
@@ -98,6 +99,11 @@ const argv = yargs(hideBin(process.argv))
9899
.option('localPackage', {
99100
type: 'boolean',
100101
description: 'Use the local package instead of downloading',
102+
})
103+
.option('skipCleanup', {
104+
type: 'boolean',
105+
description: 'Do not delete the sandbox after a run',
106+
default: false,
101107
});
102108

103109
type TestSubject = PackageDetails & {
@@ -154,6 +160,8 @@ function getInstaller(kind: PackageKind) {
154160
return installMacZIP;
155161
} else if (kind === 'windows_zip') {
156162
return installWindowsZIP;
163+
} else if (kind === 'windows_msi') {
164+
return installWindowsMSI;
157165
} else {
158166
throw new Error(`Installer for '${kind}' is not yet implemented`);
159167
}
@@ -195,8 +203,12 @@ async function run() {
195203
await uninstall();
196204
}
197205
} finally {
198-
console.log('Cleaning up sandbox');
199-
fs.rmSync(context.sandboxPath, { recursive: true });
206+
if (context.skipCleanup) {
207+
console.log(`Skipped cleaning up sandbox: ${context.sandboxPath}`);
208+
} else {
209+
console.log(`Cleaning up sandbox: ${context.sandboxPath}`);
210+
fs.rmSync(context.sandboxPath, { recursive: true });
211+
}
200212
}
201213
}
202214

packages/compass-smoke-tests/src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export type SmokeTestsContext = {
99
forceDownload?: boolean;
1010
localPackage?: boolean;
1111
sandboxPath: string;
12+
skipCleanup: boolean;
1213
};
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1-
import assert from 'node:assert/strict';
21
import { spawnSync, type SpawnOptions } from 'node:child_process';
32

3+
export class ExecuteFailure extends Error {
4+
constructor(
5+
public command: string,
6+
public args: string[],
7+
public status: number | null,
8+
public signal: NodeJS.Signals | null
9+
) {
10+
const commandDetails = `${command} ${args.join(' ')}`;
11+
const statusDetails = `status = ${status || 'null'}`;
12+
const signalDetails = `signal = ${signal || 'null'})`;
13+
super(`${commandDetails} exited with ${statusDetails} ${signalDetails}`);
14+
}
15+
}
16+
417
export function execute(
518
command: string,
619
args: string[],
@@ -10,10 +23,7 @@ export function execute(
1023
stdio: 'inherit',
1124
...options,
1225
});
13-
assert(
14-
status === 0 && signal === null,
15-
`${command} ${args.join(' ')} exited with (status = ${
16-
status || 'null'
17-
}, signal = ${signal || 'null'})`
18-
);
26+
if (status !== 0 || signal !== null) {
27+
throw new ExecuteFailure(command, args, status, signal);
28+
}
1929
}

packages/compass-smoke-tests/src/installers/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ export type InstallablePackage = {
1616

1717
export type InstalledAppInfo = {
1818
appPath: string;
19-
uninstall: () => Promise<void>;
19+
uninstall: () => void | Promise<void>;
2020
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import path from 'node:path';
2+
3+
import type { InstalledAppInfo, InstallablePackage } from './types';
4+
import { execute, ExecuteFailure } from '../execute';
5+
6+
// See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec
7+
8+
export function installWindowsMSI({
9+
appName,
10+
filepath,
11+
destinationPath,
12+
}: InstallablePackage): InstalledAppInfo {
13+
const installDirectory = path.resolve(destinationPath, appName);
14+
const appPath = path.resolve(installDirectory, `${appName}.exe`);
15+
16+
function uninstall() {
17+
execute('msiexec', ['/uninstall', filepath, '/passive']);
18+
}
19+
20+
// Installing an MSI which is already installed is a no-op
21+
// So we uninstall the MSI first (which will use the PackageCode to find the installed application)
22+
// It is fine if the uninstall exists with status 1605, as this happens if the app wasn't already installed.
23+
try {
24+
uninstall();
25+
} catch (err) {
26+
if (err instanceof ExecuteFailure && err.status === 1605) {
27+
console.log(
28+
"Uninstalling before installing failed, which is expected if the app wasn't already installed"
29+
);
30+
} else {
31+
throw err;
32+
}
33+
}
34+
35+
execute('msiexec', [
36+
'/package',
37+
filepath,
38+
'/passive',
39+
`APPLICATIONROOTDIRECTORY=${installDirectory}`,
40+
]);
41+
42+
// Check that the executable will run without being quarantined or similar
43+
execute(appPath, ['--version']);
44+
45+
return {
46+
appPath: installDirectory,
47+
uninstall,
48+
};
49+
}

0 commit comments

Comments
 (0)