diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index acf5b475190..4124ba293d1 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -664,7 +664,7 @@ functions: DEBUG: ${debug|} MONGODB_VERSION: ${mongodb_version|} MONGODB_RUNNER_VERSION: ${mongodb_version|} - BROWSER_NAME: ${browser_name} + COMPASS_WEB_BROWSER_NAME: ${browser_name} E2E_TEST_GROUPS: ${e2e_test_groups} E2E_TEST_GROUP: ${e2e_test_group} script: | diff --git a/.evergreen/node-gyp-bug-workaround.sh b/.evergreen/node-gyp-bug-workaround.sh deleted file mode 100755 index 36b233a0fbb..00000000000 --- a/.evergreen/node-gyp-bug-workaround.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -# This is a workaround for a node-gyp bug that has not fully been investigated -# due to problems reproducing it outside of CI environments (even though it -# occurs both in evergreen and github actions). -# Something seems to go wrong when node-gyp extracts the Node.js header tarball, -# on Windows specifically (this is most likely because node-tar treats -# the overwriting of existing files differently on Windows than on other OS -- -# for good reasons, but still). -# The most likely cause of this issue is that node-gyp somehow extracts the -# same headers tarball twice, in parallel, in the same location, with race -# conditions in the tar extraction code leading to issues. -# The extraction result ends up in %LOCALAPPDATA%\node-gyp\Cache. -# Manually extracting the tarballs will solve this issue, so we're doing that -# here. -# For actually resolving the bug, we would probably need somebody with a local -# reproduction. However, it seems likely that other people will also encounter -# this issue, so there's also a good chance that this workaround will just -# not be needed with a future node-gyp version. - -if [ x"$NODE_JS_VERSION" = x"" ]; then - if node -v; then - export NODE_JS_VERSION=$(node -p 'process.version.slice(1)') - else - echo "Need NODE_JS_VERSION to be set or Node.js to be installed for node-gyp bug workaround script" - exit 1 - fi -fi - -if [ x"$LOCALAPPDATA" = x"" ]; then - echo "No LOCALAPPDATA set, ignoring node-gyp bug workaround script" - exit -fi - -set -ex - -SCRIPTDIR="$(cd $(dirname "$0"); pwd)" -CACHEDIR="$LOCALAPPDATA/node-gyp/Cache" -rm -rvf "$CACHEDIR" -mkdir -p "$CACHEDIR/$NODE_JS_VERSION" -cd "$CACHEDIR/$NODE_JS_VERSION" - -bash "${SCRIPTDIR}/retry-with-backoff.sh" curl -sSfLO "https://nodejs.org/download/release/v$NODE_JS_VERSION/node-v$NODE_JS_VERSION-headers.tar.gz" -tar --strip-components=1 -xvzf "node-v$NODE_JS_VERSION-headers.tar.gz" -for arch in x64 x86 arm64; do - mkdir $arch - pushd $arch - bash "${SCRIPTDIR}/retry-with-backoff.sh" curl -sSfLO "https://nodejs.org/download/release/v$NODE_JS_VERSION/win-$arch/node.lib" || echo "no $arch v$NODE_JS_VERSION .lib file" - popd -done - -# Finally, store the right installVersion value for current node-gyp versions -echo 9 > installVersion diff --git a/.evergreen/preinstall.sh b/.evergreen/preinstall.sh index a0e247396e0..bac0ce1249e 100755 --- a/.evergreen/preinstall.sh +++ b/.evergreen/preinstall.sh @@ -39,9 +39,6 @@ if [ -n "$IS_WINDOWS" ]; then ./node.exe node_modules/npm2/bin/npm-cli.js i -g npm@$NPM_VERSION rm -rf node_modules/npm2/ chmod +x npm.cmd npm - - cd .. - .evergreen/node-gyp-bug-workaround.sh else if command -v ldd &> /dev/null && `ldd $(which bash) | grep 'libc.so' | awk '{print $3}'` | grep -Eq 'release version 2.(1|2[0-7])'; then echo "Installing unofficial nodejs compiled for glibc 2.17 v${NODE_JS_VERSION} for ${PLATFORM} on ${ARCH}..." diff --git a/.evergreen/print-compass-env.js b/.evergreen/print-compass-env.js index 38edbe68eb1..1497e9e642c 100755 --- a/.evergreen/print-compass-env.js +++ b/.evergreen/print-compass-env.js @@ -68,13 +68,10 @@ function printCompassEnv() { } if (process.env.PLATFORM === 'linux') { - // We generally require a relatively recent C++ compiler + // To build node modules on linux post electron 13 we need a newer c++ + // compiler version and at least python v3.9, this adds it. + // https://jira.mongodb.org/browse/COMPASS-5150 pathsToPrepend.unshift('/opt/mongodbtoolchain/v4/bin'); - - // no Python 3.1x on ubuntu1604 - pathsToPrepend.unshift('/opt/python/3.9/bin'); - // node-gyp is tied to a specific Python version range - pathsToPrepend.unshift('/opt/python/3.12/bin'); } PATH = maybePrependPaths(PATH, pathsToPrepend); @@ -103,10 +100,16 @@ function printCompassEnv() { printVar('IS_RHEL', process.env.IS_RHEL); printVar('IS_UBUNTU', process.env.IS_UBUNTU); printVar('DEBUG', process.env.DEBUG); - printVar('MONGODB_VERSION', process.env.MONGODB_VERSION || process.env.MONGODB_DEFAULT_VERSION); + printVar( + 'MONGODB_VERSION', + process.env.MONGODB_VERSION || process.env.MONGODB_DEFAULT_VERSION + ); printVar('DEV_VERSION_IDENTIFIER', process.env.DEV_VERSION_IDENTIFIER); printVar('EVERGREEN_REVISION', process.env.EVERGREEN_REVISION); - printVar('EVERGREEN_REVISION_ORDER_ID', process.env.EVERGREEN_REVISION_ORDER_ID); + printVar( + 'EVERGREEN_REVISION_ORDER_ID', + process.env.EVERGREEN_REVISION_ORDER_ID + ); // https://jira.mongodb.org/browse/NODE-6320 printVar('GYP_DEFINES', `kerberos_use_rtld=${process.platform === 'linux'}`); diff --git a/AUTHORS b/AUTHORS index e9a04ad3d3d..9afdce0409c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -92,3 +92,4 @@ admin-token-bot <36773031+admin-token-bot@users.noreply.github.com> Kræn Hansen Kræn Hansen Ruchitha Rajaghatta <77162985+ruchitharajaghatta@users.noreply.github.com> +syn-zhu <167124917+syn-zhu@users.noreply.github.com> diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index cc6829fa259..22c71e496e5 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -1,5 +1,5 @@ The following third-party software is used by and included in **Mongodb Compass**. -This document was automatically generated on Tue Oct 22 2024. +This document was automatically generated on Wed Oct 23 2024. ## List of dependencies diff --git a/docs/tracking-plan.md b/docs/tracking-plan.md index e3c5260cc42..88308e14087 100644 --- a/docs/tracking-plan.md +++ b/docs/tracking-plan.md @@ -1,7 +1,7 @@ # Compass Tracking Plan -Generated on Tue, Oct 22, 2024 at 02:00 PM +Generated on Wed, Oct 23, 2024 at 11:59 AM ## Table of Contents diff --git a/package.json b/package.json index f2bb606cfd4..30538aae301 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,6 @@ "package-compass": "npm run package-compass --workspace=mongodb-compass --", "package-compass-debug": "npm run package-compass-debug --workspace=mongodb-compass --", "package-compass-nocompile": "npm run package-compass-nocompile --workspace=mongodb-compass --", - "prestart": "npm run compile --workspace=@mongodb-js/webpack-config-compass", - "prestart-web": "npm run prestart", "start": "npm run start --workspace=mongodb-compass", "start-web": "npm run start --workspace=@mongodb-js/compass-web", "test": "lerna run test --concurrency 1 --stream", diff --git a/packages/compass-e2e-tests/.depcheckrc b/packages/compass-e2e-tests/.depcheckrc index a14dc086cad..e3dc5af3444 100644 --- a/packages/compass-e2e-tests/.depcheckrc +++ b/packages/compass-e2e-tests/.depcheckrc @@ -2,9 +2,12 @@ ignores: - '@mongodb-js/prettier-config-compass' - '@mongodb-js/tsconfig-compass' - '@wdio/types' - - 'mongodb-compass' - 'ps-list' - - 'mongodb-runner' +# Avoiding recursive deps on monorepo workspaces + - 'mongodb-compass' + - '@mongodb-js/mocha-config-compass' + - 'compass-e2e-tests' + - '@mongodb-js/compass-web' # TODO(COMPASS-8312): depcheck doesn't count this dependency because it's a # types only import and the depcheck parser is resolving the @types/ # package as a dep instead diff --git a/packages/compass-e2e-tests/.gitignore b/packages/compass-e2e-tests/.gitignore index f02217af9e4..74d7b19ca26 100644 --- a/packages/compass-e2e-tests/.gitignore +++ b/packages/compass-e2e-tests/.gitignore @@ -2,5 +2,3 @@ .log fixtures/*.csv fixtures/*.json -write-electron-versions.js -electron-versions.json diff --git a/packages/compass-e2e-tests/.prettierignore b/packages/compass-e2e-tests/.prettierignore index 3f0e8eed2a2..104b57fc109 100644 --- a/packages/compass-e2e-tests/.prettierignore +++ b/packages/compass-e2e-tests/.prettierignore @@ -3,7 +3,3 @@ fixtures .nyc_output coverage - -# Generated files -electron-versions.json -write-electron-versions.js diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index 97575d39fce..90af05ad2b2 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -3,14 +3,10 @@ import { expect } from 'chai'; import type { CompassBrowser } from '../compass-browser'; import * as Selectors from '../selectors'; import type { ConnectFormState } from '../connect-form-state'; -import { - DEFAULT_CONNECTION_NAME_1, - DEFAULT_CONNECTION_NAME_2, - DEFAULT_CONNECTION_STRING_1, - DEFAULT_CONNECTION_STRING_2, - TEST_MULTIPLE_CONNECTIONS, -} from '../compass'; +import { TEST_MULTIPLE_CONNECTIONS } from '../compass'; import Debug from 'debug'; +import { DEFAULT_CONNECTIONS } from '../test-runner-context'; +import { getConnectionTitle } from '@mongodb-js/connection-info'; const debug = Debug('compass-e2e-tests'); export async function resetConnectForm(browser: CompassBrowser): Promise { @@ -938,29 +934,13 @@ export async function setConnectFormState( export async function saveConnection( browser: CompassBrowser, - state: ConnectFormState, - - // TODO(COMPASS-8023): Just remove these once the single connection code is removed - favouriteName: string, - color: string + state: ConnectFormState ): Promise { await browser.setConnectFormState(state); - if (TEST_MULTIPLE_CONNECTIONS) { - await browser.clickVisible(Selectors.ConnectionModalSaveButton); - await browser - .$(Selectors.ConnectionModal) - .waitForDisplayed({ reverse: true }); - } else { - await browser.clickVisible(Selectors.ConnectionEditFavouriteButton); - await browser.$(Selectors.FavoriteModal).waitForDisplayed(); - await browser.setValueVisible(Selectors.FavoriteNameInput, favouriteName); - await browser.clickVisible( - `${Selectors.FavoriteColorSelector} [data-testid="color-pick-${color}"]` - ); - await browser.$(Selectors.FavoriteSaveButton).waitForEnabled(); - await browser.clickVisible(Selectors.FavoriteSaveButton); - await browser.$(Selectors.FavoriteModal).waitForExist({ reverse: true }); - } + await browser.clickVisible(Selectors.ConnectionModalSaveButton); + await browser + .$(Selectors.ConnectionModal) + .waitForDisplayed({ reverse: true }); } export async function setupDefaultConnections(browser: CompassBrowser) { @@ -986,10 +966,8 @@ export async function setupDefaultConnections(browser: CompassBrowser) { whereas we do have some tests that try and use those. We can easily change this in future if needed, though. */ - for (const connectionName of [ - DEFAULT_CONNECTION_NAME_1, - DEFAULT_CONNECTION_NAME_2, - ]) { + for (const connectionInfo of DEFAULT_CONNECTIONS) { + const connectionName = getConnectionTitle(connectionInfo); if (await browser.removeConnection(connectionName)) { debug('Removing existing connection so we do not create a duplicate', { connectionName, @@ -997,28 +975,11 @@ export async function setupDefaultConnections(browser: CompassBrowser) { } } - await browser.saveConnection( - { - connectionString: DEFAULT_CONNECTION_STRING_1, - // NOTE: no connectionName, we're going with the auto-generated one. Also no - // connectionColor. Passing a name and colour for single connection world, - // though, because that's the only way to create a favourite. - }, - DEFAULT_CONNECTION_NAME_1, - 'color1' - ); - - // no need for a second connection in single connection mode - if (TEST_MULTIPLE_CONNECTIONS) { - await browser.saveConnection( - { - connectionString: DEFAULT_CONNECTION_STRING_2, - // NOTE: filling in a name so that this one does _not_ have the auto-generated one - connectionName: DEFAULT_CONNECTION_NAME_2, - connectionColor: 'Iris', - }, - DEFAULT_CONNECTION_NAME_2, - 'color8' - ); + for (const connectionInfo of DEFAULT_CONNECTIONS) { + await browser.saveConnection({ + connectionString: connectionInfo.connectionOptions.connectionString, + connectionName: connectionInfo.favorite?.name, + connectionColor: connectionInfo.favorite?.color, + }); } } diff --git a/packages/compass-e2e-tests/helpers/commands/screenshot.ts b/packages/compass-e2e-tests/helpers/commands/screenshot.ts index 7a7a89f0c53..5b01dc7ae4f 100644 --- a/packages/compass-e2e-tests/helpers/commands/screenshot.ts +++ b/packages/compass-e2e-tests/helpers/commands/screenshot.ts @@ -1,6 +1,6 @@ import path from 'path'; import type { CompassBrowser } from '../compass-browser'; -import { SCREENSHOTS_PATH } from '../compass'; +import { LOG_SCREENSHOTS_PATH } from '../test-runner-context'; const withTimeout = (millis: number, promise: Promise) => { let timeoutPid: NodeJS.Timeout; @@ -26,7 +26,7 @@ export async function screenshot( // are still in progress or not. await browser.pause(1000); - const fullPath = path.join(SCREENSHOTS_PATH, filename); + const fullPath = path.join(LOG_SCREENSHOTS_PATH, filename); try { await withTimeout(10000, browser.saveScreenshot(fullPath)); } catch (err: any) { diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index 3cd10cc47a0..6bdb804e20b 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -8,6 +8,7 @@ import { execFile } from 'child_process'; import type { ExecFileOptions, ExecFileException } from 'child_process'; import { promisify } from 'util'; import zlib from 'zlib'; +import type { RemoteOptions } from 'webdriverio'; import { remote } from 'webdriverio'; import { rebuild } from '@electron/rebuild'; import type { RebuildOptions } from '@electron/rebuild'; @@ -24,6 +25,29 @@ import Debug from 'debug'; import semver from 'semver'; import crossSpawn from 'cross-spawn'; import { CHROME_STARTUP_FLAGS } from './chrome-startup-flags'; +import { + DEFAULT_CONNECTION_STRINGS, + DEFAULT_CONNECTION_NAMES, + DEFAULT_CONNECTIONS_SERVER_INFO, + ELECTRON_CHROMIUM_VERSION, + TEST_COMPASS_WEB as _TEST_COMPASS_WEB, + LOG_PATH, + LOG_COVERAGE_PATH, + COMPASS_DESKTOP_PATH, + LOG_OUTPUT_PATH, + LOG_SCREENSHOTS_PATH, + WEBDRIVER_DEFAULT_WAITFOR_TIMEOUT, + WEBDRIVER_DEFAULT_WAITFOR_INTERVAL, + TEST_COMPASS_DESKTOP_PACKAGED_APP, + ELECTRON_PATH, + COMPASS_WEB_BROWSER_NAME, + COMPASS_WEB_BROWSER_VERSION, + TEST_ATLAS_CLOUD_EXTERNAL, + TEST_ATLAS_CLOUD_EXTERNAL_COOKIES_FILE, + TEST_ATLAS_CLOUD_EXTERNAL_URL, + TEST_ATLAS_CLOUD_EXTERNAL_GROUP_ID, + COMPASS_WEB_SANDBOX_URL, +} from './test-runner-context'; const debug = Debug('compass-e2e-tests'); @@ -32,20 +56,8 @@ const { Z_SYNC_FLUSH } = zlib.constants; const packageCompassAsync = promisify(packageCompass); -export const COMPASS_PATH = path.dirname( - require.resolve('mongodb-compass/package.json') -); -export const LOG_PATH = path.resolve(__dirname, '..', '.log'); -const OUTPUT_PATH = path.join(LOG_PATH, 'output'); -export const SCREENSHOTS_PATH = path.join(LOG_PATH, 'screenshots'); -const COVERAGE_PATH = path.join(LOG_PATH, 'coverage'); - -let MONGODB_VERSION = ''; -let MONGODB_USE_ENTERPRISE = - (process.env.MONGODB_VERSION?.endsWith('-enterprise') && 'yes') ?? 'no'; - // should we test compass-web (true) or compass electron (false)? -export const TEST_COMPASS_WEB = process.argv.includes('--test-compass-web'); +export const TEST_COMPASS_WEB = _TEST_COMPASS_WEB; // multiple connections is now the default export const TEST_MULTIPLE_CONNECTIONS = true; @@ -65,80 +77,31 @@ export function skipForWeb( } } -function getBrowserName() { - return process.env.BROWSER_NAME ?? 'chrome'; -} - -export const BROWSER_NAME = getBrowserName(); - export const MONGODB_TEST_SERVER_PORT = Number( process.env.MONGODB_TEST_SERVER_PORT ?? 27091 ); -export const DEFAULT_CONNECTION_STRING_1 = `mongodb://127.0.0.1:${MONGODB_TEST_SERVER_PORT}/test`; +export const DEFAULT_CONNECTION_STRING_1 = DEFAULT_CONNECTION_STRINGS[0]; // NOTE: in browser.setupDefaultConnections() we don't give the first connection an // explicit name, so it gets a calculated one based off the connection string -export const DEFAULT_CONNECTION_NAME_1 = connectionNameFromString( - DEFAULT_CONNECTION_STRING_1 -); +export const DEFAULT_CONNECTION_NAME_1 = DEFAULT_CONNECTION_NAMES[0]; // for testing multiple connections -export const DEFAULT_CONNECTION_STRING_2 = `mongodb://127.0.0.1:${ - MONGODB_TEST_SERVER_PORT + 1 -}/test`; +export const DEFAULT_CONNECTION_STRING_2 = DEFAULT_CONNECTION_STRINGS[1]; // NOTE: in browser.setupDefaultConnections() the second connection gets given an explicit name -export const DEFAULT_CONNECTION_NAME_2 = 'connection-2'; - -export function updateMongoDBServerInfo() { - try { - const { stdout, stderr } = crossSpawn.sync( - 'npm', - [ - 'run', - '--silent', - /** - * The server info update is done through a separate script and not by - * using a MongoClient directly because doing so causes an unexplainable - * segfault crash in e2e-coverage task in evergreen CI. Moving this - * logic to a separate script seems to solve this problem, but if at any - * point the issue returns, feel free to revert this whole change - **/ - 'server-info', - '--', - '--connectionString', - `mongodb://127.0.0.1:${String(MONGODB_TEST_SERVER_PORT)}`, - ], - { encoding: 'utf-8' } - ); - if (stderr?.length) { - throw new Error(stderr); - } - const { version, enterprise } = JSON.parse(stdout); - MONGODB_VERSION = version; - MONGODB_USE_ENTERPRISE = enterprise ? 'yes' : 'no'; - debug( - `Got server info: v${String(version)} (${ - enterprise ? 'enterprise' : 'community' - })` - ); - } catch (err) { - (err as Error).message = - 'Failed trying to get MongoDB server info:\n\n' + (err as Error).message; - throw err; - } -} +export const DEFAULT_CONNECTION_NAME_2 = DEFAULT_CONNECTION_NAMES[1]; export const serverSatisfies = ( semverCondition: string, enterpriseExact?: boolean ) => { + const { version, enterprise } = DEFAULT_CONNECTIONS_SERVER_INFO[0]; return ( - semver.satisfies(MONGODB_VERSION, semverCondition, { + semver.satisfies(version, semverCondition, { includePrerelease: true, }) && (typeof enterpriseExact === 'boolean' - ? (enterpriseExact && MONGODB_USE_ENTERPRISE === 'yes') || - (!enterpriseExact && MONGODB_USE_ENTERPRISE !== 'yes') + ? (enterpriseExact && enterprise) || (!enterpriseExact && !enterprise) : true) ); }; @@ -391,13 +354,13 @@ export class Compass { }); if (coverage.main) { await fs.writeFile( - path.join(COVERAGE_PATH, `main.${this.name}.log`), + path.join(LOG_COVERAGE_PATH, `main.${this.name}.log`), coverage.main ); } if (coverage.renderer) { await fs.writeFile( - path.join(COVERAGE_PATH, `renderer.${this.name}.log`), + path.join(LOG_COVERAGE_PATH, `renderer.${this.name}.log`), coverage.renderer ); } @@ -475,13 +438,10 @@ async function getCompassExecutionParameters(): Promise<{ testPackagedApp: boolean; binary: string; }> { - const testPackagedApp = ['1', 'true'].includes( - process.env.TEST_PACKAGED_APP ?? '' - ); + const testPackagedApp = TEST_COMPASS_DESKTOP_PACKAGED_APP; const binary = testPackagedApp ? getCompassBinPath(await getCompassBuildMetadata()) - : // eslint-disable-next-line @typescript-eslint/no-var-requires - (require('electron') as unknown as string); + : ELECTRON_PATH; return { testPackagedApp, binary }; } @@ -509,7 +469,7 @@ export async function runCompassOnce(args: string[], timeout = 30_000) { const { binary } = await getCompassExecutionParameters(); debug('spawning compass...', { binary, - COMPASS_PATH, + COMPASS_DESKTOP_PATH, defaultUserDataDir, args, timeout, @@ -522,7 +482,7 @@ export async function runCompassOnce(args: string[], timeout = 30_000) { const { error, stdout, stderr } = await execFileIgnoreError( binary, [ - COMPASS_PATH, + COMPASS_DESKTOP_PATH, // When running binary without webdriver, we need to pass the same flags // as we pass when running with webdriverio to have similar behaviour. ...CHROME_STARTUP_FLAGS, @@ -585,9 +545,9 @@ async function processCommonOpts({ // for consistency let's mkdir for both of them just in case await fs.mkdir(path.dirname(chromedriverLogPath), { recursive: true }); await fs.mkdir(webdriverLogPath, { recursive: true }); - await fs.mkdir(OUTPUT_PATH, { recursive: true }); - await fs.mkdir(SCREENSHOTS_PATH, { recursive: true }); - await fs.mkdir(COVERAGE_PATH, { recursive: true }); + await fs.mkdir(LOG_OUTPUT_PATH, { recursive: true }); + await fs.mkdir(LOG_SCREENSHOTS_PATH, { recursive: true }); + await fs.mkdir(LOG_COVERAGE_PATH, { recursive: true }); // https://webdriver.io/docs/options/#webdriver-options const webdriverOptions = { @@ -597,14 +557,8 @@ async function processCommonOpts({ // https://webdriver.io/docs/options/#webdriverio const wdioOptions = { - // default is 3000ms - waitforTimeout: process.env.COMPASS_TEST_DEFAULT_WAITFOR_TIMEOUT - ? Number(process.env.COMPASS_TEST_DEFAULT_WAITFOR_TIMEOUT) - : 120_000, // shorter than the test timeout so the exact line will fail, not the test - // default is 500ms - waitforInterval: process.env.COMPASS_TEST_DEFAULT_WAITFOR_INTERVAL - ? Number(process.env.COMPASS_TEST_DEFAULT_WAITFOR_INTERVAL) - : 100, + waitforTimeout: WEBDRIVER_DEFAULT_WAITFOR_TIMEOUT, + waitforInterval: WEBDRIVER_DEFAULT_WAITFOR_INTERVAL, }; process.env.DEBUG = `${process.env.DEBUG ?? ''},mongodb-compass:main:logging`; @@ -638,7 +592,7 @@ async function startCompassElectron( if (!testPackagedApp) { // https://www.electronjs.org/docs/latest/tutorial/automated-testing#with-webdriverio - chromeArgs.push(`--app=${COMPASS_PATH}`); + chromeArgs.push(`--app=${COMPASS_DESKTOP_PATH}`); } if (opts.firstRun === false) { @@ -685,7 +639,7 @@ async function startCompassElectron( automationProtocol: 'webdriver' as const, capabilities: { browserName: 'chromium', - browserVersion: process.env.CHROME_VERSION, + browserVersion: ELECTRON_CHROMIUM_VERSION, // https://chromedriver.chromium.org/capabilities#h.p_ID_106 'goog:chromeOptions': { binary: maybeWrappedBinary, @@ -761,6 +715,16 @@ async function startCompassElectron( return compass; } +export type StoredAtlasCloudCookies = { + name: string; + value: string; + domain: string; + path: string; + secure: boolean; + httpOnly: boolean; + expirationDate: number; +}[]; + export async function startBrowser( name: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -769,25 +733,70 @@ export async function startBrowser( runCounter++; const { webdriverOptions, wdioOptions } = await processCommonOpts(); - const browser: CompassBrowser = (await remote({ + const options: RemoteOptions = { capabilities: { - browserName: BROWSER_NAME, // 'chrome' or 'firefox' - // https://webdriver.io/docs/driverbinaries/ - // If you leave out browserVersion it will try and find the browser binary - // on your system. If you specify it it will download that version. The - // main limitation then is that 'latest' is the only 'semantic' version - // that is supported for Firefox. - // https://github.com/puppeteer/puppeteer/blob/ab5d4ac60200d1cea5bcd4910f9ccb323128e79a/packages/browsers/src/browser-data/browser-data.ts#L66 - // Alternatively we can download it ourselves and specify the path to the - // binary or we can even start and stop chromedriver/geckodriver manually. - // NOTE: The version of chromedriver or geckodriver in play might also be - // relevant. - browserVersion: 'latest', + browserName: COMPASS_WEB_BROWSER_NAME, + ...(COMPASS_WEB_BROWSER_VERSION && { + browserVersion: COMPASS_WEB_BROWSER_VERSION, + }), }, ...webdriverOptions, ...wdioOptions, - })) as CompassBrowser; - await browser.navigateTo('http://localhost:7777/'); + }; + + debug('Starting browser via webdriverio with the following configuration:'); + debug(JSON.stringify(options, null, 2)); + + const browser: CompassBrowser = (await remote(options)) as CompassBrowser; + + if (TEST_ATLAS_CLOUD_EXTERNAL) { + // To be able to use `setCookies` method, we need to first open any page on + // the same domain as the cookies we are going to set + // https://webdriver.io/docs/api/browser/setCookies/ + await browser.navigateTo(`${TEST_ATLAS_CLOUD_EXTERNAL_URL!}/404`); + + type StoredAtlasCloudCookies = { + name: string; + value: string; + domain: string; + path: string; + secure: boolean; + httpOnly: boolean; + expirationDate: number; + }[]; + + const cookies: StoredAtlasCloudCookies = JSON.parse( + await fs.readFile(TEST_ATLAS_CLOUD_EXTERNAL_COOKIES_FILE!, 'utf8') + ); + + await browser.setCookies( + cookies + .filter((cookie) => { + // These are the relevant cookies for auth: + // https://github.com/10gen/mms/blob/6d27992a6ab9ab31471c8bcdaa4e347aa39f4013/server/src/features/com/xgen/svc/cukes/helpers/Client.java#L122-L130 + return ( + cookie.name.includes('mmsa-') || + cookie.name.includes('mdb-sat') || + cookie.name.includes('mdb-srt') + ); + }) + .map((cookie) => ({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path, + secure: cookie.secure, + httpOnly: cookie.httpOnly, + })) + ); + + await browser.navigateTo( + `${TEST_ATLAS_CLOUD_EXTERNAL_URL!}/v2/${TEST_ATLAS_CLOUD_EXTERNAL_GROUP_ID!}#/explorer` + ); + } else { + await browser.navigateTo(COMPASS_WEB_SANDBOX_URL); + } + const compass = new Compass(name, browser, { mode: 'web', writeCoverage: false, @@ -866,7 +875,7 @@ function formattedDate(): string { } export async function rebuildNativeModules( - compassPath = COMPASS_PATH + compassPath = COMPASS_DESKTOP_PATH ): Promise { const fullCompassPath = require.resolve( path.join(compassPath, 'package.json') @@ -893,7 +902,7 @@ export async function rebuildNativeModules( } export async function compileCompassAssets( - compassPath = COMPASS_PATH + compassPath = COMPASS_DESKTOP_PATH ): Promise { await promisify(execFile)('npm', ['run', 'compile'], { cwd: compassPath }); } @@ -925,7 +934,7 @@ async function getCompassBuildMetadata(): Promise { export async function buildCompass( force = false, - compassPath = COMPASS_PATH + compassPath = COMPASS_DESKTOP_PATH ): Promise { if (!force) { try { @@ -1040,7 +1049,7 @@ export async function init( await browser.execute(() => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { ipcRenderer } = require('electron'); - ipcRenderer.invoke('compass:maximize'); + void ipcRenderer.invoke('compass:maximize'); }); } @@ -1116,7 +1125,7 @@ export function screenshotPathName(text: string) { * @param {string} filename */ export function outputFilename(filename: string): string { - return path.join(OUTPUT_PATH, filename); + return path.join(LOG_OUTPUT_PATH, filename); } export async function screenshotIfFailed( diff --git a/packages/compass-e2e-tests/helpers/gunzip.ts b/packages/compass-e2e-tests/helpers/gunzip.ts new file mode 100755 index 00000000000..8777c224483 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/gunzip.ts @@ -0,0 +1,41 @@ +import Debug from 'debug'; +import { glob as globAsync } from 'glob'; +import { createReadStream, createWriteStream } from 'fs'; +import { pipeline } from 'stream/promises'; +import { createGunzip } from 'zlib'; +import path from 'path'; + +const debug = Debug('compass-e2e-tests:gunzip'); + +async function gunzip(input: string, output: string, signal: AbortSignal) { + const readStream = createReadStream(input); + const gunzip = createGunzip(); + const writeStream = createWriteStream(output); + try { + await pipeline(readStream, gunzip, writeStream, { signal, end: true }); + } catch (err) { + if (signal.aborted) { + return; + } + throw err; + } +} + +async function run(glob: string, cwd: string, signal: AbortSignal) { + const filenames = (await globAsync(glob, { cwd })).map((filepath) => { + return path.join(cwd, filepath); + }); + if (filenames.length === 0) { + throw new Error(`Failed to unpack ${glob} at ${cwd}: no files found`); + } + debug('Unpacking following files:', filenames); + for (const input of filenames) { + if (signal.aborted) { + return; + } + const output = input.replace(/\.gz$/, ''); + await gunzip(input, output, signal); + } +} + +export default run; diff --git a/packages/compass-e2e-tests/helpers/insert-data.ts b/packages/compass-e2e-tests/helpers/insert-data.ts index 8a2cdd18a2f..2a18bdb218b 100644 --- a/packages/compass-e2e-tests/helpers/insert-data.ts +++ b/packages/compass-e2e-tests/helpers/insert-data.ts @@ -46,7 +46,7 @@ for (let i = 0; i < 26; ++i) { let clients: MongoClient[]; let test_dbs: Db[]; -before(async () => { +export const beforeAll = async () => { // Insert data on both connections so that the same databases and collections // will exist on both servers and then anything that's not properly scoped to // the correct connection has a chance to operate on the wrong one and @@ -70,13 +70,13 @@ before(async () => { ); test_dbs = clients.map((client) => client.db('test')); -}); +}; -after(async () => { +export const afterAll = async () => { await Promise.all(clients.map((client) => client.close())); -}); +}; -beforeEach(async () => { +export const beforeEach = async () => { // Drop the databases that get created by tests or the functions below const promises = []; @@ -87,7 +87,13 @@ beforeEach(async () => { } await Promise.all(promises); -}); +}; + +export const mochaRootHooks: Mocha.RootHookObject = { + beforeAll, + beforeEach, + afterAll, +}; export async function createDummyCollections(): Promise { const promises = []; diff --git a/packages/compass-e2e-tests/helpers/result-logger.ts b/packages/compass-e2e-tests/helpers/result-logger.ts deleted file mode 100644 index ba5d65813b4..00000000000 --- a/packages/compass-e2e-tests/helpers/result-logger.ts +++ /dev/null @@ -1,340 +0,0 @@ -import assert from 'assert'; -import Debug from 'debug'; -import Mocha from 'mocha'; -import type { MongoClient, Collection } from 'mongodb'; -import type { ObjectId } from 'bson'; - -const debug = Debug('result-logger'); - -const DB_NAME = 'compass_e2e'; -const COLLECTION_NAME = 'results'; - -const { - EVENT_HOOK_BEGIN, - EVENT_HOOK_END, - EVENT_TEST_BEGIN, - EVENT_TEST_FAIL, - EVENT_TEST_PASS, -} = Mocha.Runner.constants; - -// env vars to store with the metrics. Be careful not to include secrets. -const ENV_VARS = [ - 'CI', - - // evergreen - 'EVERGREEN_AUTHOR', - 'EVERGREEN_BRANCH_NAME', - 'EVERGREEN_BUILD_ID', - 'EVERGREEN_BUILD_VARIANT', - 'EVERGREEN_EXECUTION', - 'EVERGREEN_IS_PATCH', - 'EVERGREEN_PROJECT', - 'EVERGREEN_REVISION', - 'EVERGREEN_TASK_ID', - 'EVERGREEN_TASK_NAME', - 'EVERGREEN_TASK_URL', - 'EVERGREEN_VERSION_ID', - 'EVERGREEN_WORKDIR', - - // github - 'GITHUB_WORKFLOW', - 'GITHUB_RUN_ID', - 'GITHUB_RUN_NUMBER', - 'GITHUB_JOB', - 'GITHUB_ACTION', - 'GITHUB_ACTION_PATH', - 'GITHUB_ACTIONS', - 'GITHUB_ACTOR', - 'GITHUB_REPOSITORY', - 'GITHUB_EVENT_NAME', - 'GITHUB_EVENT_PATH', - 'GITHUB_WORKSPACE', - 'GITHUB_SHA', - 'GITHUB_REF', - 'GITHUB_HEAD_REF', - 'GITHUB_BASE_REF', - 'GITHUB_SERVER_URL', - 'GITHUB_API_URL', - 'GITHUB_GRAPHQL_URL', - 'RUNNER_NAME', - 'RUNNER_OS', - 'RUNNER_TEMP', - 'RUNNER_TOOL_CACHE', -] as const; - -type Env = { [K in typeof ENV_VARS[number]]?: string }; - -type HookOrTest = Mocha.Hook | Mocha.Test; - -function joinPath(parts: string[]) { - // Turn an array of test/hook path components into a string we can use as an - // identifier for the test, hook or suite - - return parts.join(' / '); -} - -function githubWorkflowRunUrl() { - const serverURL = process.env.GITHUB_SERVER_URL ?? ''; - const repository = process.env.GITHUB_REPOSITORY ?? ''; - const runID = process.env.GITHUB_RUN_ID ?? ''; - - return `${serverURL}/${repository}/actions/runs/${runID}`; -} - -type Result = { - test_file: string; - type?: string; // tests don't have types and we delete these before handing it to evergreen - start: number; - status: 'start' | 'pass' | 'fail' | 'silentfail'; - end?: number; - elapsed?: number; - error?: any; - task_id?: string; - execution?: number; -}; - -type Report = { - results: Result[]; -}; - -export default class ResultLogger { - _id?: ObjectId; - start?: number; - end?: number; - elapsed?: number; - client?: MongoClient; - collection?: Collection; - context: { - env: Env; - ci: 'evergreen' | 'github-actions' | 'unknown'; - platform: string; - os: string; - author: string; - branch: string; - commit: string; - url: string; - }; - results: Result[]; - runner: Mocha.Runner; - - constructor(client: MongoClient, runner: Mocha.Runner) { - if (client) { - debug(`Logging E2E test metrics to ${DB_NAME}.${COLLECTION_NAME}`); - // client can be undefined if we don't want to write to the db - this.client = client; - const db = this.client.db(DB_NAME); - this.collection = db.collection(COLLECTION_NAME); - } - - this.context = { - // copy known env vars as-is if they are set - env: ENV_VARS.reduce((obj: Env, name: keyof Env) => { - const value = process.env[name]; - if (value) { - obj[name] = value; - } - return obj; - }, {}), - - // infer some common variables - ci: process.env.EVERGREEN - ? 'evergreen' - : process.env.GITHUB_ACTIONS - ? 'github-actions' - : 'unknown', - - platform: process.platform, - - // this way we should be able to distinguish between ubuntu and rhel on - // evergreen and linux on github actions - os: - process.env.EVERGREEN_BUILD_VARIANT ?? - process.env.RUNNER_OS ?? - 'unknown', - - author: - process.env.EVERGREEN_AUTHOR ?? process.env.GITHUB_ACTOR ?? 'unknown', - - // For an evergreen patch the branch name is set to main which is not what we want - branch: process.env.EVERGREEN_IS_PATCH - ? 'evergreen-patch' - : process.env.EVERGREEN_BRANCH_NAME ?? - process.env.GITHUB_HEAD_REF ?? - 'unknown', - - // EVERGREEN_REVISION is the ${revision} expansion, but the ${github_commit} one might be better? - // GITHUB_SHA also doesn't look 100% right. - commit: - process.env.EVERGREEN_REVISION ?? process.env.GITHUB_SHA ?? 'unknown', - - url: process.env.EVERGREEN - ? process.env.EVERGREEN_TASK_URL ?? '' - : process.env.GITHUB_ACTIONS - ? githubWorkflowRunUrl() - : 'unknown', - }; - - // Hooks and tests. See - // https://github.com/evergreen-ci/evergreen/wiki/Project-Commands#attach-results - // for the target structure. - this.results = []; - - this.runner = runner; - - runner.on(EVENT_HOOK_BEGIN, (hook) => { - this.startResult(hook); - }); - - runner.on(EVENT_HOOK_END, (hook) => { - // unlike for tests, with hooks end only fires when it passes - this.passResult(hook); - }); - - runner.on(EVENT_TEST_BEGIN, (test) => { - this.startResult(test); - }); - - runner.on(EVENT_TEST_PASS, (test) => { - this.passResult(test); - }); - - runner.on(EVENT_TEST_FAIL, (hookOrTest: HookOrTest, error: any) => { - // tests and hooks failing go to the same event - if (hookOrTest.type === 'hook') { - // NOTE: if this is a beforeEach hook, then the test's EVENT_TEST_BEGIN - // will have fired but it will never get a corresponding - // EVENT_TEST_FAIL, leaving it stuck in the start state - this.failResult(hookOrTest, error); - } else { - this.failResult(hookOrTest, error); - } - }); - } - - async init(): Promise { - debug('init'); - - this.start = Date.now() / 1000; - if (this.collection) { - const { insertedId } = await this.collection.insertOne({ - ...this.context, - start: this.start, - status: 'start', - }); - this._id = insertedId; - debug('resultId', this._id); - } - } - - startResult(hookOrTest: HookOrTest): void { - const test_file = joinPath(hookOrTest.titlePath()); - debug('start', test_file); - - const result = { - test_file, - type: hookOrTest.type, - start: Date.now() / 1000, - status: 'start', // evergreen only knows fail, pass, silentfail and skip - } as Result; - - this.results.push(result); - } - - passResult(hookOrTest: HookOrTest): void { - const test_file = joinPath(hookOrTest.titlePath()); - debug('pass', test_file); - const result = this.findResult(test_file); - - assert.ok(result); - - result.status = 'pass'; - result.end = Date.now() / 1000; - result.elapsed = result.end - result.start; - } - - failResult(hookOrTest: HookOrTest, error: Error): void { - const test_file = joinPath(hookOrTest.titlePath()); - debug('fail', test_file); - const result = this.findResult(test_file); - - assert.ok(result); - - result.status = 'fail'; - result.end = Date.now() / 1000; - result.elapsed = result.end - result.start; - result.error = error.stack; - } - - async done(failures: number): Promise { - debug('done'); - - this.end = Date.now() / 1000; - this.elapsed = this.end - (this.start || 0); // typescript thinks start might be undefined - - if (this.collection) { - const update = { - results: this.results, - elapsed: this.elapsed, - status: failures ? 'fail' : 'pass', - failures, - }; - - await this.collection.updateOne({ _id: this._id }, { $set: update }); - } - - return this.report(); - } - - report(): Report { - const results = this.results - .filter((r) => { - if (r.status !== 'pass') { - // keep all errors - return true; - } - // strip out passed hooks because it is a bit noisy - if (r.type === 'hook') { - return false; - } - return true; - }) - .map((r) => { - const result = { ...r }; - // change things that are still stuck as "start" to something evergreen - // understands - if (result.status === 'start') { - result.status = 'silentfail'; - } - - // copy over some evergreen-specific fields if they exist - if (process.env.EVERGREEN_TASK_ID) { - result.task_id = process.env.EVERGREEN_TASK_ID; - } - if (process.env.EVERGREEN_EXECUTION) { - result.execution = parseInt(process.env.EVERGREEN_EXECUTION, 10); - } - - if (result.type) { - delete result.type; - } - - // only include fields that evergreen knows about - // https://github.com/evergreen-ci/evergreen/wiki/Project-Commands#attach-results - delete result.error; - - return result; - }); - - return { results }; - } - - findResult(test_file: string): Result | null { - for (const result of this.results) { - if (result.test_file === test_file) { - return result; - } - } - - return null; - } -} diff --git a/packages/compass-e2e-tests/helpers/test-runner-context.ts b/packages/compass-e2e-tests/helpers/test-runner-context.ts new file mode 100644 index 00000000000..3464c11bbbc --- /dev/null +++ b/packages/compass-e2e-tests/helpers/test-runner-context.ts @@ -0,0 +1,193 @@ +import { + getConnectionTitle, + type ConnectionInfo, +} from '@mongodb-js/connection-info'; +import path from 'path'; +import electronPath from 'electron'; +import electronPackageJson from 'electron/package.json'; +// @ts-expect-error no types for this package +import { electronToChromium } from 'electron-to-chromium'; +import type { MongoClusterOptions } from 'mongodb-runner'; + +if (typeof electronPath !== 'string') { + throw new Error( + 'Running e2e tests in an unsupported runtime: `electronPath` is not a string' + ); +} + +// TODO: Probably time to use some arg parser for this already +export const ALLOWED_RUNNER_ARGS = [ + '--test-compass-web', + '--no-compile', + '--no-native-modules', + '--test-packaged-app', + '--disable-start-stop', + '--bail', +]; + +/** + * Variables used by a special use-case of running e2e tests against a + * cloud(-dev).mongodb.com URL. If you're changing anything related to these, + * make sure that the tests in mms are also updated to account for that + */ +export const TEST_ATLAS_CLOUD_EXTERNAL_URL = + process.env.TEST_ATLAS_CLOUD_EXTERNAL_URL; +export const TEST_ATLAS_CLOUD_EXTERNAL_COOKIES_FILE = + process.env.TEST_ATLAS_CLOUD_EXTERNAL_COOKIES_FILE; +export const TEST_ATLAS_CLOUD_EXTERNAL_GROUP_ID = + process.env.TEST_ATLAS_CLOUD_EXTERNAL_GROUP_ID; +const TEST_ATLAS_CLOUD_EXTERNAL_DEFAULT_CONNECTIONS: ConnectionInfo[] | null = + JSON.parse(process.env.TEST_ATLAS_CLOUD_DEFAULT_CONNECTIONS ?? 'null'); + +const ALL_ATLAS_CLOUD_EXTERNAL_VARS = [ + TEST_ATLAS_CLOUD_EXTERNAL_URL, + TEST_ATLAS_CLOUD_EXTERNAL_COOKIES_FILE, + TEST_ATLAS_CLOUD_EXTERNAL_GROUP_ID, + TEST_ATLAS_CLOUD_EXTERNAL_DEFAULT_CONNECTIONS, +]; + +export const TEST_ATLAS_CLOUD_EXTERNAL = ALL_ATLAS_CLOUD_EXTERNAL_VARS.some( + (val) => { + return !!val; + } +); + +if ( + TEST_ATLAS_CLOUD_EXTERNAL && + ALL_ATLAS_CLOUD_EXTERNAL_VARS.some((val) => { + return !val; + }) +) { + throw new Error( + 'Trying to test Atlas Cloud external URL but some required variables are missing' + ); +} + +export const TEST_COMPASS_WEB = + process.argv.includes('--test-compass-web') || TEST_ATLAS_CLOUD_EXTERNAL; +export const TEST_COMPASS_DESKTOP = !TEST_COMPASS_WEB; +export const TEST_COMPASS_DESKTOP_PACKAGED_APP = process.argv.includes( + '--test-packaged-app' +); +// Skip this step if you are running tests consecutively and don't need to +// rebuild modules all the time. Also no need to ever recompile when testing +// compass-web. +export const SKIP_COMPASS_DESKTOP_COMPILE = + process.argv.includes('--no-compile') && !TEST_COMPASS_WEB; +// Skip this step if you want to run tests against your own compilation (e.g, a +// dev build or a build running in watch mode that autorecompiles). Also no need +// to recompile when testing compass-web. +export const SKIP_NATIVE_MODULE_REBUILD = + process.argv.includes('--no-native-modules') && !TEST_COMPASS_WEB; +export const DISABLE_START_STOP = process.argv.includes('--disable-start-stop'); +export const MOCHA_BAIL = process.argv.includes('--bail'); + +export const COMPASS_WEB_BROWSER_NAME = + process.env.COMPASS_WEB_BROWSER_NAME ?? 'chrome'; +// https://webdriver.io/docs/driverbinaries/ +// +// If you leave out browserVersion it will try and find the browser binary on +// your system. If you specify it it will download that version. The main +// limitation then is that 'latest' is the only 'semantic' version that is +// supported for Firefox. +// https://github.com/puppeteer/puppeteer/blob/ab5d4ac60200d1cea5bcd4910f9ccb323128e79a/packages/browsers/src/browser-data/browser-data.ts#L66 +// +// Alternatively we can download it ourselves and specify the path to the binary +// or we can even start and stop chromedriver/geckodriver manually. +// +// NOTE: The version of chromedriver or geckodriver in play might also be +// relevant. +export const COMPASS_WEB_BROWSER_VERSION = + process.env.COMPASS_WEB_BROWSER_VERSION === 'unset' + ? undefined + : process.env.BROWSER_VERSION ?? 'latest'; +export const COMPASS_WEB_SANDBOX_URL = 'http://localhost:7777'; + +const MONGODB_TESTSERVER_VERSION = + process.env.MONGODB_VERSION ?? process.env.MONGODB_RUNNER_VERSION; + +export const DEFAULT_CONNECTIONS: (ConnectionInfo & { + testServer?: Partial; +})[] = + TEST_ATLAS_CLOUD_EXTERNAL && TEST_ATLAS_CLOUD_EXTERNAL_DEFAULT_CONNECTIONS + ? TEST_ATLAS_CLOUD_EXTERNAL_DEFAULT_CONNECTIONS + : [ + { + id: 'test-connection-1', + connectionOptions: { + connectionString: 'mongodb://127.0.0.1:27091/test', + }, + testServer: { + version: MONGODB_TESTSERVER_VERSION, + topology: 'replset', + secondaries: 0, + args: ['--port', '27091'], + }, + }, + { + id: 'test-connection-2', + connectionOptions: { + connectionString: 'mongodb://127.0.0.1:27092/test', + }, + favorite: { + name: 'connection-2', + color: 'Iris', + }, + testServer: { + version: MONGODB_TESTSERVER_VERSION, + topology: 'replset', + secondaries: 0, + args: ['--port', '27092'], + }, + }, + ]; + +export const DEFAULT_CONNECTION_STRINGS = DEFAULT_CONNECTIONS.map((info) => { + return info.connectionOptions.connectionString; +}); + +export const DEFAULT_CONNECTION_NAMES = DEFAULT_CONNECTIONS.map((info) => { + return getConnectionTitle(info); +}); + +export const DEFAULT_CONNECTIONS_SERVER_INFO: { + version: string; + enterprise: boolean; +}[] = []; + +export const E2E_WORKSPACE_PATH = path.dirname( + require.resolve('compass-e2e-tests/package.json') +); +// /packages/compass-e2e-tests +// /packages +// +export const MONOREPO_ROOT_PATH = path.resolve(E2E_WORKSPACE_PATH, '..', '..'); +export const COMPASS_DESKTOP_PATH = path.dirname( + require.resolve('mongodb-compass/package.json') +); +export const COMPASS_WEB_PATH = path.dirname( + require.resolve('@mongodb-js/compass-web/package.json') +); +export const LOG_PATH = path.resolve(E2E_WORKSPACE_PATH, '.log'); +export const LOG_OUTPUT_PATH = path.join(LOG_PATH, 'output'); +export const LOG_SCREENSHOTS_PATH = path.join(LOG_PATH, 'screenshots'); +export const LOG_COVERAGE_PATH = path.join(LOG_PATH, 'coverage'); +// Set coverage to the root of the monorepo so it will be generated for +// everything and not just packages/compass +export const COVERAGE_PATH = (process.env.COVERAGE = MONOREPO_ROOT_PATH); + +export const ELECTRON_PATH = electronPath; +export const ELECTRON_VERSION = electronPackageJson.version; +export const ELECTRON_CHROMIUM_VERSION = electronToChromium(ELECTRON_VERSION); + +export const WEBDRIVER_DEFAULT_WAITFOR_TIMEOUT = process.env + .COMPASS_TEST_DEFAULT_WAITFOR_TIMEOUT + ? Number(process.env.COMPASS_TEST_DEFAULT_WAITFOR_TIMEOUT) + : 120_000; // default is 3000ms +export const WEBDRIVER_DEFAULT_WAITFOR_INTERVAL = process.env + .COMPASS_TEST_DEFAULT_WAITFOR_INTERVAL + ? Number(process.env.COMPASS_TEST_DEFAULT_WAITFOR_INTERVAL) + : 100; // default is 500ms +// Kinda arbitrary, but longer than WEBDRIVER_DEFAULT_WAITFOR_TIMEOUT so the +// test can fail before Mocha times out +export const MOCHA_DEFAULT_TIMEOUT = WEBDRIVER_DEFAULT_WAITFOR_TIMEOUT * 2; diff --git a/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts b/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts new file mode 100644 index 00000000000..8fd8e030921 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/test-runner-global-fixtures.ts @@ -0,0 +1,225 @@ +import gunzip from './gunzip'; +import fs from 'fs'; +import { + COMPASS_WEB_SANDBOX_URL, + DEFAULT_CONNECTIONS, + DEFAULT_CONNECTIONS_SERVER_INFO, + DISABLE_START_STOP, + E2E_WORKSPACE_PATH, + LOG_PATH, + SKIP_COMPASS_DESKTOP_COMPILE, + SKIP_NATIVE_MODULE_REBUILD, + TEST_ATLAS_CLOUD_EXTERNAL, + TEST_COMPASS_DESKTOP, + TEST_COMPASS_DESKTOP_PACKAGED_APP, + TEST_COMPASS_WEB, +} from './test-runner-context'; +import Debug from 'debug'; +import { startTestServer } from '@mongodb-js/compass-test-server'; +import crossSpawn from 'cross-spawn'; +import kill from 'tree-kill'; +import { MongoClient } from 'mongodb'; +import { isEnterprise } from 'mongodb-build-info'; +import { + buildCompass, + compileCompassAssets, + rebuildNativeModules, + removeUserDataDir, +} from './compass'; +import { getConnectionTitle } from '@mongodb-js/connection-info'; + +export const globalFixturesAbortController = new AbortController(); + +function throwIfAborted() { + if (globalFixturesAbortController.signal.aborted) { + throw new Error('Mocha run was aborted while global setup was in progress'); + } +} + +export let abortRunner: (() => void) | undefined; + +const debug = Debug('compass-e2e-tests:mocha-global-fixtures'); + +const wait = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +const cleanupFns: (() => Promise | void)[] = []; + +/** + * Main hook that does all the main pre-setup before running tests: + * - Unpacks the fixtures + * - Starts MongoDB servers for every default connection defined in + * DEFAULT_CONNECTIONS + * - Starts compass-web sandbox + * - Updates server metadata that will be used by tests + * - Compiles the desktop app + */ +export async function mochaGlobalSetup(this: Mocha.Runner) { + abortRunner = () => { + globalFixturesAbortController.abort(); + this.abort(); + }; + + try { + debug('Unzipping fixtures...'); + await gunzip( + // Not using absolute paths because Windows fails to resolve glob + // collectly in this case + 'fixtures/*.gz', + E2E_WORKSPACE_PATH, + globalFixturesAbortController.signal + ); + + throwIfAborted(); + + debug('X DISPLAY', process.env.DISPLAY); + + if (!DISABLE_START_STOP) { + for (const connectionInfo of DEFAULT_CONNECTIONS) { + if (connectionInfo.testServer) { + debug( + 'Starting MongoDB server for connection %s', + getConnectionTitle(connectionInfo) + ); + const server = await startTestServer(connectionInfo.testServer); + cleanupFns.push(() => { + debug( + 'Stopping server for connection %s', + getConnectionTitle(connectionInfo) + ); + return server.close(); + }); + } + throwIfAborted(); + } + + if (TEST_COMPASS_WEB && !TEST_ATLAS_CLOUD_EXTERNAL) { + debug('Starting Compass Web server ...'); + const compassWeb = spawnCompassWeb(); + cleanupFns.push(() => { + if (compassWeb.pid) { + debug(`Killing compass-web [${compassWeb.pid}]`); + kill(compassWeb.pid, 'SIGINT'); + } else { + debug('No pid for compass-web'); + } + }); + await waitForCompassWebToBeReady(); + } + } + + debug('Getting mongodb server info'); + await updateMongoDBServerInfo(); + + throwIfAborted(); + + try { + debug('Clearing out past logs'); + fs.rmdirSync(LOG_PATH, { recursive: true }); + } catch (e) { + debug('.log dir already removed'); + } + + fs.mkdirSync(LOG_PATH, { recursive: true }); + + if (TEST_COMPASS_DESKTOP) { + if (TEST_COMPASS_DESKTOP_PACKAGED_APP) { + debug('Building Compass before running the tests ...'); + await buildCompass(); + } else { + debug('Preparing Compass before running the tests'); + + if (!SKIP_NATIVE_MODULE_REBUILD) { + debug('Rebuilding native modules ...'); + await rebuildNativeModules(); + } + + if (!SKIP_COMPASS_DESKTOP_COMPILE) { + debug('Compiling Compass assets ...'); + await compileCompassAssets(); + } + } + } + + throwIfAborted(); + + cleanupFns.push(() => { + removeUserDataDir(); + }); + } catch (err) { + if (globalFixturesAbortController.signal.aborted) { + return; + } + throw err; + } +} + +export async function mochaGlobalTeardown() { + debug('Cleaning up after the tests ...'); + await Promise.allSettled( + cleanupFns.map((fn) => { + return fn(); + }) + ); +} + +function spawnCompassWeb() { + const proc = crossSpawn.spawn( + 'npm', + ['run', '--unsafe-perm', 'start', '--workspace', '@mongodb-js/compass-web'], + { + env: { + ...process.env, + OPEN_BROWSER: 'false', // tell webpack dev server not to open the default browser + DISABLE_DEVSERVER_OVERLAY: 'true', + APP_ENV: 'webdriverio', + }, + } + ); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + return proc; +} + +async function waitForCompassWebToBeReady() { + let serverReady = false; + const start = Date.now(); + while (!serverReady) { + throwIfAborted(); + if (Date.now() - start >= 120_000) { + throw new Error( + 'The compass-web sandbox is still not running after 120000ms' + ); + } + try { + const res = await fetch(COMPASS_WEB_SANDBOX_URL); + serverReady = res.ok; + debug('Web server ready:', serverReady); + } catch (err) { + debug('Failed to connect to dev server:', (err as any).message); + } + await wait(1000); + } +} + +async function updateMongoDBServerInfo() { + try { + for (const { connectionOptions } of DEFAULT_CONNECTIONS) { + let client: MongoClient | undefined; + try { + client = new MongoClient(connectionOptions.connectionString); + const info = await client.db('admin').command({ buildInfo: 1 }); + DEFAULT_CONNECTIONS_SERVER_INFO.push({ + version: info.version, + enterprise: isEnterprise(info), + }); + } finally { + void client?.close(true); + } + } + } catch (err) { + debug('Failed to get MongoDB server info:', err); + } +} diff --git a/packages/compass-e2e-tests/index.ts b/packages/compass-e2e-tests/index.ts index 309767330a2..7efbb9710b8 100644 --- a/packages/compass-e2e-tests/index.ts +++ b/packages/compass-e2e-tests/index.ts @@ -1,384 +1,175 @@ #!/usr/bin/env ts-node import path from 'path'; -import fs from 'fs'; import { glob } from 'glob'; import crossSpawn from 'cross-spawn'; -import type { ChildProcessWithoutNullStreams } from 'child_process'; -// @ts-expect-error it thinks process does not have getActiveResourcesInfo -import { getActiveResourcesInfo } from 'process'; + import Mocha from 'mocha'; import Debug from 'debug'; -import type { MongoClient } from 'mongodb'; -import kill from 'tree-kill'; import { - rebuildNativeModules, - compileCompassAssets, - buildCompass, - COMPASS_PATH, - LOG_PATH, - removeUserDataDir, - updateMongoDBServerInfo, -} from './helpers/compass'; -import ResultLogger from './helpers/result-logger'; + ALLOWED_RUNNER_ARGS, + MOCHA_BAIL, + MOCHA_DEFAULT_TIMEOUT, +} from './helpers/test-runner-context'; +import { + abortRunner, + mochaGlobalSetup, + mochaGlobalTeardown, +} from './helpers/test-runner-global-fixtures'; +import { mochaRootHooks } from './helpers/insert-data'; +// @ts-expect-error no types for this package +import logRunning from 'why-is-node-running'; const debug = Debug('compass-e2e-tests'); -const wait = (ms: number) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - -const allowedArgs = [ - '--test-compass-web', - '--no-compile', - '--no-native-modules', - '--test-packaged-app', - '--disable-start-stop', - '--bail', -]; - for (const arg of process.argv) { - if (arg.startsWith('--') && !allowedArgs.includes(arg)) { + if (arg.startsWith('--') && !ALLOWED_RUNNER_ARGS.includes(arg)) { throw Error( - `Unknown command argument "${arg}". Usage:\n\n npm run test ${allowedArgs - .map((arg) => `[${arg}]`) - .join(' ')}\n` + `Unknown command argument "${arg}". Usage:\n\n npm run test ${ALLOWED_RUNNER_ARGS.map( + (arg) => `[${arg}]` + ).join(' ')}\n` ); } } -// We can't import mongodb here yet because native modules will be recompiled -let metricsClient: MongoClient; - const FIRST_TEST = 'tests/time-to-first-query.test.ts'; -let compassWeb: ChildProcessWithoutNullStreams; - -async function setup() { - debug('X DISPLAY', process.env.DISPLAY); - - const disableStartStop = process.argv.includes('--disable-start-stop'); - const shouldTestCompassWeb = process.argv.includes('--test-compass-web'); - - // When working on the tests it is faster to just keep the server running. - if (!disableStartStop) { - debug('Starting MongoDB server'); - crossSpawn.sync('npm', ['run', 'start-servers'], { stdio: 'inherit' }); - - if (shouldTestCompassWeb) { - debug('Starting Compass Web'); - compassWeb = crossSpawn.spawn( - 'npm', - ['run', '--unsafe-perm', 'start-web'], - { - cwd: path.resolve(__dirname, '..', '..'), - env: { - ...process.env, - OPEN_BROWSER: 'false', // tell webpack dev server not to open the default browser - DISABLE_DEVSERVER_OVERLAY: 'true', - APP_ENV: 'webdriverio', - }, - } - ); - - compassWeb.stdout.pipe(process.stdout); - compassWeb.stderr.pipe(process.stderr); - - let serverReady = false; - const start = Date.now(); - while (!serverReady) { - if (Date.now() - start >= 120_000) { - throw new Error( - 'The compass-web sandbox is still not running after 120000ms' - ); - } - try { - const res = await fetch('http://localhost:7777'); - serverReady = res.ok; - debug('Web server ready: %s', serverReady); - } catch (err) { - debug('Failed to connect to dev server: %s', (err as any).message); - } - await wait(1000); - } - } else { - debug('Writing electron-versions.json'); - crossSpawn.sync('scripts/write-electron-versions.sh', [], { - stdio: 'inherit', - }); - } - } - - try { - debug('Clearing out past logs'); - fs.rmdirSync(LOG_PATH, { recursive: true }); - } catch (e) { - debug('.log dir already removed'); - } - - fs.mkdirSync(LOG_PATH, { recursive: true }); - - debug('Getting mongodb server info'); - updateMongoDBServerInfo(); -} - -function getResources() { - const resources: Record = {}; - for (const resource of getActiveResourcesInfo()) { - if (resources[resource] === undefined) { - resources[resource] = 0; - } - ++resources[resource]; - } - return resources; +// Trigger a mocha abort on interrupt. This doesn't stop the test runner +// immediately as it will still try to finish running the current in-progress +// suite before exiting, but the upside is that we are getting a way more robust +// cleanup where all the after hooks are taken into account as expected rarely +// leaving anythihg "hanging" +async function cleanupOnInterrupt() { + // First trigger an abort on the mocha runner + abortRunner?.(); + await runnerPromise; } -function cleanup() { - removeUserDataDir(); - - const disableStartStop = process.argv.includes('--disable-start-stop'); - const shouldTestCompassWeb = process.argv.includes('--test-compass-web'); - - if (!disableStartStop) { - if (shouldTestCompassWeb) { - debug('Stopping compass-web'); - try { - if (compassWeb.pid) { - debug(`killing compass-web [${compassWeb.pid}]`); - kill(compassWeb.pid, 'SIGINT'); - } else { - debug('no pid for compass-web'); - } - } catch (e) { - debug('Failed to stop compass-web', e); - } - } - - debug('Stopping MongoDB server'); - try { - crossSpawn.sync('npm', ['run', 'stop-servers'], { - // If it's taking too long we might as well kill the process and move on, - // mongodb-runner is flaky sometimes and in ci `posttest-ci` script will - // take care of additional clean up anyway - timeout: 120_000, - stdio: 'inherit', - }); - } catch (e) { - debug('Failed to stop MongoDB Server', e); - } - debug('Done stopping'); - } - - // Since the webdriverio update something is messing with the terminal's - // cursor. This brings it back. - crossSpawn.sync('tput', ['cnorm'], { stdio: 'inherit' }); - - // Log what's preventing the process from exiting normally to help debug the - // cases where the process hangs and gets killed 10 minutes later by evergreen - // because there's no output. - const intervalId = setInterval(() => { - console.log(getResources()); - }, 1_000); - - // Don't keep logging forever because then evergreen can't kill the job after - // 10 minutes of inactivity if we get into a broken state +function terminateOnTimeout() { + // Don't keep logging forever because then evergreen can't kill the job + // after 10 minutes of inactivity if we get into a broken state const timeoutId = setTimeout(() => { - clearInterval(intervalId); - - // Just exit now rather than waiting for 10 minutes just so evergreen can - // kill the task and fail anyway. - process.exit(process.exitCode ?? 1); - }, 60_000); - - // No need to hold things up for a minute if there's nothing else preventing - // the process from exiting. - intervalId.unref(); + debug('Mocha is still cleaning up:'); + // Log what's preventing the process from exiting normally to help debug the + // cases where the process hangs and gets killed 10 minutes later by evergreen + // because there's no output. + logRunning(console); + debug('Terminating the process ...'); + // Just exit now rather than waiting for 10 minutes just so evergreen + // can kill the task and fail anyway. + process.exitCode ??= 1; + process.exit(); + }, 30_000); timeoutId.unref(); } -async function main() { - await setup(); - - const shouldTestCompassWeb = process.argv.includes('--test-compass-web'); - - if (!shouldTestCompassWeb) { - if (!process.env.CHROME_VERSION) { - // written during setup() if disableStartStop is false - const versionsJSON = await fs.promises.readFile( - 'electron-versions.json', - 'utf8' - ); - const versions = JSON.parse(versionsJSON); - process.env.CHROME_VERSION = versions.chrome; - } - debug( - 'Chrome version corresponding to Electron:', - process.env.CHROME_VERSION - ); - } - - // These are mutually exclusive since compass-web is always going to browse to - // the running webserver. - const shouldTestPackagedApp = - process.argv.includes('--test-packaged-app') && !shouldTestCompassWeb; - - // Skip this step if you are running tests consecutively and don't need to - // rebuild modules all the time. Also no need to ever recompile when testing - // compass-web. - const noNativeModules = - process.argv.includes('--no-native-modules') || shouldTestCompassWeb; - - // Skip this step if you want to run tests against your own compilation (e.g, - // a dev build or a build running in watch mode that autorecompiles). Also no - // need to recompile when testing compass-web. - const noCompile = - process.argv.includes('--no-compile') || shouldTestCompassWeb; - - if (shouldTestPackagedApp) { - process.env.TEST_PACKAGED_APP = '1'; - debug('Building Compass before running the tests ...'); - await buildCompass(); - } else { - delete process.env.TEST_PACKAGED_APP; - - // set coverage to the root of the monorepo so it will be generated for - // everything and not just packages/compass - process.env.COVERAGE = path.dirname(path.dirname(COMPASS_PATH)); - - debug('Preparing Compass before running the tests'); - if (!noNativeModules) { - debug('Rebuilding native modules ...'); - await rebuildNativeModules(); - } - if (!noCompile) { - debug('Compiling Compass assets ...'); - await compileCompassAssets(); - } - } +let runnerPromise: Promise | undefined; +async function main() { const e2eTestGroupsAmount = parseInt(process.env.E2E_TEST_GROUPS || '1'); const e2eTestGroup = parseInt(process.env.E2E_TEST_GROUP || '1'); + const e2eTestFilter = process.env.E2E_TEST_FILTER || '*'; - const rawTests = ( - await glob('tests/**/*.{test,spec}.ts', { + const tests = ( + await glob(`tests/**/${e2eTestFilter}.{test,spec}.ts`, { cwd: __dirname, }) - ).filter((value, index, array) => { - const testsPerGroup = Math.ceil(array.length / e2eTestGroupsAmount); - const minGroupIndex = (e2eTestGroup - 1) * testsPerGroup; - const maxGroupIndex = minGroupIndex + testsPerGroup - 1; + ) + .filter((_value, index, array) => { + const testsPerGroup = Math.ceil(array.length / e2eTestGroupsAmount); + const minGroupIndex = (e2eTestGroup - 1) * testsPerGroup; + const maxGroupIndex = minGroupIndex + testsPerGroup - 1; - return index >= minGroupIndex && index <= maxGroupIndex; - }); + return index >= minGroupIndex && index <= maxGroupIndex; + }) + .sort((a, b) => { + // The only test file that's interested in the first-run experience (at the + // time of writing) is time-to-first-query.ts and that happens to be + // alphabetically right at the end. Which is fine, but the first test to run + // will also get the slow first run experience for no good reason unless it is + // the time-to-first-query.ts test. + // So yeah.. this is a bit of a micro optimisation. + if (a === FIRST_TEST) { + return -1; + } else if (b === FIRST_TEST) { + return 1; + } else { + return 0; + } + }); - console.info('Test files:', rawTests); + debug('Test files:', tests); - // The only test file that's interested in the first-run experience (at the - // time of writing) is time-to-first-query.ts and that happens to be - // alphabetically right at the end. Which is fine, but the first test to run - // will also get the slow first run experience for no good reason unless it is - // the time-to-first-query.ts test. - // So yeah.. this is a bit of a micro optimisation. - const tests = [FIRST_TEST, ...rawTests.filter((t) => t !== FIRST_TEST)]; + const mocha = new Mocha({ + timeout: MOCHA_DEFAULT_TIMEOUT, + bail: MOCHA_BAIL, + reporter: require.resolve('@mongodb-js/mocha-config-compass/reporter'), + }); - // Ensure the insert-data mocha hooks are run. - tests.unshift(path.join('helpers', 'insert-data.ts')); + // @ts-expect-error mocha types are incorrect, global setup this is bound to + // runner, not context + mocha.globalSetup(mochaGlobalSetup); + mocha.enableGlobalSetup(true); - const bail = process.argv.includes('--bail'); + mocha.globalTeardown(mochaGlobalTeardown); + mocha.enableGlobalTeardown(true); - const mocha = new Mocha({ - timeout: 240_000, // kinda arbitrary, but longer than waitforTimeout set in helpers/compass.ts so the test can fail before it times out - bail, - reporter: path.resolve( - __dirname, - '..', - '..', - 'configs/mocha-config-compass/reporter.js' - ), - }); + mocha.rootHooks(mochaRootHooks); // print the test order for debugging purposes and so we can tweak the groups later - console.log('test order', tests); + debug('Test order:', tests); tests.forEach((testPath: string) => { mocha.addFile(path.join(__dirname, testPath)); }); - const metricsConnection = process.env.E2E_TESTS_METRICS_URI; - if (metricsConnection) { - debug('Connecting to E2E_TESTS_METRICS_URI'); - // only require it down here because it gets rebuilt up top - const mongodb = await import('mongodb'); - metricsClient = new mongodb.MongoClient(metricsConnection); - await metricsClient.connect(); - } else { - debug('Not logging metrics to a database.'); - } - debug('Running E2E tests'); - // mocha.run has a callback and returns a result, so just promisify it manually - const { resultLogger, failures } = await new Promise<{ - resultLogger: ResultLogger; - failures: number; - }>((resolve, reject) => { - // eslint-disable-next-line prefer-const - let resultLogger: ResultLogger; - - const runner = mocha.run((failures: number) => { + runnerPromise = new Promise((resolve) => { + mocha.run((failures: number) => { + debug('Finished running e2e tests', { failures }); process.exitCode = failures ? 1 : 0; - resolve({ resultLogger, failures }); - }); - - debug('Initialising ResultLogger'); - resultLogger = new ResultLogger(metricsClient, runner); - - // Synchronously create the ResultLogger so it can start listening to events - // on runner immediately after calling mocha.run() before any of the events - // fire. - resultLogger.init().catch((err: Error) => { - // reject() doesn't stop mocha.run()... - reject(err); + // Since the webdriverio update something is messing with the terminal's + // cursor. This brings it back. + crossSpawn.sync('tput', ['cnorm'], { stdio: 'inherit' }); + terminateOnTimeout(); + resolve(failures); }); }); - - await resultLogger.done(failures); } process.once('SIGINT', () => { - debug(`Process was interrupted. Cleaning-up and exiting.`); - cleanup(); - process.kill(process.pid, 'SIGINT'); + debug(`Process was interrupted. Waiting for mocha to abort and clean-up ...`); + void (async () => { + await cleanupOnInterrupt(); + process.kill(process.pid, 'SIGINT'); + })(); }); process.once('SIGTERM', () => { - debug(`Process was terminated. Cleaning-up and exiting.`); - cleanup(); - process.kill(process.pid, 'SIGTERM'); + debug(`Process was terminated. Waiting for mocha to abort and clean-up ...`); + void (async () => { + await cleanupOnInterrupt(); + process.kill(process.pid, 'SIGTERM'); + })(); }); process.once('uncaughtException', (err: Error) => { - debug('Uncaught exception. Cleaning-up and exiting.'); - cleanup(); - throw err; + debug('Uncaught exception:'); + console.error(err.stack || err.message || err); + debug('Waiting for mocha to abort and clean-up ...'); + process.exitCode = 1; + void cleanupOnInterrupt(); }); process.on('unhandledRejection', (err: Error) => { - debug('Unhandled exception. Cleaning-up and exiting.'); - cleanup(); + debug('Unhandled exception:'); console.error(err.stack || err.message || err); + debug('Waiting for mocha to abort and clean-up ...'); process.exitCode = 1; + void cleanupOnInterrupt(); }); async function run() { - try { - await main(); - } finally { - if (metricsClient) { - await metricsClient.close(); - } - - cleanup(); - } + await main(); } void run(); diff --git a/packages/compass-e2e-tests/package.json b/packages/compass-e2e-tests/package.json index 7acaff74787..c1b6f329197 100644 --- a/packages/compass-e2e-tests/package.json +++ b/packages/compass-e2e-tests/package.json @@ -10,7 +10,6 @@ "lint": "npm run eslint . && npm run prettier -- --check .", "depcheck": "depcheck", "check": "npm run lint && npm run depcheck", - "pretest": "npm run unzip-fixtures", "test": "xvfb-maybe --auto-servernum --server-args=\"-screen 0 1432x840x24\" -- ts-node index.ts", "test-ci": "npm run test", "posttest-ci": "npm run coverage-report", @@ -23,14 +22,12 @@ "stop-server-2": "mongodb-runner stop --id=e2e-2", "start-servers": "npm run start-server-1 && npm run start-server-2", "stop-servers": "npm run stop-server-1 && npm run stop-server-2", - "unzip-fixtures": "ts-node ./scripts/gunzip.ts fixtures/*.gz", "test-noserver": "env DEBUG=hadron*,mongo*,compass*,xvfb-maybe* npm run test -- --disable-start-stop --bail", "test-noserver-nocompile": "env DEBUG=hadron*,mongo*,compass*,xvfb-maybe* npm run test -- --no-native-modules --no-compile --disable-start-stop --bail", "test-web": "env DEBUG=hadron*,mongo*,compass*,xvfb-maybe* npm run test -- --test-compass-web", "test-web-noserver": "env DEBUG=hadron*,mongo*,compass*,xvfb-maybe* npm run test -- --test-compass-web --disable-start-stop --bail", "coverage-merge": "nyc merge .log/coverage .nyc_output/coverage.json", - "coverage-report": "npm run coverage-merge && nyc report", - "server-info": "ts-node ./scripts/server-info.ts" + "coverage-report": "npm run coverage-merge && nyc report" }, "devDependencies": { "@electron/rebuild": "^3.7.0", @@ -53,13 +50,14 @@ "debug": "^4.3.4", "depcheck": "^1.4.1", "electron": "^32.2.1", + "electron-to-chromium": "^1.5.41", "eslint": "^7.25.0", - "fast-glob": "^3.2.7", "glob": "^10.2.5", "hadron-build": "^25.5.12", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.9.0", + "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", "mongodb-log-writer": "^1.4.2", "mongodb-runner": "^5.6.3", @@ -73,6 +71,7 @@ "tree-kill": "^1.2.2", "ts-node": "^10.9.1", "webdriverio": "^8.40.0", + "why-is-node-running": "^2.3.0", "xvfb-maybe": "^0.2.1" } } diff --git a/packages/compass-e2e-tests/scripts/gunzip.ts b/packages/compass-e2e-tests/scripts/gunzip.ts deleted file mode 100755 index 8974f4588a1..00000000000 --- a/packages/compass-e2e-tests/scripts/gunzip.ts +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env ts-node - -import fastGlob from 'fast-glob'; -import { createReadStream, createWriteStream } from 'fs'; -import { pipeline } from 'stream'; -import { promisify } from 'util'; -import { createGunzip } from 'zlib'; - -const pipe = promisify(pipeline); - -async function gunzip(input: string, output: string) { - const readStream = createReadStream(input); - const gunzip = createGunzip(); - const writeStream = createWriteStream(output); - - await pipe(readStream, gunzip, writeStream); -} - -async function run() { - // windows does not expand * automatically - const filenames = await fastGlob(process.argv.slice(2)); - - for (const input of filenames) { - const output = input.replace(/\.gz$/, ''); - console.log(input, '=>', output); - await gunzip(input, output); - } -} - -if (require.main === module) { - run().catch((err: Error) => { - console.error('An error occurred:', err); - process.exitCode = 1; - }); -} - -module.exports = gunzip; diff --git a/packages/compass-e2e-tests/scripts/server-info.ts b/packages/compass-e2e-tests/scripts/server-info.ts deleted file mode 100644 index abe9bf1f883..00000000000 --- a/packages/compass-e2e-tests/scripts/server-info.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MongoClient } from 'mongodb'; - -export async function getServerVersion(connectionString: string) { - const client = await MongoClient.connect(connectionString); - try { - const buildInfo = await client.db('admin').command({ buildInfo: 1 }); - return { - version: buildInfo.version, - enterprise: buildInfo.modules?.includes('enterprise') || false, - }; - } finally { - await client.close(); - } -} - -void (async () => { - try { - const index = process.argv.indexOf('--connectionString') ?? -1; - const connectionString = - index === -1 ? 'mongodb://127.0.0.1:27091' : process.argv[index + 1]; - console.log(JSON.stringify(await getServerVersion(connectionString))); - } catch (err) { - const { name, message } = err as Error; - console.error(`${name}: ${message}`); - process.exitCode = 1; - } -})(); diff --git a/packages/compass-e2e-tests/scripts/write-electron-versions.sh b/packages/compass-e2e-tests/scripts/write-electron-versions.sh deleted file mode 100755 index afaf990782a..00000000000 --- a/packages/compass-e2e-tests/scripts/write-electron-versions.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -# To get the versions of various electron dependencies, including chromium we're -# mostly interested in, we run a script file using currently installed electron -# version binary. We don't use interactive electron repl (electron -i) here -# instead because Windows doesn't support it. -script=" -const fs = require('fs'); -fs.writeFileSync( - 'electron-versions.json', - JSON.stringify(process.versions), - 'utf8' -); -process.exit(); -" -script_name=write-electron-versions.js -echo $script >$script_name -npx electron --no-sandbox $script_name diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index bb902fddfb4..8e5e171bbc6 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -40,6 +40,7 @@ "webpack": "webpack-compass", "postcompile": "npm run typescript", "typescript": "tsc -p tsconfig-build.json --emitDeclarationOnly", + "prestart": "npm run compile -w @mongodb-js/webpack-config-compass", "start": "electron ./scripts/electron-proxy.js", "analyze": "npm run webpack -- --mode production --analyze", "watch": "npm run webpack -- --mode development --watch", diff --git a/packages/compass/package.json b/packages/compass/package.json index c4df6b276a0..90e5d20e96c 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -141,7 +141,7 @@ "scripts": { "install": "node scripts/download-fonts.js && node scripts/download-csfle.js", "electron-rebuild": "electron-rebuild --only kerberos,keytar,interruptor,os-dns-native,win-export-certificate-and-key,macos-export-certificate-and-key --prebuild-tag-prefix not-real-prefix-to-force-rebuild", - "prestart": "npm run electron-rebuild", + "prestart": "npm run electron-rebuild && npm run compile --workspace=@mongodb-js/webpack-config-compass", "start": "HADRON_DISTRIBUTION=${HADRON_DISTRIBUTION:-compass} npm run webpack serve -- --mode development", "test-electron": "npm run test-main && npm run test-renderer", "test-main": "xvfb-maybe electron-mocha --no-sandbox \"./src/main/**/*.spec.*\" \"./src/main/**/*.test.*\"",