diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index 5b075ade7e6..0fd64ce3bcd 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -678,8 +678,8 @@ functions: if [[ "$IS_WINDOWS" == "true" ]]; then # TODO: windows_setup - 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 + 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 if [[ "$IS_OSX" == "true" ]]; then diff --git a/packages/compass-e2e-tests/tests/auto-update.test.ts b/packages/compass-e2e-tests/tests/auto-update.test.ts index aa399e5ed9e..f2da620e2ec 100644 --- a/packages/compass-e2e-tests/tests/auto-update.test.ts +++ b/packages/compass-e2e-tests/tests/auto-update.test.ts @@ -13,66 +13,94 @@ function wait(ms: number) { } describe('Auto-update', function () { - it('auto-update from', async function () { - if (process.env.TEST_NAME !== 'auto-update-from') { - // we don't want this test to execute along with all the others under - // normal circumstances because it is destructive - it overwrites Compass - // itself - this.skip(); - } - - // run the app and wait for it to auto-update - console.log('starting compass the first time'); - const compass = await init('auto-update from', { firstRun: true }); - const { browser } = compass; - try { - await browser.$(Selectors.AutoUpdateToast).waitForDisplayed(); - - if (process.env.AUTO_UPDATE_UPDATABLE === 'true') { - const restartButton = browser.$(Selectors.AutoUpdateRestartButton); - await restartButton.waitForDisplayed(); - - // We could click the restart button to apply the update and restart the - // app, but restarting the app confuses webdriverio or at least our test - // helpers. So we're going to just restart the app manually. - await browser.pause(1000); - } else { - // When auto-update is not supported the toast contains a link to - // download - const linkElement = browser.$(Selectors.AutoUpdateDownloadLink); - await linkElement.waitForDisplayed(); - expect(await linkElement.getAttribute('href')).to.equal( - 'https://www.mongodb.com/try/download/compass?utm_source=compass&utm_medium=product' - ); + for (const testName of ['auto-update-from', 'auto-update-to']) { + it(testName, async function () { + if (process.env.TEST_NAME !== testName) { + // we don't want this test to execute along with all the others under + // normal circumstances because it is destructive - it overwrites Compass + // itself + this.skip(); } - } finally { - await browser.screenshot(screenshotPathName('auto-update-from')); - await cleanup(compass); - } - if (process.env.AUTO_UPDATE_UPDATABLE === 'true') { - console.log( - 'pause to make sure the app properly exited before starting again' - ); - await wait(10_000); - - console.log('starting compass a second time'); - // run the app again and check that the version changed - const compass = await init('auto-update from restart', { - firstRun: false, - }); + // run the app and wait for it to auto-update + console.log('starting compass the first time'); + const compass = await init(testName, { firstRun: true }); const { browser } = compass; try { await browser.$(Selectors.AutoUpdateToast).waitForDisplayed(); - await browser - .$(Selectors.AutoUpdateReleaseNotesLink) - .waitForDisplayed(); + + if (process.env.AUTO_UPDATE_UPDATABLE === 'true') { + const restartButton = browser.$(Selectors.AutoUpdateRestartButton); + await restartButton.waitForDisplayed(); + + if (process.env.EXPECTED_UPDATE_VERSION) { + expect( + await browser.$(Selectors.AutoUpdateToast).getText() + ).to.contain( + `Compass is ready to update to ${process.env.EXPECTED_UPDATE_VERSION}!` + ); + } + + // We could click the restart button to apply the update and restart the + // app, but restarting the app confuses webdriverio or at least our test + // helpers. So we're going to just restart the app manually. + await browser.pause(1000); + } else { + // When auto-update is not supported the toast contains a link to + // download + const linkElement = browser.$(Selectors.AutoUpdateDownloadLink); + await linkElement.waitForDisplayed(); + expect(await linkElement.getAttribute('href')).to.equal( + 'https://www.mongodb.com/try/download/compass?utm_source=compass&utm_medium=product' + ); + + if (process.env.EXPECTED_UPDATE_VERSION) { + expect( + await browser.$(Selectors.AutoUpdateToast).getText() + ).to.contain( + `Compass ${process.env.EXPECTED_UPDATE_VERSION} is available` + ); + } + } } finally { - await browser.screenshot( - screenshotPathName('auto-update-from-restart') - ); + await browser.screenshot(screenshotPathName(testName)); await cleanup(compass); } - } - }); + + if (process.env.AUTO_UPDATE_UPDATABLE === 'true') { + console.log( + 'pause to make sure the app properly exited before starting again' + ); + await wait(10_000); + + console.log('starting compass a second time'); + // run the app again and check that the version changed + const compass = await init(`${testName} restart`, { + firstRun: false, + }); + const { browser } = compass; + try { + await browser.$(Selectors.AutoUpdateToast).waitForDisplayed(); + const releaseNotesLink = browser.$( + Selectors.AutoUpdateReleaseNotesLink + ); + await releaseNotesLink.waitForDisplayed(); + // for now we only know the new version in the auto-update-to case + if (process.env.EXPECTED_UPDATE_VERSION) { + expect( + await browser.$(Selectors.AutoUpdateToast).getText() + ).to.contain( + `Compass ${process.env.EXPECTED_UPDATE_VERSION} installed successfully` + ); + expect(await releaseNotesLink.getAttribute('href')).to.equal( + `https://github.com/mongodb-js/compass/releases/tag/v${process.env.EXPECTED_UPDATE_VERSION}` + ); + } + } finally { + await browser.screenshot(screenshotPathName(`${testName}-restart`)); + await cleanup(compass); + } + } + }); + } }); diff --git a/packages/compass-smoke-tests/src/build-info.ts b/packages/compass-smoke-tests/src/build-info.ts index 61d4f9b9791..860bf0a8f41 100644 --- a/packages/compass-smoke-tests/src/build-info.ts +++ b/packages/compass-smoke-tests/src/build-info.ts @@ -5,17 +5,20 @@ import path from 'node:path'; import { handler as writeBuildInfo } from 'hadron-build/commands/info'; import { type PackageKind } from './packages'; -import { type SmokeTestsContext } from './context'; +import { type SmokeTestsContextWithSandbox } from './context'; import { pick } from 'lodash'; -function assertObjectHasKeys( - obj: unknown, - name: string, - keys: readonly string[] -) { +const SUPPORTED_CHANNELS = ['dev', 'beta', 'stable'] as const; + +export type Channel = typeof SUPPORTED_CHANNELS[number]; + +function assertObjectHasKeys< + Keys extends readonly string[], + Obj extends Record +>(obj: unknown, name: string, keys: Keys): asserts obj is Obj { assert( typeof obj === 'object' && obj !== null, - 'Expected buildInfo to be an object' + `Expected ${name} to be an object` ); for (const key of keys) { @@ -25,13 +28,21 @@ function assertObjectHasKeys( // subsets of the hadron-build info result -export const commonKeys = ['productName'] as const; -export type CommonBuildInfo = Record; +export const commonKeys = ['productName', 'version', 'channel'] as const; +export type CommonBuildInfo = Record & { + channel: Channel; +}; export function assertCommonBuildInfo( buildInfo: unknown ): asserts buildInfo is CommonBuildInfo { assertObjectHasKeys(buildInfo, 'buildInfo', commonKeys); + assert( + SUPPORTED_CHANNELS.includes(buildInfo.channel as Channel), + `Expected ${JSON.stringify( + buildInfo.channel + )} to be in ${SUPPORTED_CHANNELS.join(',')}` + ); } export const windowsFilenameKeys = [ @@ -221,7 +232,7 @@ export function readPackageDetails( } export function writeAndReadPackageDetails( - context: SmokeTestsContext + context: SmokeTestsContextWithSandbox ): PackageDetails { const compassDir = path.resolve(__dirname, '../../compass'); const infoArgs = { diff --git a/packages/compass-smoke-tests/src/cli.ts b/packages/compass-smoke-tests/src/cli.ts index 1f3facf907b..05d9b83e1c2 100755 --- a/packages/compass-smoke-tests/src/cli.ts +++ b/packages/compass-smoke-tests/src/cli.ts @@ -1,33 +1,17 @@ #!/usr/bin/env npx ts-node -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import path from 'node:path'; -import { once } from 'node:events'; - import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { pick } from 'lodash'; -import { execute, executeAsync } from './execute'; -import { - type PackageDetails, - readPackageDetails, - writeAndReadPackageDetails, -} from './build-info'; -import { createSandbox } from './directories'; -import { downloadFile } from './downloads'; -import { type PackageKind, SUPPORTED_PACKAGES } from './packages'; +import { SUPPORTED_TESTS } from './tests/types'; import { type SmokeTestsContext } from './context'; - -import { installMacDMG } from './installers/mac-dmg'; -import { installMacZIP } from './installers/mac-zip'; -import { installWindowsZIP } from './installers/windows-zip'; -import { installWindowsMSI } from './installers/windows-msi'; +import { SUPPORTED_PACKAGES } from './packages'; +import { testTimeToFirstQuery } from './tests/time-to-first-query'; +import { testAutoUpdateFrom } from './tests/auto-update-from'; +import { testAutoUpdateTo } from './tests/auto-update-to'; const SUPPORTED_PLATFORMS = ['win32', 'darwin', 'linux'] as const; const SUPPORTED_ARCHS = ['x64', 'arm64'] as const; -const SUPPORTED_TESTS = ['time-to-first-query', 'auto-update-from'] as const; - function isSupportedPlatform( value: unknown ): value is typeof SUPPORTED_PLATFORMS[number] { @@ -116,74 +100,10 @@ const argv = yargs(hideBin(process.argv)) }) .default('tests', SUPPORTED_TESTS.slice()); -type TestSubject = PackageDetails & { - filepath: string; - /** - * Is the package unsigned? - * In which case we'll expect auto-updating to fail. - */ - unsigned?: boolean; -}; - -/** - * Either finds the local package or downloads the package - */ -async function getTestSubject( - context: SmokeTestsContext -): Promise { - if (context.localPackage) { - const compassDistPath = path.resolve( - __dirname, - '../../packages/compass/dist' - ); - const buildInfoPath = path.resolve(compassDistPath, 'target.json'); - assert( - fs.existsSync(buildInfoPath), - `Expected '${buildInfoPath}' to exist` - ); - const details = readPackageDetails(context.package, buildInfoPath); - return { - ...details, - filepath: path.resolve(compassDistPath, details.filename), - unsigned: true, - }; - } else { - assert( - context.bucketName !== undefined && context.bucketKeyPrefix !== undefined, - 'Bucket name and key prefix are needed to download' - ); - const details = writeAndReadPackageDetails(context); - const filepath = await downloadFile({ - url: `https://${context.bucketName}.s3.amazonaws.com/${context.bucketKeyPrefix}/${details.filename}`, - targetFilename: details.filename, - clearCache: context.forceDownload, - }); - - return { ...details, filepath }; - } -} - -function getInstaller(kind: PackageKind) { - if (kind === 'osx_dmg') { - return installMacDMG; - } else if (kind === 'osx_zip') { - return installMacZIP; - } else if (kind === 'windows_zip') { - return installWindowsZIP; - } else if (kind === 'windows_msi') { - return installWindowsMSI; - } else { - throw new Error(`Installer for '${kind}' is not yet implemented`); - } -} - async function run() { - const context: SmokeTestsContext = { - ...argv.parseSync(), - sandboxPath: createSandbox(), - }; + const context: SmokeTestsContext = argv.parseSync(); - console.log(`Running tests in ${context.sandboxPath}`); + console.log(`Running tests`); console.log( 'context', @@ -198,155 +118,16 @@ async function run() { ]) ); - const { kind, appName, filepath, autoUpdatable } = await getTestSubject( - context - ); - const install = getInstaller(kind); - - try { - if (context.tests.length === 0) { - console.log('Warning: not performing any tests. Pass --tests.'); - } - - for (const testName of context.tests) { - const { appPath, uninstall } = install({ - appName, - filepath, - destinationPath: context.sandboxPath, - }); - - try { - if (testName === 'time-to-first-query') { - // Auto-update does not work on mac in CI at the moment. So in that case - // we just run the E2E tests to make sure the app at least starts up. - runTimeToFirstQuery({ - appName, - appPath, - }); - } - if (testName === 'auto-update-from') { - await runUpdateTest({ - appName, - appPath, - autoUpdatable, - testName, - }); - } - } finally { - await uninstall(); - } - } - } finally { - if (context.skipCleanup) { - console.log(`Skipped cleaning up sandbox: ${context.sandboxPath}`); - } else { - console.log(`Cleaning up sandbox: ${context.sandboxPath}`); - fs.rmSync(context.sandboxPath, { recursive: true }); - } - } -} - -async function importUpdateServer() { - try { - return (await import('compass-mongodb-com')).default; - } catch (err: unknown) { - console.log('Remember to npm link compass-mongodb-com'); - throw err; - } -} - -async function startAutoUpdateServer() { - console.log('Starting auto-update server'); - const { httpServer, updateChecker, start } = (await importUpdateServer())(); - start(); - await once(updateChecker, 'refreshed'); - - return httpServer; -} + for (const testName of context.tests) { + console.log(testName); -type RunE2ETestOptions = { - appName: string; - appPath: string; -}; - -function runTimeToFirstQuery({ appName, appPath }: RunE2ETestOptions) { - execute( - 'npm', - [ - 'run', - '--unsafe-perm', - 'test-packaged', - '--workspace', - 'compass-e2e-tests', - '--', - '--test-filter=time-to-first-query', - ], - { - // We need to use a shell to get environment variables setup correctly - shell: true, - env: { - ...process.env, - COMPASS_APP_NAME: appName, - COMPASS_APP_PATH: appPath, - }, + if (testName === 'time-to-first-query') { + await testTimeToFirstQuery(context); + } else if (testName === 'auto-update-from') { + await testAutoUpdateFrom(context); + } else if (testName === 'auto-update-to') { + await testAutoUpdateTo(context); } - ); -} - -type RunUpdateTestOptions = { - appName: string; - appPath: string; - autoUpdatable?: boolean; - testName: string; -}; - -async function runUpdateTest({ - appName, - appPath, - autoUpdatable, - testName, -}: RunUpdateTestOptions) { - process.env.PORT = '0'; // dynamic port - process.env.UPDATE_CHECKER_ALLOW_DOWNGRADES = 'true'; - - const server = await startAutoUpdateServer(); - - const address = server.address(); - assert(typeof address === 'object' && address !== null); - const port = address.port; - const HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE = `http://localhost:${port}`; - console.log({ HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE }); - - try { - // must be async because the update server is running in the same process - await executeAsync( - 'npm', - [ - 'run', - '--unsafe-perm', - 'test-packaged', - '--workspace', - 'compass-e2e-tests', - '--', - '--test-filter=auto-update', - ], - { - // We need to use a shell to get environment variables setup correctly - shell: true, - env: { - ...process.env, - HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE, - AUTO_UPDATE_UPDATABLE: (!!autoUpdatable).toString(), - TEST_NAME: testName, - COMPASS_APP_NAME: appName, - COMPASS_APP_PATH: appPath, - }, - } - ); - } finally { - console.log('Stopping auto-update server'); - server.close(); - delete process.env.UPDATE_CHECKER_ALLOW_DOWNGRADES; } } diff --git a/packages/compass-smoke-tests/src/context.ts b/packages/compass-smoke-tests/src/context.ts index 268ff41b03c..0f46d634583 100644 --- a/packages/compass-smoke-tests/src/context.ts +++ b/packages/compass-smoke-tests/src/context.ts @@ -1,4 +1,5 @@ import { type PackageKind } from './packages'; +import { type TestName } from './tests/types'; export type SmokeTestsContext = { bucketName?: string; @@ -8,7 +9,10 @@ export type SmokeTestsContext = { package: PackageKind; forceDownload?: boolean; localPackage?: boolean; - sandboxPath: string; - tests: string[]; + tests: TestName[]; skipCleanup: boolean; }; + +export type SmokeTestsContextWithSandbox = SmokeTestsContext & { + sandboxPath: string; +}; diff --git a/packages/compass-smoke-tests/src/installers/index.ts b/packages/compass-smoke-tests/src/installers/index.ts new file mode 100644 index 00000000000..a3ac52ff4d2 --- /dev/null +++ b/packages/compass-smoke-tests/src/installers/index.ts @@ -0,0 +1,19 @@ +import { type PackageKind } from '../packages'; +import { installMacDMG } from './mac-dmg'; +import { installMacZIP } from './mac-zip'; +import { installWindowsZIP } from './windows-zip'; +import { installWindowsMSI } from './windows-msi'; + +export function getInstaller(kind: PackageKind) { + if (kind === 'osx_dmg') { + return installMacDMG; + } else if (kind === 'osx_zip') { + return installMacZIP; + } else if (kind === 'windows_zip') { + return installWindowsZIP; + } else if (kind === 'windows_msi') { + return installWindowsMSI; + } else { + throw new Error(`Installer for '${kind}' is not yet implemented`); + } +} diff --git a/packages/compass-smoke-tests/src/releases.ts b/packages/compass-smoke-tests/src/releases.ts new file mode 100644 index 00000000000..0d8cc807391 --- /dev/null +++ b/packages/compass-smoke-tests/src/releases.ts @@ -0,0 +1,119 @@ +import { downloadFile } from './downloads'; +import { type PackageKind } from './packages'; + +type Arch = 'arm64' | 'x64'; + +type PlatformShortName = + | 'darwin-arm64' + | 'darwin-x64' + | 'windows' + | 'linux_deb' + | 'linux_rpm'; + +/** + * Determines a short name (package type) passable to the update server. + * See https://github.com/10gen/compass-mongodb-com/blob/a1fb99908815d3c3ab0efb5960430cc3faf99a15/src/routes/update.js#L353-L377 for the possible values. + */ +function getPlatformShortName( + arch: Arch, + kind: PackageKind +): PlatformShortName { + if (arch === 'arm64') { + if (kind === 'osx_dmg' || kind === 'osx_zip') { + return 'darwin-arm64'; + } + } + if (arch === 'x64') { + if (kind === 'osx_dmg' || kind === 'osx_zip') { + return 'darwin-x64'; + } + if ( + kind === 'windows_setup' || + kind === 'windows_msi' || + kind === 'windows_zip' + ) { + return 'windows'; + } + if (kind === 'linux_deb' || kind === 'linux_tar') { + return 'linux_deb'; + } + if (kind === 'linux_rpm' || kind === 'rhel_tar') { + return 'linux_rpm'; + } + } + + throw new Error(`Unsupported arch/kind combo: ${arch}/${kind}`); +} + +type PlatformExtension = 'dmg' | 'exe' | 'deb' | 'rpm'; + +function getPlatformExtension( + arch: Arch, + kind: PackageKind +): PlatformExtension { + if (arch === 'arm64') { + if (kind === 'osx_dmg' || kind === 'osx_zip') { + return 'dmg'; + } + } + if (arch === 'x64') { + if (kind === 'osx_dmg' || kind === 'osx_zip') { + return 'dmg'; + } + if ( + kind === 'windows_setup' || + kind === 'windows_msi' || + kind === 'windows_zip' + ) { + return 'exe'; + } + if (kind === 'linux_deb' || kind === 'linux_tar') { + return 'deb'; + } + if (kind === 'linux_rpm' || kind === 'rhel_tar') { + return 'rpm'; + } + } + + throw new Error(`Unsupported arch/kind combo: ${arch}/${kind}`); +} + +export async function getLatestRelease( + channel: 'dev' | 'beta' | 'stable', + arch: Arch, + kind: PackageKind, + forceDownload?: boolean +): Promise { + const shortName = getPlatformShortName(arch, kind); + const extension = getPlatformExtension(arch, kind); + + return await downloadFile({ + url: `http://compass.mongodb.com/api/v2/download/latest/compass/${channel}/${shortName}`, + targetFilename: `latest-${channel}.${extension}`, + clearCache: forceDownload, + }); +} + +export function getLatestReleaseKindByKind(kind: PackageKind): PackageKind { + if (kind === 'osx_dmg' || kind === 'osx_zip') { + return 'osx_dmg'; + } + + if ( + kind === 'windows_setup' || + kind === 'windows_msi' || + kind === 'windows_zip' + ) { + return 'windows_setup'; + } + + if (kind === 'linux_deb' || kind === 'linux_tar') { + return 'linux_deb'; + } + + if (kind === 'linux_rpm' || kind === 'rhel_tar') { + return 'linux_rpm'; + } + + throw new Error(`Unsupported kind: ${kind as string}`); +} diff --git a/packages/compass-smoke-tests/src/test-subject.ts b/packages/compass-smoke-tests/src/test-subject.ts new file mode 100644 index 00000000000..97399ea91d2 --- /dev/null +++ b/packages/compass-smoke-tests/src/test-subject.ts @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { type SmokeTestsContextWithSandbox } from './context'; + +import { + type PackageDetails, + readPackageDetails, + writeAndReadPackageDetails, +} from './build-info'; +import { downloadFile } from './downloads'; + +type TestSubjectDetails = PackageDetails & { + /** + * Is the package unsigned? + * In which case we'll expect auto-updating to fail. + */ + unsigned?: boolean; +}; + +type TestSubject = TestSubjectDetails & { + filepath: string; +}; +/** + * Either uses the local package details or calculates it + */ +export function getTestSubjectDetails( + context: SmokeTestsContextWithSandbox +): TestSubjectDetails { + if (context.localPackage) { + const compassDistPath = path.resolve( + __dirname, + '../../packages/compass/dist' + ); + const buildInfoPath = path.resolve(compassDistPath, 'target.json'); + assert( + fs.existsSync(buildInfoPath), + `Expected '${buildInfoPath}' to exist` + ); + const details = readPackageDetails(context.package, buildInfoPath); + return { + ...details, + unsigned: true, + }; + } else { + return writeAndReadPackageDetails(context); + } +} + +/** + * Either finds the local package or downloads the package + */ +export async function getTestSubject( + context: SmokeTestsContextWithSandbox +): Promise { + const subject = getTestSubjectDetails(context); + if (context.localPackage) { + const compassDistPath = path.resolve( + __dirname, + '../../packages/compass/dist' + ); + return { + ...subject, + filepath: path.resolve(compassDistPath, subject.filename), + }; + } else { + assert( + context.bucketName !== undefined && context.bucketKeyPrefix !== undefined, + 'Bucket name and key prefix are needed to download' + ); + + const filepath = await downloadFile({ + url: `https://${context.bucketName}.s3.amazonaws.com/${context.bucketKeyPrefix}/${subject.filename}`, + targetFilename: subject.filename, + clearCache: context.forceDownload, + }); + + return { ...subject, filepath }; + } +} diff --git a/packages/compass-smoke-tests/src/tests/auto-update-from.ts b/packages/compass-smoke-tests/src/tests/auto-update-from.ts new file mode 100644 index 00000000000..dd6f5df0c04 --- /dev/null +++ b/packages/compass-smoke-tests/src/tests/auto-update-from.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { type SmokeTestsContext } from '../context'; +import { getInstaller } from '../installers'; +import { createSandbox } from '../directories'; +import { getTestSubject } from '../test-subject'; +import { executeAsync } from '../execute'; +import { startAutoUpdateServer } from './update-server'; + +export async function testAutoUpdateFrom(context: SmokeTestsContext) { + const sandboxPath = createSandbox(); + const { kind, appName, filepath, autoUpdatable } = await getTestSubject({ + ...context, + sandboxPath, + }); + + try { + const install = getInstaller(kind); + + const { appPath, uninstall } = install({ + appName, + filepath, + destinationPath: sandboxPath, + }); + + try { + process.env.PORT = '0'; // dynamic port + process.env.UPDATE_CHECKER_ALLOW_DOWNGRADES = 'true'; + + const server = await startAutoUpdateServer(); + + const address = server.address(); + assert(typeof address === 'object' && address !== null); + const port = address.port; + const HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE = `http://localhost:${port}`; + + try { + // must be async because the update server is running in the same process + await executeAsync( + 'npm', + [ + 'run', + '--unsafe-perm', + 'test-packaged', + '--workspace', + 'compass-e2e-tests', + '--', + '--test-filter=auto-update', + ], + { + // We need to use a shell to get environment variables setup correctly + shell: true, + env: { + ...process.env, + HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE, + AUTO_UPDATE_UPDATABLE: (!!autoUpdatable).toString(), + TEST_NAME: 'auto-update-from', + COMPASS_APP_NAME: appName, + COMPASS_APP_PATH: appPath, + }, + } + ); + } finally { + console.log('Stopping auto-update server'); + server.close(); + delete process.env.UPDATE_CHECKER_ALLOW_DOWNGRADES; + } + } finally { + await uninstall(); + } + } finally { + if (context.skipCleanup) { + console.log(`Skipped cleaning up sandbox: ${sandboxPath}`); + } else { + console.log(`Cleaning up sandbox: ${sandboxPath}`); + fs.rmSync(sandboxPath, { recursive: true }); + } + } +} diff --git a/packages/compass-smoke-tests/src/tests/auto-update-to.ts b/packages/compass-smoke-tests/src/tests/auto-update-to.ts new file mode 100644 index 00000000000..b3f8e3bba0a --- /dev/null +++ b/packages/compass-smoke-tests/src/tests/auto-update-to.ts @@ -0,0 +1,107 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { type SmokeTestsContext } from '../context'; +import { getInstaller } from '../installers'; +import { createSandbox } from '../directories'; +import { getTestSubjectDetails } from '../test-subject'; +import { executeAsync } from '../execute'; +import { getLatestRelease, getLatestReleaseKindByKind } from '../releases'; +import { startAutoUpdateServer } from './update-server'; + +export async function testAutoUpdateTo(context: SmokeTestsContext) { + assert( + context.bucketKeyPrefix !== undefined, + 'Bucket key prefix is needed to download' + ); + + const sandboxPath = createSandbox(); + const { + kind, + appName, + autoUpdatable, + buildInfo: { channel, version }, + } = getTestSubjectDetails({ ...context, sandboxPath }); + + try { + const install = getInstaller(getLatestReleaseKindByKind(kind)); + const filepath = await getLatestRelease( + channel, + context.arch, + kind, + context.forceDownload + ); + + const { appPath, uninstall } = install({ + appName, + filepath, + destinationPath: sandboxPath, + }); + + try { + process.env.PORT = '0'; // dynamic port + + if (channel === 'dev') { + process.env.DEV_RELEASE = JSON.stringify({ + version: version, + bucket_key_prefix: context.bucketKeyPrefix, + }); + } else { + process.env.PUBLISHED_RELEASES = JSON.stringify({ + name: version, + body: version, + bucket_key_prefix: context.bucketKeyPrefix, + }); + } + + const server = await startAutoUpdateServer(); + + const address = server.address(); + assert(typeof address === 'object' && address !== null); + const port = address.port; + const HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE = `http://localhost:${port}`; + + try { + // must be async because the update server is running in the same process + await executeAsync( + 'npm', + [ + 'run', + '--unsafe-perm', + 'test-packaged', + '--workspace', + 'compass-e2e-tests', + '--', + '--test-filter=auto-update', + ], + { + // We need to use a shell to get environment variables setup correctly + shell: true, + env: { + ...process.env, + HADRON_AUTO_UPDATE_ENDPOINT_OVERRIDE, + AUTO_UPDATE_UPDATABLE: (!!autoUpdatable).toString(), + TEST_NAME: 'auto-update-to', + EXPECTED_UPDATE_VERSION: version, + COMPASS_APP_NAME: appName, + COMPASS_APP_PATH: appPath, + }, + } + ); + } finally { + console.log('Stopping auto-update server'); + server.close(); + delete process.env.DEV_RELEASE; + delete process.env.PUBLISHED_RELEASES; + } + } finally { + await uninstall(); + } + } finally { + if (context.skipCleanup) { + console.log(`Skipped cleaning up sandbox: ${sandboxPath}`); + } else { + console.log(`Cleaning up sandbox: ${sandboxPath}`); + fs.rmSync(sandboxPath, { recursive: true }); + } + } +} diff --git a/packages/compass-smoke-tests/src/tests/time-to-first-query.ts b/packages/compass-smoke-tests/src/tests/time-to-first-query.ts new file mode 100644 index 00000000000..9566cb92ffc --- /dev/null +++ b/packages/compass-smoke-tests/src/tests/time-to-first-query.ts @@ -0,0 +1,57 @@ +import fs from 'node:fs'; +import { type SmokeTestsContext } from '../context'; +import { execute } from '../execute'; +import { getInstaller } from '../installers'; +import { createSandbox } from '../directories'; +import { getTestSubject } from '../test-subject'; + +export async function testTimeToFirstQuery(context: SmokeTestsContext) { + const sandboxPath = createSandbox(); + const { kind, appName, filepath } = await getTestSubject({ + ...context, + sandboxPath, + }); + + try { + const install = getInstaller(kind); + + const { appPath, uninstall } = install({ + appName, + filepath, + destinationPath: sandboxPath, + }); + + try { + execute( + 'npm', + [ + 'run', + '--unsafe-perm', + 'test-packaged', + '--workspace', + 'compass-e2e-tests', + '--', + '--test-filter=time-to-first-query', + ], + { + // We need to use a shell to get environment variables setup correctly + shell: true, + env: { + ...process.env, + COMPASS_APP_NAME: appName, + COMPASS_APP_PATH: appPath, + }, + } + ); + } finally { + await uninstall(); + } + } finally { + if (context.skipCleanup) { + console.log(`Skipped cleaning up sandbox: ${sandboxPath}`); + } else { + console.log(`Cleaning up sandbox: ${sandboxPath}`); + fs.rmSync(sandboxPath, { recursive: true }); + } + } +} diff --git a/packages/compass-smoke-tests/src/tests/types.ts b/packages/compass-smoke-tests/src/tests/types.ts new file mode 100644 index 00000000000..df5695dc8f9 --- /dev/null +++ b/packages/compass-smoke-tests/src/tests/types.ts @@ -0,0 +1,7 @@ +export const SUPPORTED_TESTS = [ + 'time-to-first-query', + 'auto-update-from', + 'auto-update-to', +] as const; + +export type TestName = typeof SUPPORTED_TESTS[number]; diff --git a/packages/compass-smoke-tests/src/tests/update-server.ts b/packages/compass-smoke-tests/src/tests/update-server.ts new file mode 100644 index 00000000000..fd0c149213e --- /dev/null +++ b/packages/compass-smoke-tests/src/tests/update-server.ts @@ -0,0 +1,19 @@ +import { once } from 'node:events'; + +async function importUpdateServer() { + try { + return (await import('compass-mongodb-com')).default; + } catch (err: unknown) { + console.log('Remember to npm link compass-mongodb-com'); + throw err; + } +} + +export async function startAutoUpdateServer() { + console.log('Starting auto-update server'); + const { httpServer, updateChecker, start } = (await importUpdateServer())(); + start(); + await once(updateChecker, 'refreshed'); + + return httpServer; +}