diff --git a/jest/integration/config/jest.config.js b/jest/integration/config/jest.config.js index b00053063fe7..cd047bfaae41 100644 --- a/jest/integration/config/jest.config.js +++ b/jest/integration/config/jest.config.js @@ -25,4 +25,5 @@ module.exports = { transformIgnorePatterns: ['.*'], testRunner: './jest/integration/runner/index.js', watchPathIgnorePatterns: ['/jest/integration/build/'], + globalSetup: './jest/integration/runner/warmup/index.js', }; diff --git a/jest/integration/runner/runner.js b/jest/integration/runner/runner.js index 03132b507e68..b8da6171a26f 100644 --- a/jest/integration/runner/runner.js +++ b/jest/integration/runner/runner.js @@ -12,27 +12,29 @@ import type {TestSuiteResult} from '../runtime/setup'; import entrypointTemplate from './entrypoint-template'; -import {spawnSync} from 'child_process'; -import crypto from 'crypto'; +import { + getBuckModeForPlatform, + getDebugInfoFromCommandResult, + getShortHash, + runBuck2, + symbolicateStackTrace, +} from './utils'; import fs from 'fs'; // $FlowExpectedError[untyped-import] import {formatResultsErrors} from 'jest-message-util'; import Metro from 'metro'; import nullthrows from 'nullthrows'; -import os from 'os'; import path from 'path'; -// $FlowExpectedError[untyped-import] -import {SourceMapConsumer} from 'source-map'; const BUILD_OUTPUT_PATH = path.resolve(__dirname, '..', 'build'); const ENABLE_OPTIMIZED_MODE: false = false; const PRINT_FANTOM_OUTPUT: false = false; -function parseRNTesterCommandResult( - commandArgs: $ReadOnlyArray, - result: ReturnType, -): {logs: string, testResult: TestSuiteResult} { +function parseRNTesterCommandResult(result: ReturnType): { + logs: string, + testResult: TestSuiteResult, +} { const stdout = result.stdout.toString(); const outputArray = stdout @@ -48,51 +50,26 @@ function parseRNTesterCommandResult( testResult = JSON.parse(nullthrows(testResultJSON)); } catch (error) { throw new Error( - [ - 'Failed to parse test results from RN tester binary result. Full output:', - 'buck2 ' + commandArgs.join(' '), - 'stdout:', - stdout, - 'stderr:', - result.stderr.toString(), - ].join('\n'), + 'Failed to parse test results from RN tester binary result.\n' + + getDebugInfoFromCommandResult(result), ); } return {logs: outputArray.join('\n'), testResult}; } -function getBuckModeForPlatform() { - const mode = ENABLE_OPTIMIZED_MODE ? 'opt' : 'dev'; - - switch (os.platform()) { - case 'linux': - return `@//arvr/mode/linux/${mode}`; - case 'darwin': - return os.arch() === 'arm64' - ? `@//arvr/mode/mac-arm/${mode}` - : `@//arvr/mode/mac/${mode}`; - case 'win32': - return `@//arvr/mode/win/${mode}`; - default: - throw new Error(`Unsupported platform: ${os.platform()}`); - } -} - -function getShortHash(contents: string): string { - return crypto.createHash('md5').update(contents).digest('hex').slice(0, 8); -} - function generateBytecodeBundle({ sourcePath, bytecodePath, + isOptimizedMode, }: { sourcePath: string, bytecodePath: string, + isOptimizedMode: boolean, }): void { - const hermesCompilerCommandArgs = [ + const hermesCompilerCommandResult = runBuck2([ 'run', - getBuckModeForPlatform(), + getBuckModeForPlatform(isOptimizedMode), '//xplat/hermes/tools/hermesc:hermesc', '--', '-emit-binary', @@ -102,65 +79,13 @@ function generateBytecodeBundle({ '-out', bytecodePath, sourcePath, - ]; - - const hermesCompilerCommandResult = spawnSync( - 'buck2', - hermesCompilerCommandArgs, - { - encoding: 'utf8', - env: { - ...process.env, - PATH: `/usr/local/bin:${process.env.PATH ?? ''}`, - }, - }, - ); + ]); if (hermesCompilerCommandResult.status !== 0) { - throw new Error( - [ - 'Failed to run Hermes compiler. Full output:', - 'buck2 ' + hermesCompilerCommandArgs.join(' '), - 'stdout:', - hermesCompilerCommandResult.stdout, - 'stderr:', - hermesCompilerCommandResult.stderr, - 'error:', - hermesCompilerCommandResult.error, - ].join('\n'), - ); + throw new Error(getDebugInfoFromCommandResult(hermesCompilerCommandResult)); } } -function symbolicateStackTrace( - sourceMapPath: string, - stackTrace: string, -): string { - const sourceMapData = JSON.parse(fs.readFileSync(sourceMapPath, 'utf8')); - const consumer = new SourceMapConsumer(sourceMapData); - - return stackTrace - .split('\n') - .map(line => { - const match = line.match(/at (.*) \((.*):(\d+):(\d+)\)/); - if (match) { - const functionName = match[1]; - // const fileName = match[2]; - const lineNumber = parseInt(match[3], 10); - const columnNumber = parseInt(match[4], 10); - // Get the original position - const originalPosition = consumer.originalPositionFor({ - line: lineNumber, - column: columnNumber, - }); - return `at ${originalPosition.name ?? functionName} (${originalPosition.source}:${originalPosition.line}:${originalPosition.column})`; - } else { - return line; - } - }) - .join('\n'); -} - module.exports = async function runTest( globalConfig: {...}, config: {...}, @@ -213,57 +138,28 @@ module.exports = async function runTest( generateBytecodeBundle({ sourcePath: testJSBundlePath, bytecodePath: testBytecodeBundlePath, + isOptimizedMode, }); } - const rnTesterCommandArgs = [ + const rnTesterCommandResult = runBuck2([ 'run', - getBuckModeForPlatform(), + getBuckModeForPlatform(isOptimizedMode), '//xplat/ReactNative/react-native-cxx/samples/tester:tester', '--', '--bundlePath', testBundlePath, - ]; - const rnTesterCommandResult = spawnSync('buck2', rnTesterCommandArgs, { - encoding: 'utf8', - env: { - ...process.env, - PATH: `/usr/local/bin:${process.env.PATH ?? ''}`, - }, - }); + ]); if (rnTesterCommandResult.status !== 0) { - throw new Error( - [ - 'Failed to run test in RN tester binary. Full output:', - 'buck2 ' + rnTesterCommandArgs.join(' '), - 'stdout:', - rnTesterCommandResult.stdout, - 'stderr:', - rnTesterCommandResult.stderr, - 'error:', - rnTesterCommandResult.error, - ].join('\n'), - ); + throw new Error(getDebugInfoFromCommandResult(rnTesterCommandResult)); } if (PRINT_FANTOM_OUTPUT) { - console.log( - [ - 'RN tester binary. Full output:', - 'buck2 ' + rnTesterCommandArgs.join(' '), - 'stdout:', - rnTesterCommandResult.stdout, - 'stderr:', - rnTesterCommandResult.stderr, - 'error:', - rnTesterCommandResult.error, - ].join('\n'), - ); + console.log(getDebugInfoFromCommandResult(rnTesterCommandResult)); } const rnTesterParsedOutput = parseRNTesterCommandResult( - rnTesterCommandArgs, rnTesterCommandResult, ); diff --git a/jest/integration/runner/utils.js b/jest/integration/runner/utils.js new file mode 100644 index 000000000000..3671f636afba --- /dev/null +++ b/jest/integration/runner/utils.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import {spawnSync} from 'child_process'; +import crypto from 'crypto'; +import fs from 'fs'; +import os from 'os'; +// $FlowExpectedError[untyped-import] +import {SourceMapConsumer} from 'source-map'; + +export function getBuckModeForPlatform(enableRelease: boolean = false): string { + const mode = enableRelease ? 'opt' : 'dev'; + + switch (os.platform()) { + case 'linux': + return `@//arvr/mode/linux/${mode}`; + case 'darwin': + return os.arch() === 'arm64' + ? `@//arvr/mode/mac-arm/${mode}` + : `@//arvr/mode/mac/${mode}`; + case 'win32': + return `@//arvr/mode/win/${mode}`; + default: + throw new Error(`Unsupported platform: ${os.platform()}`); + } +} + +type SpawnResultWithOriginalCommand = { + ...ReturnType, + originalCommand: string, + ... +}; + +export function runBuck2(args: Array): SpawnResultWithOriginalCommand { + const result = spawnSync('buck2', args, { + encoding: 'utf8', + env: { + ...process.env, + PATH: `/usr/local/bin:${process.env.PATH ?? ''}`, + }, + }); + + return { + ...result, + originalCommand: `buck2 ${args.join(' ')}`, + }; +} + +export function getDebugInfoFromCommandResult( + commandResult: SpawnResultWithOriginalCommand, +): string { + const logLines = [ + `Command ${commandResult.status === 0 ? 'succeeded' : 'failed'}: ${commandResult.originalCommand}`, + '', + 'stdout:', + commandResult.stdout, + '', + 'stderr:', + commandResult.stderr, + ]; + + if (commandResult.error) { + logLines.push('', 'error:', String(commandResult.error)); + } + + return logLines.join('\n'); +} + +export function getShortHash(contents: string): string { + return crypto.createHash('md5').update(contents).digest('hex').slice(0, 8); +} + +export function symbolicateStackTrace( + sourceMapPath: string, + stackTrace: string, +): string { + const sourceMapData = JSON.parse(fs.readFileSync(sourceMapPath, 'utf8')); + const consumer = new SourceMapConsumer(sourceMapData); + + return stackTrace + .split('\n') + .map(line => { + const match = line.match(/at (.*) \((.*):(\d+):(\d+)\)/); + if (match) { + const functionName = match[1]; + // const fileName = match[2]; + const lineNumber = parseInt(match[3], 10); + const columnNumber = parseInt(match[4], 10); + // Get the original position + const originalPosition = consumer.originalPositionFor({ + line: lineNumber, + column: columnNumber, + }); + return `at ${originalPosition.name ?? functionName} (${originalPosition.source}:${originalPosition.line}:${originalPosition.column})`; + } else { + return line; + } + }) + .join('\n'); +} diff --git a/jest/integration/runner/warmup/index.js b/jest/integration/runner/warmup/index.js new file mode 100644 index 000000000000..1e3f3ef54bab --- /dev/null +++ b/jest/integration/runner/warmup/index.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +require('../../../../scripts/build/babel-register').registerForMonorepo(); + +module.exports = require('./warmup'); diff --git a/jest/integration/runner/warmup/warmup.js b/jest/integration/runner/warmup/warmup.js new file mode 100644 index 000000000000..e78dc321127c --- /dev/null +++ b/jest/integration/runner/warmup/warmup.js @@ -0,0 +1,97 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import { + getBuckModeForPlatform, + getDebugInfoFromCommandResult, + runBuck2, +} from '../utils'; +// $FlowExpectedError[untyped-import] +import fs from 'fs'; +import Metro from 'metro'; +import os from 'os'; +import path from 'path'; + +export default async function warmUp( + globalConfig: {...}, + projectConfig: {...}, +): Promise { + try { + warmUpHermesCompiler(); + warmUpRNTesterCLI(); + await warmUpMetro(); + } catch (e) { + // Sandcastle fails to parse the test output if we log stuff to stdout/stderr. + if ((process.env.SANDCASTLE ?? '') !== '') { + console.error( + 'Global warmup failed. Tests will continue to run but will likely fail. Details:\n', + e, + ); + } + } +} + +async function warmUpMetro(): Promise { + const metroConfig = await Metro.loadConfig({ + config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'), + }); + + const entrypointPath = path.resolve( + __dirname, + '..', + '..', + 'runtime', + 'WarmUpEntryPoint.js', + ); + + const bundlePath = path.join( + os.tmpdir(), + `fantom-warmup-bundle-${Date.now()}.js`, + ); + + await Metro.runBuild(metroConfig, { + entry: entrypointPath, + out: bundlePath, + platform: 'android', + minify: false, + dev: true, + }); + + try { + fs.unlinkSync(bundlePath); + } catch {} +} + +function warmUpHermesCompiler(): void { + const buildHermesCompilerCommandResult = runBuck2([ + 'build', + getBuckModeForPlatform(), + '//xplat/hermes/tools/hermesc:hermesc', + ]); + + if (buildHermesCompilerCommandResult.status !== 0) { + throw new Error( + getDebugInfoFromCommandResult(buildHermesCompilerCommandResult), + ); + } +} + +function warmUpRNTesterCLI(): void { + const buildRNTesterCommandResult = runBuck2([ + 'build', + getBuckModeForPlatform(), + '//xplat/ReactNative/react-native-cxx/samples/tester:tester', + ]); + + if (buildRNTesterCommandResult.status !== 0) { + throw new Error(getDebugInfoFromCommandResult(buildRNTesterCommandResult)); + } +} diff --git a/jest/integration/runtime/WarmUpEntryPoint.js b/jest/integration/runtime/WarmUpEntryPoint.js new file mode 100644 index 000000000000..b2848107a29f --- /dev/null +++ b/jest/integration/runtime/WarmUpEntryPoint.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +/** + * This is just an entrypoint to warm up the Metro cache before the tests run. + */ + +import '../../../packages/react-native/Libraries/Core/InitializeCore.js'; +import '../../../packages/react-native/src/private/__tests__/ReactNativeTester'; +import './setup';