diff --git a/.evergreen/buildvariants-and-tasks.in.yml b/.evergreen/buildvariants-and-tasks.in.yml index b9370851955..1ec21489680 100644 --- a/.evergreen/buildvariants-and-tasks.in.yml +++ b/.evergreen/buildvariants-and-tasks.in.yml @@ -80,18 +80,18 @@ const SMOKETEST_BUILD_VARIANTS = [ // run_on: 'rhel80-large', // depends_on: 'package-rhel', // }, -// { -// name: 'smoketest-macos-x64', -// display_name: 'Smoketest MacOS Intel', -// run_on: 'macos-14', -// depends_on: 'package-macos-x64', -// }, -// { -// name: 'smoketest-macos-arm', -// display_name: 'Smoketest MacOS Arm64', -// run_on: 'macos-14-arm64', -// depends_on: 'package-macos-arm', -// } + { + name: 'smoketest-macos-x64', + display_name: 'Smoketest MacOS Intel', + run_on: 'macos-14', + depends_on: 'package-macos-x64', + }, + { + name: 'smoketest-macos-arm', + display_name: 'Smoketest MacOS Arm64', + run_on: 'macos-14-arm64', + depends_on: 'package-macos-arm', + } ]; const TEST_PACKAGED_APP_BUILD_VARIANTS = [ diff --git a/.evergreen/buildvariants-and-tasks.yml b/.evergreen/buildvariants-and-tasks.yml index b5987af6c34..d0a40dec336 100644 --- a/.evergreen/buildvariants-and-tasks.yml +++ b/.evergreen/buildvariants-and-tasks.yml @@ -84,6 +84,22 @@ buildvariants: variant: package-ubuntu tasks: - name: smoketest-compass + - name: smoketest-macos-x64-compass + display_name: Smoketest MacOS Intel (compass) + run_on: macos-14 + depends_on: + - name: package-compass + variant: package-macos-x64 + tasks: + - name: smoketest-compass + - name: smoketest-macos-arm-compass + display_name: Smoketest MacOS Arm64 (compass) + run_on: macos-14-arm64 + depends_on: + - name: package-compass + variant: package-macos-arm + tasks: + - name: smoketest-compass - name: test-eol-servers display_name: Test EoL Servers run_on: ubuntu1804-large diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index 245309cfac5..87690679e90 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -674,7 +674,7 @@ functions: export COMPASS_E2E_DISABLE_CLIPBOARD_USAGE="true" fi - npm run --workspace compass-e2e-tests smoketest + npm run --unsafe-perm --workspace compass-e2e-tests smoketest test-web-sandbox: - command: shell.exec diff --git a/packages/compass-e2e-tests/helpers/chrome-startup-flags.ts b/packages/compass-e2e-tests/helpers/chrome-startup-flags.ts index fb0fc4d3733..fc7ac6dc1b6 100644 --- a/packages/compass-e2e-tests/helpers/chrome-startup-flags.ts +++ b/packages/compass-e2e-tests/helpers/chrome-startup-flags.ts @@ -1,5 +1,6 @@ // Copied from https://github.com/webdriverio/webdriverio/blob/1825c633aead82bc650dff1f403ac30cff7c7cb3/packages/devtools/src/constants.ts // These are the default flags that webdriverio uses to start Chrome driver. +// NOTE: this has since been removed along with the devtools automation protocol https://github.com/webdriverio/webdriverio/commit/28e64e439ffc36a95f24aeda9f1d21111429dfa3#diff-6ea151d6c0687197931735239f397b7f5f0140a588c5b2b82ff584bbe73be069 const DEFAULT_WEBDRIVER_FLAGS = [ // suppresses Save Password prompt window '--enable-automation', diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index 317823b09d0..7a9912de96f 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -547,7 +547,7 @@ async function processCommonOpts({ // https://webdriver.io/docs/options/#webdriver-options const webdriverOptions = { - logLevel: 'warn' as const, // info is super verbose right now + logLevel: 'trace' as const, outputDir: webdriverLogPath, }; @@ -601,7 +601,9 @@ async function startCompassElectron( // See https://www.electronjs.org/docs/latest/api/command-line-switches#--enable-loggingfile '--enable-logging=file', // See https://www.electronjs.org/docs/latest/api/command-line-switches#--log-filepath - `--log-file=${electronLogFile}` + `--log-file=${electronLogFile}`, + // See // https://chromium.googlesource.com/chromium/src/+/master/docs/chrome_os_logging.md + '--log-level=0' ); if (opts.extraSpawnArgs) { @@ -643,9 +645,10 @@ async function startCompassElectron( }, // from https://github.com/webdriverio-community/wdio-electron-service/blob/32457f60382cb4970c37c7f0a19f2907aaa32443/packages/wdio-electron-service/src/launcher.ts#L102 'wdio:enforceWebDriverClassic': true, - }, - 'wdio:chromedriverOptions': { - // TODO: enable logging so we don't have to debug things blindly + 'wdio:chromedriverOptions': { + // enable logging so we don't have to debug things blindly + verbose: true, + }, }, ...webdriverOptions, ...wdioOptions, @@ -657,7 +660,10 @@ async function startCompassElectron( let browser: CompassBrowser; try { - browser = (await remote(options)) as CompassBrowser; + // webdriverio's type is wrong for + // options.capabilities['wdio:chromedriverOptions'] and it doesn't allow + // verbose even though it does work + browser = (await remote(options as any)) as CompassBrowser; } catch (err) { debug('Failed to start remote webdriver session', { error: (err as Error).stack, diff --git a/packages/compass-e2e-tests/installers/helpers.ts b/packages/compass-e2e-tests/installers/helpers.ts new file mode 100644 index 00000000000..b1fb872f97d --- /dev/null +++ b/packages/compass-e2e-tests/installers/helpers.ts @@ -0,0 +1,45 @@ +import { spawn } from 'child_process'; +import type { SpawnOptions } from 'child_process'; + +export function execute( + command: string, + args: string[], + options?: SpawnOptions +): Promise { + return new Promise((resolve, reject) => { + console.log(command, ...args); + const p = spawn(command, args, { + stdio: 'inherit', + ...options, + }); + p.on('error', (err: any) => { + reject(err); + }); + p.on('close', (code: number | null, signal: NodeJS.Signals | null) => { + if (code !== null) { + if (code === 0) { + resolve(); + } else { + reject( + new Error(`${command} ${args.join(' ')} exited with code ${code}`) + ); + } + } else { + if (signal !== null) { + reject( + new Error( + `${command} ${args.join(' ')} exited with signal ${signal}` + ) + ); + } else { + // shouldn't happen + reject( + new Error( + `${command} ${args.join(' ')} exited with no code or signal` + ) + ); + } + } + }); + }); +} diff --git a/packages/compass-e2e-tests/installers/mac-dmg.ts b/packages/compass-e2e-tests/installers/mac-dmg.ts new file mode 100644 index 00000000000..238a2b701e4 --- /dev/null +++ b/packages/compass-e2e-tests/installers/mac-dmg.ts @@ -0,0 +1,57 @@ +import path from 'path'; +import { existsSync } from 'fs'; +import type { InstalledAppInfo, Package } from './types'; +import { execute } from './helpers'; + +export async function installMacDMG( + appName: string, + { filepath }: Package +): Promise { + const fullDestinationPath = `/Applications/${appName}.app`; + + if (existsSync(fullDestinationPath)) { + // Would ideally just throw here, but unfortunately in CI the mac + // environments aren't all clean so somewhere we have to remove it anyway. + console.log(`${fullDestinationPath} already exists. Removing.`); + await execute('rm', ['-rf', fullDestinationPath]); + } + + await execute('hdiutil', ['attach', filepath]); + try { + await execute('cp', [ + '-Rp', + `/Volumes/${appName}/${appName}.app`, + '/Applications', + ]); + } finally { + await execute('hdiutil', ['detach', `/Volumes/${appName}`]); + } + + // get debug output so we can see that it copied everything with the correct + // permissions + await execute('ls', ['-laR', `/Applications/${appName}.app`]); + + // see if the executable will run without being quarantined or similar + await execute(`/Applications/${appName}.app/Contents/MacOS/${appName}`, [ + '--version', + ]); + + if (process.env.HOME) { + const settingsDir = path.resolve( + process.env.HOME, + 'Library', + 'Application Support', + appName + ); + + if (existsSync(settingsDir)) { + console.log(`${settingsDir} already exists. Removing.`); + await execute('rm', ['-rf', settingsDir]); + } + } + + return Promise.resolve({ + appName, + appPath: `/Applications/${appName}.app`, + }); +} diff --git a/packages/compass-e2e-tests/installers/types.ts b/packages/compass-e2e-tests/installers/types.ts new file mode 100644 index 00000000000..70855624aa3 --- /dev/null +++ b/packages/compass-e2e-tests/installers/types.ts @@ -0,0 +1,9 @@ +export type Package = { + filename: string; + filepath: string; +}; + +export type InstalledAppInfo = { + appName: string; + appPath: string; +}; diff --git a/packages/compass-e2e-tests/smoke-test.ts b/packages/compass-e2e-tests/smoke-test.ts index 67a9cd2de64..d6b18da3f89 100755 --- a/packages/compass-e2e-tests/smoke-test.ts +++ b/packages/compass-e2e-tests/smoke-test.ts @@ -7,6 +7,9 @@ import { hideBin } from 'yargs/helpers'; import https from 'https'; import { pick } from 'lodash'; import { handler as writeBuildInfo } from 'hadron-build/commands/info'; +import type { InstalledAppInfo, Package } from './installers/types'; +import { installMacDMG } from './installers/mac-dmg'; +import { execute } from './installers/helpers'; const argv = yargs(hideBin(process.argv)) .scriptName('smoke-tests') @@ -137,6 +140,10 @@ async function run() { writeBuildInfo(infoArgs); const buildInfo = JSON.parse(await fs.readFile(infoArgs.out, 'utf8')); + if (!buildInfoIsCommon(buildInfo)) { + throw new Error('buildInfo is missing'); + } + // filter the extensions given the platform (isWindows, isOSX, isUbuntu, isRHEL) and extension const { isWindows, isOSX, isRHEL, isUbuntu, extension } = context; @@ -150,9 +157,9 @@ async function run() { if (!context.skipDownload) { await Promise.all( - packages.map(async ({ name, filepath }) => { + packages.map(async ({ filename, filepath }) => { await fs.mkdir(path.dirname(filepath), { recursive: true }); - const url = `https://${context.bucketName}.s3.amazonaws.com/${context.bucketKeyPrefix}/${name}`; + const url = `https://${context.bucketName}.s3.amazonaws.com/${context.bucketKeyPrefix}/${filename}`; console.log(url); return downloadFile(url, filepath); }) @@ -162,6 +169,24 @@ async function run() { verifyPackagesExist(packages); // TODO(COMPASS-8533): extract or install each package and then test the Compass binary + for (const pkg of packages) { + let appInfo: InstalledAppInfo | undefined = undefined; + + console.log('installing', pkg.filepath); + + if (pkg.filename.endsWith('.dmg')) { + appInfo = await installMacDMG(buildInfo.productName, pkg); + } + + // TODO: all the other installers go here + + if (appInfo) { + console.log('testing', appInfo.appPath); + await testInstalledApp(appInfo); + } else { + console.log(`no app got installed for ${pkg.filename}`); + } + } } function platformFromContext( @@ -189,6 +214,18 @@ type PackageFilterConfig = Pick< // subsets of the hadron-build info result +const commonKeys = ['productName']; +type CommonBuildInfo = Record; + +function buildInfoIsCommon(buildInfo: any): buildInfo is CommonBuildInfo { + for (const key of commonKeys) { + if (!buildInfo[key]) { + return false; + } + } + return true; +} + const windowsFilenameKeys = [ 'windows_setup_filename', 'windows_msi_filename', @@ -245,11 +282,6 @@ function buildInfoIsRHEL(buildInfo: any): buildInfo is RHELBuildInfo { return true; } -type Package = { - name: string; - filepath: string; -}; - function getFilteredPackages( compassDir: string, buildInfo: any, @@ -282,11 +314,11 @@ function getFilteredPackages( const extension = config.extension; return names - .filter((name) => !extension || name.endsWith(extension)) - .map((name) => { + .filter((filename) => !extension || filename.endsWith(extension)) + .map((filename) => { return { - name, - filepath: path.join(compassDir, 'dist', name), + filename, + filepath: path.join(compassDir, 'dist', filename), }; }); } @@ -333,6 +365,28 @@ function verifyPackagesExist(packages: Package[]): void { } } +async function testInstalledApp(appInfo: InstalledAppInfo): Promise { + await execute( + 'npm', + [ + 'run', + '--unsafe-perm', + 'test-packaged', + '--workspace', + 'compass-e2e-tests', + '--', + '--test-filter=time-to-first-query', + ], + { + env: { + ...process.env, + COMPASS_APP_NAME: appInfo.appName, + COMPASS_APP_PATH: appInfo.appPath, + }, + } + ); +} + run() .then(function () { console.log('done'); diff --git a/packages/compass/src/main/application.ts b/packages/compass/src/main/application.ts index be6e8e8e605..8cccea7a651 100644 --- a/packages/compass/src/main/application.ts +++ b/packages/compass/src/main/application.ts @@ -106,29 +106,38 @@ class CompassApplication { safeStorage.setUsePlainTextEncryption(true); } + process.stdout.write('before setupPreferencesAndUser\n'); const { preferences } = await setupPreferencesAndUser( globalPreferences, safeStorage ); + process.stdout.write('after setupPreferencesAndUser\n'); this.preferences = preferences; await this.setupLogging(); + process.stdout.write('after setupLogging\n'); await this.setupProxySupport(app, 'Application'); + process.stdout.write('after setupProxySupport\n'); // need to happen after setupPreferencesAndUser and setupProxySupport await this.setupTelemetry(); + process.stdout.write('after setupTelemetry\n'); await setupProtocolHandlers( process.argv.includes('--squirrel-uninstall') ? 'uninstall' : 'install', this.preferences ); + process.stdout.write('after setupProtocolHandlers\n'); // needs to happen after setupProtocolHandlers if ((await import('electron-squirrel-startup')).default) { debug('electron-squirrel-startup event handled sucessfully\n'); return; } + process.stdout.write('after electron-squirrel-startup\n'); // Accessing isEncryptionAvailable is not allowed when app is not ready on Windows // https://github.com/electron/electron/issues/33640 await app.whenReady(); + + process.stdout.write('after app.whenReady\n'); log.info( mongoLogId(1_001_000_307), 'Application', @@ -153,12 +162,14 @@ class CompassApplication { { message: (e as Error).message } ); } + process.stdout.write('after connectionStorage.migrateToSafeStorage\n'); if (mode === 'CLI') { return; } await this.setupCORSBypass(); + process.stdout.write('after setupCORSBypass\n'); void this.setupCompassAuthService(); // TODO(COMPASS-7618): For now don't setup auto-update in CI because the // toasts will obscure other things which we don't expect yet. @@ -166,6 +177,7 @@ class CompassApplication { this.setupAutoUpdate(); } await setupCSFLELibrary(); + process.stdout.write('after setupCSFLELibrary\n'); setupTheme(this); this.setupJavaScriptArguments(); this.setupLifecycleListeners(); @@ -182,7 +194,12 @@ class CompassApplication { } private static async setupCompassAuthService() { - await CompassAuthService.init(this.preferences, this.httpClient); + try { + await CompassAuthService.init(this.preferences, this.httpClient); + } catch (err: any) { + process.stdout.write('Got CompassAuthService.init error\n'); + throw err; + } this.addExitHandler(() => { return CompassAuthService.onExit(); }); @@ -298,14 +315,18 @@ class CompassApplication { logContext: string ): Promise<() => void> { const onChange = async (value: string) => { + process.stdout.write('setupProxySupport onChange\n'); try { const proxyOptions = proxyPreferenceToProxyOptions(value); await app.whenReady(); + process.stdout.write('after setupProxySupport app.whenReady\n'); + try { const electronProxyConfig = translateToElectronProxyConfig(proxyOptions); await target.setProxy(electronProxyConfig); + process.stdout.write('after setupProxySupport target.setProxy\n'); } catch (err) { const headline = String( err && typeof err === 'object' && 'message' in err @@ -323,6 +344,7 @@ class CompassApplication { } ); await target.setProxy({}); + process.stdout.write('after setupProxySupport target.setProxy({})\n'); } const agent = createAgent(proxyOptions); @@ -349,6 +371,9 @@ class CompassApplication { ), } ); + process.stdout.write( + 'after setupProxySupport Unable to set proxy configuration\n' + ); } }; const unsubscribe = this.preferences.onPreferenceValueChanged( diff --git a/packages/compass/src/main/index.ts b/packages/compass/src/main/index.ts index 474b75a0c22..6d6484c4ac8 100644 --- a/packages/compass/src/main/index.ts +++ b/packages/compass/src/main/index.ts @@ -29,6 +29,10 @@ process.title = app.getName(); void main(); async function main(): Promise { + process.stdout.write('before first app.whenReady\n'); + await app.whenReady(); + process.stdout.write('after first app.whenReady\n'); + const globalPreferences = await parseAndValidateGlobalPreferences(); // These are expected to go away at some point. @@ -51,6 +55,13 @@ async function main(): Promise { ...globalPreferences.hardcoded, }; const preferenceParseErrorsString = preferenceParseErrors.join('\n'); + + process.stdout.write( + `combined preferences: ${JSON.stringify( + preferences + )}, preferenceParseErrorsString: ${preferenceParseErrorsString}\n` + ); + if (preferences.version) { process.stdout.write(`${app.getName()} ${app.getVersion()}\n`); return app.exit(0); @@ -85,6 +96,8 @@ async function main(): Promise { trackingProps: { context: 'CLI' }, }; + process.stdout.write(`importing CompassApplication\n`); + const { CompassApplication } = await import('./application'); const doImportExport = @@ -124,9 +137,14 @@ async function main(): Promise { }); } + process.stdout.write(`Starting CompassApplication ${mode}\n`); + try { await CompassApplication.init(mode, globalPreferences); } catch (e) { + process.stdout.write( + `Error during CompassApplication.init ${(e as any)?.message ?? ''}\n` + ); if (mode === 'CLI') { process.stderr.write('Exiting due to try/catch:\n'); // eslint-disable-next-line no-console @@ -138,6 +156,8 @@ async function main(): Promise { return; } + process.stdout.write(`Done starting CompassApplication ${mode}\n`); + if (mode === 'CLI') { let exitCode = 0; try {