From b8d48e019e336a25b4814e4f2f838c917d6c3fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 29 Nov 2024 05:25:08 -0800 Subject: [PATCH 1/3] Extract some logic from runner to utils module (#48014) Summary: Changelog: [internal] Just a small refactor in preparation for a following change that will add more usages for these utilities. It also cleans up the runner file which is good too. Differential Revision: D66595405 --- jest/integration/runner/runner.js | 112 ++++++------------------------ jest/integration/runner/utils.js | 88 +++++++++++++++++++++++ 2 files changed, 111 insertions(+), 89 deletions(-) create mode 100644 jest/integration/runner/utils.js diff --git a/jest/integration/runner/runner.js b/jest/integration/runner/runner.js index 03132b507e68..ea02e154cd3d 100644 --- a/jest/integration/runner/runner.js +++ b/jest/integration/runner/runner.js @@ -12,27 +12,28 @@ import type {TestSuiteResult} from '../runtime/setup'; import entrypointTemplate from './entrypoint-template'; -import {spawnSync} from 'child_process'; -import crypto from 'crypto'; +import { + getBuckModeForPlatform, + 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 @@ -50,7 +51,7 @@ function parseRNTesterCommandResult( throw new Error( [ 'Failed to parse test results from RN tester binary result. Full output:', - 'buck2 ' + commandArgs.join(' '), + result.originalCommand, 'stdout:', stdout, 'stderr:', @@ -62,37 +63,18 @@ function parseRNTesterCommandResult( 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,25 +84,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(' '), + hermesCompilerCommandResult.originalCommand, 'stdout:', hermesCompilerCommandResult.stdout, 'stderr:', @@ -132,35 +102,6 @@ function generateBytecodeBundle({ } } -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,30 +154,24 @@ 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(' '), + rnTesterCommandResult.originalCommand, 'stdout:', rnTesterCommandResult.stdout, 'stderr:', @@ -251,7 +186,7 @@ module.exports = async function runTest( console.log( [ 'RN tester binary. Full output:', - 'buck2 ' + rnTesterCommandArgs.join(' '), + rnTesterCommandResult.originalCommand, 'stdout:', rnTesterCommandResult.stdout, 'stderr:', @@ -263,7 +198,6 @@ module.exports = async function runTest( } 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..e1d0a1799fc9 --- /dev/null +++ b/jest/integration/runner/utils.js @@ -0,0 +1,88 @@ +/** + * 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 Buck2SpawnResult = { + ...ReturnType, + originalCommand: string, + ... +}; + +export function runBuck2(args: Array): Buck2SpawnResult { + 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 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'); +} From 018685b69705d55ef7bd67af57681be49a13c8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 29 Nov 2024 05:25:08 -0800 Subject: [PATCH 2/3] Implement warm up step to remove costly builds from individual test running time (#48015) Summary: Changelog: [internal] Right now, when we run individual Fantom tests, we compile Hermes and the RN Tester CLI as part of the test, which causes the first test to run to be very slow and the remaining tests in the same run to be very fast. This is misleading because it makes it look like the test itself is slow, when it's actually paying a price for everyone. Fortunately, Jest has an option to do a global setup before any tests in the project run (and it doesn't run if none of the tests in the project run, in multi-project setups), so we can use it to do the necessary warmup so it doesn't end up being attributed to individual tests. Differential Revision: D66595406 --- jest/integration/config/jest.config.js | 1 + jest/integration/runner/warmup/index.js | 13 +++ jest/integration/runner/warmup/warmup.js | 113 +++++++++++++++++++ jest/integration/runtime/WarmUpEntryPoint.js | 18 +++ 4 files changed, 145 insertions(+) create mode 100644 jest/integration/runner/warmup/index.js create mode 100644 jest/integration/runner/warmup/warmup.js create mode 100644 jest/integration/runtime/WarmUpEntryPoint.js 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/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..9836209b84a0 --- /dev/null +++ b/jest/integration/runner/warmup/warmup.js @@ -0,0 +1,113 @@ +/** + * 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, 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( + [ + 'Failed to build Hermes compiler. Full output:', + buildHermesCompilerCommandResult.originalCommand, + 'stdout:', + buildHermesCompilerCommandResult.stdout, + 'stderr:', + buildHermesCompilerCommandResult.stderr, + 'error:', + buildHermesCompilerCommandResult.error, + ].join('\n'), + ); + } +} + +function warmUpRNTesterCLI(): void { + const buildRNTesterCommandResult = runBuck2([ + 'build', + getBuckModeForPlatform(), + '//xplat/ReactNative/react-native-cxx/samples/tester:tester', + ]); + + if (buildRNTesterCommandResult.status !== 0) { + throw new Error( + [ + 'Failed to build RN tester binary. Full output:', + buildRNTesterCommandResult.originalCommand, + 'stdout:', + buildRNTesterCommandResult.stdout, + 'stderr:', + buildRNTesterCommandResult.stderr, + 'error:', + buildRNTesterCommandResult.error, + ].join('\n'), + ); + } +} 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'; From e4b7a5da1297d8ca374d25c046c6a7a734f63547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 29 Nov 2024 05:25:08 -0800 Subject: [PATCH 3/3] Extract logic to debug command errors to shared function Differential Revision: D66596730 --- jest/integration/runner/runner.js | 50 +++--------------------- jest/integration/runner/utils.js | 24 +++++++++++- jest/integration/runner/warmup/warmup.js | 30 ++++---------- 3 files changed, 35 insertions(+), 69 deletions(-) diff --git a/jest/integration/runner/runner.js b/jest/integration/runner/runner.js index ea02e154cd3d..b8da6171a26f 100644 --- a/jest/integration/runner/runner.js +++ b/jest/integration/runner/runner.js @@ -14,6 +14,7 @@ import type {TestSuiteResult} from '../runtime/setup'; import entrypointTemplate from './entrypoint-template'; import { getBuckModeForPlatform, + getDebugInfoFromCommandResult, getShortHash, runBuck2, symbolicateStackTrace, @@ -49,14 +50,8 @@ function parseRNTesterCommandResult(result: ReturnType): { testResult = JSON.parse(nullthrows(testResultJSON)); } catch (error) { throw new Error( - [ - 'Failed to parse test results from RN tester binary result. Full output:', - result.originalCommand, - 'stdout:', - stdout, - 'stderr:', - result.stderr.toString(), - ].join('\n'), + 'Failed to parse test results from RN tester binary result.\n' + + getDebugInfoFromCommandResult(result), ); } @@ -87,18 +82,7 @@ function generateBytecodeBundle({ ]); if (hermesCompilerCommandResult.status !== 0) { - throw new Error( - [ - 'Failed to run Hermes compiler. Full output:', - hermesCompilerCommandResult.originalCommand, - 'stdout:', - hermesCompilerCommandResult.stdout, - 'stderr:', - hermesCompilerCommandResult.stderr, - 'error:', - hermesCompilerCommandResult.error, - ].join('\n'), - ); + throw new Error(getDebugInfoFromCommandResult(hermesCompilerCommandResult)); } } @@ -168,33 +152,11 @@ module.exports = async function runTest( ]); if (rnTesterCommandResult.status !== 0) { - throw new Error( - [ - 'Failed to run test in RN tester binary. Full output:', - rnTesterCommandResult.originalCommand, - '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:', - rnTesterCommandResult.originalCommand, - 'stdout:', - rnTesterCommandResult.stdout, - 'stderr:', - rnTesterCommandResult.stderr, - 'error:', - rnTesterCommandResult.error, - ].join('\n'), - ); + console.log(getDebugInfoFromCommandResult(rnTesterCommandResult)); } const rnTesterParsedOutput = parseRNTesterCommandResult( diff --git a/jest/integration/runner/utils.js b/jest/integration/runner/utils.js index e1d0a1799fc9..3671f636afba 100644 --- a/jest/integration/runner/utils.js +++ b/jest/integration/runner/utils.js @@ -33,13 +33,13 @@ export function getBuckModeForPlatform(enableRelease: boolean = false): string { } } -type Buck2SpawnResult = { +type SpawnResultWithOriginalCommand = { ...ReturnType, originalCommand: string, ... }; -export function runBuck2(args: Array): Buck2SpawnResult { +export function runBuck2(args: Array): SpawnResultWithOriginalCommand { const result = spawnSync('buck2', args, { encoding: 'utf8', env: { @@ -54,6 +54,26 @@ export function runBuck2(args: Array): Buck2SpawnResult { }; } +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); } diff --git a/jest/integration/runner/warmup/warmup.js b/jest/integration/runner/warmup/warmup.js index 9836209b84a0..e78dc321127c 100644 --- a/jest/integration/runner/warmup/warmup.js +++ b/jest/integration/runner/warmup/warmup.js @@ -9,7 +9,11 @@ * @oncall react_native */ -import {getBuckModeForPlatform, runBuck2} from '../utils'; +import { + getBuckModeForPlatform, + getDebugInfoFromCommandResult, + runBuck2, +} from '../utils'; // $FlowExpectedError[untyped-import] import fs from 'fs'; import Metro from 'metro'; @@ -75,16 +79,7 @@ function warmUpHermesCompiler(): void { if (buildHermesCompilerCommandResult.status !== 0) { throw new Error( - [ - 'Failed to build Hermes compiler. Full output:', - buildHermesCompilerCommandResult.originalCommand, - 'stdout:', - buildHermesCompilerCommandResult.stdout, - 'stderr:', - buildHermesCompilerCommandResult.stderr, - 'error:', - buildHermesCompilerCommandResult.error, - ].join('\n'), + getDebugInfoFromCommandResult(buildHermesCompilerCommandResult), ); } } @@ -97,17 +92,6 @@ function warmUpRNTesterCLI(): void { ]); if (buildRNTesterCommandResult.status !== 0) { - throw new Error( - [ - 'Failed to build RN tester binary. Full output:', - buildRNTesterCommandResult.originalCommand, - 'stdout:', - buildRNTesterCommandResult.stdout, - 'stderr:', - buildRNTesterCommandResult.stderr, - 'error:', - buildRNTesterCommandResult.error, - ].join('\n'), - ); + throw new Error(getDebugInfoFromCommandResult(buildRNTesterCommandResult)); } }