From 5e2b3e90b26e3a26b29a09ecd6aa51c572aae30c Mon Sep 17 00:00:00 2001 From: David Evans Date: Sun, 29 Oct 2023 13:35:36 +0000 Subject: [PATCH] Convert test launcher to javascript --- package.json | 4 +- scripts/build.mjs | 41 +++--- scripts/e2e.sh | 132 ------------------- scripts/helpers/proc.mjs | 268 ++++++++++++++++++++++++++++++--------- scripts/install.mjs | 12 +- scripts/lint.mjs | 45 ++++--- scripts/start.mjs | 43 ++++--- scripts/test.mjs | 165 ++++++++++++++++++++++++ scripts/test.sh | 19 --- 9 files changed, 443 insertions(+), 286 deletions(-) delete mode 100755 scripts/e2e.sh create mode 100755 scripts/test.mjs delete mode 100755 scripts/test.sh diff --git a/package.json b/package.json index 119adfa..6372e53 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "install": "scripts/install.mjs --force", "lint": "scripts/install.mjs && scripts/lint.mjs", "start": "SKIP_E2E_DEPS=true scripts/install.mjs && scripts/start.mjs", - "test": "scripts/install.mjs && scripts/lint.mjs && scripts/test.sh", + "test": "scripts/install.mjs && scripts/lint.mjs && scripts/test.mjs", "format": "npm --prefix=backend run format --quiet && npm --prefix=frontend run format --quiet && npm --prefix=e2e run format --quiet", "test:backend": "npm --prefix=backend test --quiet --", "test:frontend": "npm --prefix=frontend test --quiet --", "test:frontend:watch": "npm --prefix=frontend test --quiet -- --watch", - "test:e2e": "scripts/install.mjs && scripts/e2e.sh" + "test:e2e": "scripts/install.mjs && scripts/test.mjs --only-e2e" } } diff --git a/scripts/build.mjs b/scripts/build.mjs index 98b53fe..23c6281 100755 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -12,7 +12,7 @@ import { copy, } from './helpers/io.mjs'; import { stat, rename, chmod } from 'node:fs/promises'; -import { runTask, runTaskPrefixOutput } from './helpers/proc.mjs'; +import { runMultipleTasks, runTask } from './helpers/proc.mjs'; const PARALLEL_BUILD = (process.env['PARALLEL_BUILD'] ?? 'true') === 'true'; const KEEP_DEPS = process.argv.slice(2).includes('--keep-deps'); @@ -24,39 +24,28 @@ const packages = [ const builddir = join(basedir, 'build'); const staticdir = join(builddir, 'static'); -try { - await runTask('diff', [ +await runTask({ + command: 'diff', + args: [ join(basedir, 'frontend', 'src', 'shared', 'api-entities.ts'), join(basedir, 'backend', 'src', 'shared', 'api-entities.ts'), - ]); -} catch { - log('Shared api-entities.ts files do not match.'); - process.exit(1); -} + ], + outputMode: 'fail_atomic', + failureMessage: 'Shared api-entities.ts files do not match.', +}); -async function buildPackage({ dir, format }) { - log(`Building ${dir}...`); - await runTaskPrefixOutput({ +await runMultipleTasks( + packages.map(({ dir, format }) => ({ command: 'npm', args: ['run', 'build', '--quiet'], cwd: join(basedir, dir), + beginMessage: `Building ${dir}...`, + failureMessage: `Failed to build ${dir}.`, outputPrefix: dir, prefixFormat: format, - }); -} - -try { - if (PARALLEL_BUILD) { - await Promise.all(packages.map(buildPackage)); - } else { - for (const pkg of packages) { - await buildPackage(pkg); - } - } -} catch { - log('Build failed.'); - process.exit(1); -} + })), + { parallel: PARALLEL_BUILD }, +); let preserveBuildModules = false; const buildModules = join(builddir, 'node_modules'); diff --git a/scripts/e2e.sh b/scripts/e2e.sh deleted file mode 100755 index ab0eabd..0000000 --- a/scripts/e2e.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash -set -e; -set -o pipefail; - -BASEDIR="$(dirname "$0")/.."; -BUILDDIR="$BASEDIR/build"; - -E2E_WORKDIR="$BASEDIR/e2e/build"; -DOWNLOADS="$E2E_WORKDIR/downloads"; - -rm -rf "$E2E_WORKDIR" || true; -mkdir -p "$E2E_WORKDIR"; -mkdir -p "$DOWNLOADS"; - -if [ -z "$TARGET_HOST" ]; then - PORT="${PORT:-5010}"; - - # Build and launch all-in-one server - "$BASEDIR/scripts/build.mjs" --keep-deps; - - if [ ! -d "$BUILDDIR/node_modules" ]; then - echo "Installing production dependencies..."; - npm --prefix="$BUILDDIR" install --omit=dev --quiet; - fi; - - echo 'Using mock authentication provider'; - MOCK_SSO_PORT="$((PORT + 2))"; - MOCK_SSO_HOST="http://localhost:$MOCK_SSO_PORT"; - export SSO_GOOGLE_CLIENT_ID='mock-client-id'; - export SSO_GOOGLE_AUTH_URL="$MOCK_SSO_HOST/auth"; - export SSO_GOOGLE_TOKEN_INFO_URL="$MOCK_SSO_HOST/tokeninfo"; - - echo 'Using randomised secrets'; - export $("$BASEDIR/scripts/random-secrets.mjs" | tee /dev/stderr | xargs); - - # TODO replace express with something else to be able to add --disallow-code-generation-from-strings - # TODO once https://github.com/nodejs/node/issues/50452 is resolved, add NODE_OPTIONS='--experimental-policy="'"$BUILDDIR/policy.json"'"' - PORT="$PORT" \ - MOCK_SSO_PORT="$MOCK_SSO_PORT" \ - SERVER_BIND_ADDRESS="localhost" \ - DB_URL="memory://refacto?simulatedLatency=50" \ - node \ - --disable-proto throw \ - "$BUILDDIR/index.js" \ - > "$E2E_WORKDIR/app.log" 2>&1 & APP_PID="$!"; - - trap "kill '$APP_PID'; wait '$APP_PID' > /dev/null && false" EXIT; - - # Wait for startup - while [ ! -f "$E2E_WORKDIR/app.log" ] || ! grep 'Available at' < "$E2E_WORKDIR/app.log" > /dev/null 2>&1; do - if ! ps -p "$APP_PID" > /dev/null; then - trap - EXIT; - APP_EXIT_CODE="$(wait "$APP_PID" > /dev/null; echo "$?")"; - echo; - echo 'Application logs:'; - sed "s/^/> /" < "$E2E_WORKDIR/app.log"; - echo; - echo "Failed to start server for E2E tests (exit code: $APP_EXIT_CODE)."; - false; - fi; - sleep 0.1; - done; - - TARGET_HOST="http://localhost:$PORT/"; -fi; - -export TARGET_HOST; - -# Run tests - -E2E_PIDS=""; -FAILED=''; - -launch_e2e() { - local NAME="$1"; - if [ -n "$BROWSER" ] && [ "$BROWSER" != "$NAME" ]; then - echo "Skipping E2E testing in $NAME"; - return; - fi; - echo "E2E testing in $NAME..."; - if [ "$PARALLEL_E2E" == 'true' ]; then - # pipefail required here - SELENIUM_BROWSER="$NAME" \ - npm --prefix="$BASEDIR/e2e" test --quiet 2>&1 | sed "s/^/$NAME: /" & - E2E_PIDS="$E2E_PIDS $!"; - else - if ! SELENIUM_BROWSER="$NAME" npm --prefix="$BASEDIR/e2e" test --quiet; then - FAILED='true'; - fi; - E2E_PIDS="-"; - fi; -} - -launch_e2e 'chrome'; -launch_e2e 'firefox'; - -if [ "$E2E_PIDS" != '' ] && [ "$E2E_PIDS" != '-' ]; then - for PID in $E2E_PIDS; do - if ! wait "$PID"; then - FAILED='true'; - fi; - done; -fi; - -# Shutdown app server -if [ -n "$APP_PID" ]; then - kill -2 "$APP_PID"; - wait "$APP_PID" || true; - trap - EXIT; -fi; - -if [ "$E2E_PIDS" == '' ]; then - echo 'Did not run any end-to-end tests as no drivers were found.'; - false; -fi; - -if [ "$FAILED" != '' ]; then - echo; - echo 'Application logs:'; - sed "s/^/> /" < "$E2E_WORKDIR/app.log"; - echo; - echo 'Files downloaded:'; - ls "$DOWNLOADS"; - echo; - echo 'End-to-end tests failed.'; - false; -fi; - -rm -rf "$DOWNLOADS" || true; - -echo; -echo 'End-to-end tests complete.'; diff --git a/scripts/helpers/proc.mjs b/scripts/helpers/proc.mjs index 6386424..f9b266d 100644 --- a/scripts/helpers/proc.mjs +++ b/scripts/helpers/proc.mjs @@ -1,16 +1,37 @@ -import { spawn, execFile } from 'node:child_process'; +import { spawn } from 'node:child_process'; + +const activeChildren = new Set(); +let shuttingDown = false; + +async function closeAllChildren() { + const snapshot = [...activeChildren.values()]; + activeChildren.clear(); + await Promise.allSettled(snapshot.map(interruptTask)); +} + +export async function exitWithCode(code, message) { + shuttingDown = true; + await closeAllChildren(); + if (message) { + process.stderr.write(`\n${message}\n`); + } + process.exit(code); +} + +process.on('SIGINT', () => exitWithCode(2, 'interrupted').catch(() => null)); const MAX_LINE_BUFFER = 10000; export function propagateStreamWithPrefix( - stream, target, + stream, prefix, - prefixFormat, + prefixFormat = '', ) { - const pre = target.isTTY - ? `\u001B[${prefixFormat}m${prefix}:\u001B[0m ` - : `${prefix}: `; + const pre = + target.isTTY && prefixFormat + ? `\u001B[${prefixFormat}m${prefix}:\u001B[0m ` + : `${prefix}: `; const suf = target.isTTY ? '\u001B[0m\n' : '\n'; const curLine = Buffer.alloc(MAX_LINE_BUFFER); let curLineP = 0; @@ -52,6 +73,69 @@ export function propagateStreamWithPrefix( }); } +export function printPrefixed(target, message, prefix, prefixFormat = '') { + const pre = + target.isTTY && prefixFormat + ? `\u001B[${prefixFormat}m${prefix}:\u001B[0m ` + : `${prefix}: `; + const suf = target.isTTY ? '\u001B[0m\n' : '\n'; + const lines = message.split('\n'); + for (const line of lines) { + target.write(pre); + target.write(line); + target.write(suf); + } +} + +export function waitForOutput(stream, output, timeout = 60 * 60 * 1000) { + let all = []; + const promise = new Promise((resolve, reject) => { + let found = !output; + let tm; + if (!found) { + tm = setTimeout(() => { + if (!found) { + reject(new Error('timeout')); + } + }, timeout); + } + stream.on('data', (data) => { + all.push(data); + if (found) { + return; + } + if (data.toString('utf-8').includes(output)) { + found = true; + clearTimeout(tm); + resolve(); + } + if (all.length < 2) { + return; + } + const combined = Buffer.concat(all); + all = [combined]; + // check the boundary between data frames + const boundary = all.length - data.length; + const boundaryRegion = all.subarray( + Math.max(boundary - output.length * 4, 0), + boundary + output.length * 4, + ); + if (boundaryRegion.toString('utf-8').includes(output)) { + found = true; + clearTimeout(tm); + resolve(); + } + }); + stream.on('close', () => { + if (!found) { + clearTimeout(tm); + reject(new Error('closed')); + } + }); + }); + return { promise, getOutput: () => Buffer.concat(all).toString('utf-8') }; +} + const handleExit = (resolve, reject) => (code, signal) => { if (code === 0) { resolve(); @@ -62,72 +146,136 @@ const handleExit = (resolve, reject) => (code, signal) => { } }; -export function runTaskPrefixOutput({ - command, - args, - outputTarget = process.stderr, - outputPrefix = command, - prefixFormat = '35', - ...options -} = {}) { - return new Promise((resolve, reject) => { - const proc = spawn(command, args, { - ...options, - stdio: ['ignore', 'pipe', 'pipe'], - }); - propagateStreamWithPrefix( - proc.stdio[1], - outputTarget, - outputPrefix, - prefixFormat, - ); - propagateStreamWithPrefix( - proc.stdio[2], - outputTarget, - outputPrefix, - prefixFormat, - ); - proc.on('error', reject); - proc.on('exit', handleExit(resolve, reject)); - }); -} - -export function runTaskPrintOnFailure({ +export function runTask({ command, args = [], - failTarget = process.stderr, - failMessage = `${command} ${args.join(' ')} failed`, + output = process.stderr, + outputPrefix = '', + prefixFormat = '', + outputMode = 'live', + beginMessage = '', + successMessage = '', + failureMessage = '', + exitOnFailure = true, ...options -} = {}) { +}) { + let stdio = ['ignore', 'pipe', 'pipe']; + if (outputMode === 'live' && !outputPrefix) { + stdio = ['ignore', 'inherit', 'inherit']; + } return new Promise((resolve, reject) => { - const proc = execFile(command, args, options, (err, stdout, stderr) => { - if (!err && proc.exitCode === 0) { - resolve(); + if (shuttingDown) { + return; // neither resolve nor reject - the app is shutting down and will exit soon anyway + } + if (beginMessage) { + output.write(`${beginMessage}\n`); + } + let handled = false; + const proc = spawn(command, args, { ...options, stdio }); + activeChildren.add(proc); + let printInfo = () => undefined; + if (outputMode === 'live') { + if (outputPrefix) { + propagateStreamWithPrefix( + output, + proc.stdio[1], + outputPrefix, + prefixFormat, + ); + propagateStreamWithPrefix( + output, + proc.stdio[2], + outputPrefix, + prefixFormat, + ); + } + } else { + const s1 = waitForOutput(proc.stdio[1], ''); + const s2 = waitForOutput(proc.stdio[2], ''); + printInfo = () => { + output.write(`\nexit code: ${proc.exitCode}\n`); + const v1 = s1.getOutput(); + if (v1) { + output.write(`\nstdout:\n`); + printPrefixed(output, v1, outputPrefix, prefixFormat); + } + const v2 = s2.getOutput(); + if (v2) { + output.write(`\nstderr:\n`); + printPrefixed(output, v2, outputPrefix, prefixFormat); + } + }; + } + const wrappedResolve = (v) => { + activeChildren.delete(proc); + if (shuttingDown || handled) { return; } - failTarget.write(`${failMessage}\n\nexit code: ${proc.exitCode}\n`); - if (stdout) { - failTarget.write('\nstdout:\n'); - failTarget.write(stdout); - failTarget.write('\n'); + handled = true; + if (successMessage) { + output.write(`${successMessage}\n`); } - if (stderr) { - failTarget.write('\nstderr:\n'); - failTarget.write(stderr); - failTarget.write('\n'); + if (outputMode === 'atomic' || outputMode === 'success_atomic') { + printInfo(); } - reject(err ?? new Error(`returned exit code ${code}`)); - }); + resolve(v); + }; + const wrappedReject = (e) => { + activeChildren.delete(proc); + if (shuttingDown || handled) { + return; + } + handled = true; + if (failureMessage) { + output.write(`${failureMessage}\n`); + } + if (outputMode === 'atomic' || outputMode === 'fail_atomic') { + printInfo(); + } + if (exitOnFailure) { + exitWithCode(proc.exitCode || 1).catch(() => null); + } else { + reject(e); + } + }; + proc.on('error', wrappedReject); + proc.on('exit', handleExit(wrappedResolve, wrappedReject)); }); } -export function runTask(command, args, options = {}) { +export async function runMultipleTasks(tasks, { parallel = true } = {}) { + if (parallel) { + try { + await Promise.all(tasks.map(runTask)); + } catch (e) { + await closeAllChildren(); + throw e; + } + } else { + for (const task of tasks) { + await runTask(task); + } + } +} + +export function runBackgroundTask({ command, args = [], ...options }) { + if (shuttingDown) { + throw new Error('shutting down'); + } + const proc = spawn(command, args, options); + activeChildren.add(proc); + proc.on('error', () => activeChildren.delete(proc)); + proc.on('exit', () => activeChildren.delete(proc)); + return proc; +} + +export function interruptTask(proc) { return new Promise((resolve, reject) => { - const proc = spawn(command, args, { - ...options, - stdio: ['ignore', 'inherit', 'inherit'], - }); - proc.on('error', reject); + if (proc.exitCode !== null || proc.signalCode !== null) { + handleExit(resolve, reject)(proc.exitCode, proc.signalCode); + return; + } proc.on('exit', handleExit(resolve, reject)); + proc.kill(2); }); } diff --git a/scripts/install.mjs b/scripts/install.mjs index 7b031bb..aaf578c 100755 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -3,7 +3,7 @@ import { join } from 'node:path'; import { basedir, deleteDirectory, log, readJSON } from './helpers/io.mjs'; import { stat } from 'node:fs/promises'; -import { runTask } from './helpers/proc.mjs'; +import { exitWithCode, runTask } from './helpers/proc.mjs'; const SKIP_E2E_DEPS = (process.env['SKIP_E2E_DEPS'] ?? 'false') === 'true'; const FORCE = process.argv.slice(2).includes('--force'); @@ -19,15 +19,17 @@ Dependencies should not be installed in root package.json! - add the dependencies to the desired subproject instead - re-run install `); - deleteDirectory(join(basedir, 'node_modules')); - process.exit(1); + await deleteDirectory(join(basedir, 'node_modules')); + await exitWithCode(1); } async function installPackage(pkg) { const s = await stat(join(basedir, pkg, 'node_modules')).catch(() => null); if (s === null || FORCE) { - log(`Installing ${pkg} dependencies...`); - await runTask('npm', ['install', '--quiet'], { + await runTask({ + command: 'npm', + args: ['install', '--quiet'], + beginMessage: `Installing ${pkg} dependencies...`, cwd: join(basedir, pkg), env: { ...process.env, DISABLE_OPENCOLLECTIVE: '1' }, }); diff --git a/scripts/lint.mjs b/scripts/lint.mjs index d6bbaf8..3f094b4 100755 --- a/scripts/lint.mjs +++ b/scripts/lint.mjs @@ -2,7 +2,7 @@ import { join } from 'node:path'; import { basedir, log } from './helpers/io.mjs'; -import { runTaskPrintOnFailure } from './helpers/proc.mjs'; +import { runMultipleTasks } from './helpers/proc.mjs'; const packages = ['frontend', 'backend', 'e2e']; @@ -14,27 +14,26 @@ if (process.stdout.isTTY) { tscArgs.push('--pretty'); } -try { - await Promise.all( - packages.map(async (pkg) => { - await runTaskPrintOnFailure({ - command: join(basedir, pkg, 'node_modules', '.bin', 'tsc'), - args: tscArgs, - cwd: join(basedir, pkg), - failMessage: `Lint ${pkg} (tsc) failed`, - }); - await runTaskPrintOnFailure({ - command: join(basedir, pkg, 'node_modules', '.bin', 'prettier'), - args: prettierArgs, - cwd: join(basedir, pkg), - failMessage: `Lint ${pkg} (prettier) failed`, - }); - log(`Lint ${pkg} succeeded`); - }), - ); -} catch { - log('Linting failed'); - process.exit(1); -} +await runMultipleTasks( + packages.flatMap((pkg) => [ + { + command: join(basedir, pkg, 'node_modules', '.bin', 'tsc'), + args: tscArgs, + cwd: join(basedir, pkg), + outputMode: 'fail_atomic', + successMessage: `Lint ${pkg} (tsc) passed`, + failureMessage: `Lint ${pkg} (tsc) failed`, + }, + { + command: join(basedir, pkg, 'node_modules', '.bin', 'prettier'), + args: prettierArgs, + cwd: join(basedir, pkg), + outputMode: 'fail_atomic', + successMessage: `Lint ${pkg} (prettier) passed`, + failureMessage: `Lint ${pkg} (prettier) failed`, + }, + ]), + { parallel: true }, +); log('Linting successful'); diff --git a/scripts/start.mjs b/scripts/start.mjs index b92ac8f..e3069b9 100755 --- a/scripts/start.mjs +++ b/scripts/start.mjs @@ -2,7 +2,7 @@ import { join } from 'node:path'; import { basedir, log } from './helpers/io.mjs'; -import { runTaskPrefixOutput } from './helpers/proc.mjs'; +import { runMultipleTasks } from './helpers/proc.mjs'; const forceMockSSO = process.argv.slice(2).includes('--mock-sso'); const apiPort = Number.parseInt(process.env['PORT'] ?? '5000'); @@ -36,21 +36,26 @@ if ( } log('Starting application...'); -await Promise.all([ - runTaskPrefixOutput({ - command: 'npm', - args: ['start', '--quiet'], - cwd: join(basedir, 'frontend'), - env: frontendEnv, - outputPrefix: 'frontend', - prefixFormat: '35', - }), - runTaskPrefixOutput({ - command: 'npm', - args: ['start', '--quiet'], - cwd: join(basedir, 'backend'), - env: backendEnv, - outputPrefix: 'backend', - prefixFormat: '36', - }), -]); +await runMultipleTasks( + [ + { + command: 'npm', + args: ['start', '--quiet'], + cwd: join(basedir, 'frontend'), + env: frontendEnv, + failureMessage: 'Failed to run frontend', + outputPrefix: 'frontend', + prefixFormat: '35', + }, + { + command: 'npm', + args: ['start', '--quiet'], + cwd: join(basedir, 'backend'), + env: backendEnv, + failureMessage: 'Failed to run backend', + outputPrefix: 'backend', + prefixFormat: '36', + }, + ], + { parallel: true }, +); diff --git a/scripts/test.mjs b/scripts/test.mjs new file mode 100755 index 0000000..16b9b0f --- /dev/null +++ b/scripts/test.mjs @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +import { join } from 'node:path'; +import { basedir, deleteDirectory, findFiles, log } from './helpers/io.mjs'; +import { stat, mkdir } from 'node:fs/promises'; +import { + exitWithCode, + printPrefixed, + runBackgroundTask, + runMultipleTasks, + runTask, + waitForOutput, +} from './helpers/proc.mjs'; +import { makeRandomAppSecrets } from './helpers/random.mjs'; + +const PARALLEL_E2E = (process.env['PARALLEL_E2E'] ?? 'true') === 'true'; +const FOCUS_BROWSER = process.env['BROWSER']; +const SKIP_UNIT = process.argv.slice(2).includes('--only-e2e'); + +const units = ['frontend', 'backend']; + +if (!SKIP_UNIT) { + await runMultipleTasks( + units.map((pkg) => ({ + command: 'npm', + args: ['test', '--quiet'], + cwd: join(basedir, pkg), + beginMessage: `\nTesting ${pkg}...\n`, + failureMessage: `Unit tests failed: ${pkg}.`, + })), + { parallel: false }, + ); +} + +log('\nRunning end-to-end tests...'); + +const browsers = [ + { browser: 'chrome', format: '32' }, + { browser: 'firefox', format: '33' }, +]; + +const filteredBrowsers = FOCUS_BROWSER + ? browsers.filter(({ browser }) => browser === FOCUS_BROWSER) + : browsers; + +if (!filteredBrowsers.length) { + await exitWithCode( + 1, + `No end-to-end tests to run: ${FOCUS_BROWSER} is not available.`, + ); +} + +const builddir = join(basedir, 'build'); +const e2eworkdir = join(basedir, 'e2e', 'build'); +const downloads = join(e2eworkdir, 'downloads'); + +await deleteDirectory(e2eworkdir); +await mkdir(e2eworkdir, { recursive: true }); +await mkdir(downloads, { recursive: true }); + +const testEnv = { ...process.env }; + +let appLogs; +if (!testEnv['TARGET_HOST']) { + const port = Number.parseInt(process.env['PORT'] ?? '5010'); + + log('Building application...'); + await runTask({ + command: join(basedir, 'scripts', 'build.mjs'), + args: ['--keep-deps'], + }); + + if (!(await stat(join(builddir, 'node_modules')).catch(() => null))) { + log('Installing production dependencies...'); + await runTask({ + command: 'npm', + args: ['install', '--omit=dev', '--quiet'], + cwd: builddir, + }); + } + + log('Using mock authentication provider'); + const mockSSOPort = port + 2; + const mockSSOHost = `http://localhost:${mockSSOPort}`; + + const appEnv = { + ...process.env, + PORT: String(port), + SERVER_BIND_ADDRESS: 'localhost', + DB_URL: 'memory://refacto?simulatedLatency=50', + MOCK_SSO_PORT: mockSSOPort, + SSO_GOOGLE_CLIENT_ID: 'mock-client-id', + SSO_GOOGLE_AUTH_URL: `${mockSSOHost}/auth`, + SSO_GOOGLE_TOKEN_INFO_URL: `${mockSSOHost}/tokeninfo`, + // TODO: uncomment once https://github.com/nodejs/node/issues/50452 is resolved + //NODE_OPTIONS: `--experimental-policy=${JSON.stringify( + // join(builddir, 'policy.json'), + //)}`, + }; + + log('Using randomised secrets'); + const secrets = makeRandomAppSecrets(); + for (const [env, value] of secrets) { + process.stderr.write(`${env}=${value}\n`); + appEnv[env] = value; + } + + log('Starting application'); + const begin = Date.now(); + const appProc = runBackgroundTask({ + command: 'node', + args: [ + '--disable-proto=throw', + // TODO replace express with something else to be able to add this + //'--disallow-code-generation-from-strings', + join(builddir, 'index.js'), + ], + env: appEnv, + stdio: ['ignore', 'ignore', 'pipe'], + }); + + // Wait for startup + appLogs = waitForOutput(appProc.stdio[2], 'Available at', 30 * 1000); + try { + await appLogs.promise; + log(`Application started in ${((Date.now() - begin) * 0.001).toFixed(3)}s`); + } catch (e) { + log(`Application logs:`); + printPrefixed(process.stderr, appLogs.getOutput(), 'log'); + await exitWithCode( + 1, + `Failed to start server for E2E tests: ${e} (exit code: ${appProc.exitCode})`, + ); + } + + testEnv['TARGET_HOST'] = `http://localhost:${port}/`; +} + +// Run tests + +try { + await runMultipleTasks( + filteredBrowsers.map(({ browser, format }) => ({ + command: 'npm', + args: ['test', '--quiet'], + cwd: join(basedir, 'e2e'), + env: { ...testEnv, SELENIUM_BROWSER: browser }, + beginMessage: `E2E testing in ${browser}...`, + outputPrefix: browser, + prefixFormat: format, + exitOnFailure: false, + })), + { parallel: PARALLEL_E2E }, + ); +} catch { + if (appLogs) { + log(`\nApplication logs:`); + printPrefixed(process.stderr, appLogs.getOutput(), 'log'); + } + log('\nFiles downloaded:'); + log((await findFiles(downloads)).join('\n')); + await exitWithCode(1, 'End-to-end tests failed.'); +} + +await exitWithCode(0, 'Testing complete: pass.'); // ensure app is closed diff --git a/scripts/test.sh b/scripts/test.sh deleted file mode 100755 index c337dff..0000000 --- a/scripts/test.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -set -e; - -BASEDIR="$(dirname "$0")/.."; - -echo; -echo 'Testing frontend...'; -npm --prefix="$BASEDIR/frontend" test --quiet; - -echo; -echo 'Testing backend...'; -npm --prefix="$BASEDIR/backend" test --quiet; - -echo; -echo 'Running end-to-end tests...'; -PARALLEL_E2E="${PARALLEL_E2E:-true}" \ -"$BASEDIR/scripts/e2e.sh"; - -echo 'Testing complete: pass.';