diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index 0fd64ce3bcd..3a450402af7 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -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 diff --git a/packages/compass-smoke-tests/src/installers/index.ts b/packages/compass-smoke-tests/src/installers/index.ts index a3ac52ff4d2..31fc022b0c0 100644 --- a/packages/compass-smoke-tests/src/installers/index.ts +++ b/packages/compass-smoke-tests/src/installers/index.ts @@ -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') { @@ -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`); } diff --git a/packages/compass-smoke-tests/src/installers/windows-registry.ts b/packages/compass-smoke-tests/src/installers/windows-registry.ts new file mode 100644 index 00000000000..452d3185247 --- /dev/null +++ b/packages/compass-smoke-tests/src/installers/windows-registry.ts @@ -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*(?\w+) +(?\w+) *(?.*)$/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}` + ); + } +} diff --git a/packages/compass-smoke-tests/src/installers/windows-setup.ts b/packages/compass-smoke-tests/src/installers/windows-setup.ts new file mode 100644 index 00000000000..fd6e4ec4fa3 --- /dev/null +++ b/packages/compass-smoke-tests/src/installers/windows-setup.ts @@ -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, + }; +}