Skip to content

feat(smoke-tests): test Setup.exe installer for on Windows COMPASS-8706 #6671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 10, 2025
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
2 changes: 1 addition & 1 deletion .evergreen/functions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ functions:
npm i -w packages/compass-smoke-tests https://x-access-token:${generated_token}@github.com/10gen/compass-mongodb-com --engine-strict=false

if [[ "$IS_WINDOWS" == "true" ]]; then
# TODO: windows_setup
npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_setup --tests time-to-first-query
npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_zip --tests auto-update-from
npm run --unsafe-perm --workspace @mongodb-js/compass-smoke-tests start -- --package=windows_msi --tests auto-update-from
fi
Expand Down
3 changes: 3 additions & 0 deletions packages/compass-smoke-tests/src/installers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { installMacDMG } from './mac-dmg';
import { installMacZIP } from './mac-zip';
import { installWindowsZIP } from './windows-zip';
import { installWindowsMSI } from './windows-msi';
import { installWindowsSetup } from './windows-setup';

export function getInstaller(kind: PackageKind) {
if (kind === 'osx_dmg') {
Expand All @@ -13,6 +14,8 @@ export function getInstaller(kind: PackageKind) {
return installWindowsZIP;
} else if (kind === 'windows_msi') {
return installWindowsMSI;
} else if (kind === 'windows_setup') {
return installWindowsSetup;
} else {
throw new Error(`Installer for '${kind}' is not yet implemented`);
}
Expand Down
53 changes: 53 additions & 0 deletions packages/compass-smoke-tests/src/installers/windows-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import cp from 'node:child_process';

type RegistryEntry = {
key: string;
type: string;
value: string;
};

function isRegistryEntry(value: unknown): value is RegistryEntry {
return (
typeof value === 'object' &&
value !== null &&
'key' in value &&
typeof value.key === 'string' &&
'type' in value &&
typeof value.type === 'string' &&
'value' in value &&
typeof value.value === 'string'
);
}

/**
* Parse the putput of a "reg query" call.
*/
function parseQueryRegistryOutput(output: string): RegistryEntry[] {
const result = output.matchAll(
/^\s*(?<key>\w+) +(?<type>\w+) *(?<value>.*)$/gm
);
return [...result].map(({ groups }) => groups).filter(isRegistryEntry);
}

/**
* Query the Windows registry by key.
*/
export function query(key: string) {
const result = cp.spawnSync('reg', ['query', key], { encoding: 'utf8' });
if (result.status === 0) {
const entries = parseQueryRegistryOutput(result.stdout);
return Object.fromEntries(entries.map(({ key, value }) => [key, value]));
} else if (
result.status === 1 &&
result.stderr.trim() ===
'ERROR: The system was unable to find the specified registry key or value.'
) {
return null;
} else {
throw Error(
`Expected either an entry or status code 1, got status ${
result.status ?? '?'
}: ${result.stderr}`
);
}
}
83 changes: 83 additions & 0 deletions packages/compass-smoke-tests/src/installers/windows-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import path from 'node:path';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import cp from 'node:child_process';

import type { InstalledAppInfo, InstallablePackage } from './types';
import { execute } from '../execute';
import * as windowsRegistry from './windows-registry';

type UninstallOptions = {
/**
* Expect the app to already be uninstalled, warn if that's not the case.
*/
expectMissing?: boolean;
};

/**
* Install using the Windows installer.
*/
export function installWindowsSetup({
appName,
filepath,
}: InstallablePackage): InstalledAppInfo {
function queryRegistry() {
return windowsRegistry.query(
`HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${appName}`
);
}

function uninstall({ expectMissing = false }: UninstallOptions = {}) {
const entry = queryRegistry();
if (entry) {
const {
InstallLocation: installLocation,
UninstallString: uninstallCommand,
} = entry;
assert(
typeof installLocation === 'string',
'Expected an entry in the registry with the install location'
);
assert(
typeof uninstallCommand === 'string',
'Expected an entry in the registry with the uninstall command'
);
if (expectMissing) {
console.warn(
'Found an existing registry entry (likely from a previous run)'
);
}
assert(
typeof uninstallCommand === 'string',
'Expected an UninstallString in the registry entry'
);
console.log(`Running command to uninstall: ${uninstallCommand}`);
cp.execSync(uninstallCommand, { stdio: 'inherit' });
// Removing the any remaining files manually
fs.rmSync(installLocation, { recursive: true, force: true });
}
}

uninstall({ expectMissing: true });

// Run the installer
console.warn(
"Installing globally, since we haven't discovered a way to specify an install path"
);
execute(filepath, []);

const entry = queryRegistry();
assert(entry !== null, 'Expected an entry in the registry after installing');
const { InstallLocation: appPath } = entry;
assert(
typeof appPath === 'string',
'Expected an entry in the registry with the install location'
);
const appExecutablePath = path.resolve(appPath, `${appName}.exe`);
execute(appExecutablePath, ['--version']);

return {
appPath,
uninstall,
};
}
Loading