From 973409fc0bbc53fdc2de516ddefffbc9c6884270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 01/10] Follow naming convention for NativeFantom module Differential Revision: D67549203 --- .../src/getFantomRenderedOutput.js | 4 ++-- packages/react-native-fantom/src/index.js | 12 ++++++------ .../specs/{NativeFantomModule.js => NativeFantom.js} | 4 +++- 3 files changed, 11 insertions(+), 9 deletions(-) rename packages/react-native-fantom/src/specs/{NativeFantomModule.js => NativeFantom.js} (90%) diff --git a/packages/react-native-fantom/src/getFantomRenderedOutput.js b/packages/react-native-fantom/src/getFantomRenderedOutput.js index 496a462a3d32..45500fde1742 100644 --- a/packages/react-native-fantom/src/getFantomRenderedOutput.js +++ b/packages/react-native-fantom/src/getFantomRenderedOutput.js @@ -9,7 +9,7 @@ * @oncall react_native */ -import FantomModule from './specs/NativeFantomModule'; +import NativeFantom from './specs/NativeFantom'; // $FlowExpectedError[untyped-import] import micromatch from 'micromatch'; import * as React from 'react'; @@ -114,7 +114,7 @@ export default function getFantomRenderedOutput( } = config; return new FantomRenderedOutput( JSON.parse( - FantomModule.getRenderedOutput(surfaceId, { + NativeFantom.getRenderedOutput(surfaceId, { includeRoot, includeLayoutMetrics, }), diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index b765810f66a0..7b06e4e9a655 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -15,7 +15,7 @@ import type { import type {MixedElement} from 'react'; import getFantomRenderedOutput from './getFantomRenderedOutput'; -import FantomModule from './specs/NativeFantomModule'; +import NativeFantom from './specs/NativeFantom'; import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric'; let globalSurfaceIdCounter = 1; @@ -35,7 +35,7 @@ class Root { render(element: MixedElement) { if (!this.#hasRendered) { - FantomModule.startSurface(this.#surfaceId); + NativeFantom.startSurface(this.#surfaceId); this.#hasRendered = true; } @@ -43,13 +43,13 @@ class Root { } getMountingLogs(): Array { - return FantomModule.getMountingManagerLogs(this.#surfaceId); + return NativeFantom.getMountingManagerLogs(this.#surfaceId); } destroy() { // TODO: check for leaks. - FantomModule.stopSurface(this.#surfaceId); - FantomModule.flushMessageQueue(); + NativeFantom.stopSurface(this.#surfaceId); + NativeFantom.flushMessageQueue(); } getRenderedOutput(config: RenderOutputConfig = {}): FantomRenderedOutput { @@ -100,7 +100,7 @@ export function runWorkLoop(): void { try { flushingQueue = true; - FantomModule.flushMessageQueue(); + NativeFantom.flushMessageQueue(); } finally { flushingQueue = false; } diff --git a/packages/react-native-fantom/src/specs/NativeFantomModule.js b/packages/react-native-fantom/src/specs/NativeFantom.js similarity index 90% rename from packages/react-native-fantom/src/specs/NativeFantomModule.js rename to packages/react-native-fantom/src/specs/NativeFantom.js index da67f38a1e75..a2d2db9fc31c 100644 --- a/packages/react-native-fantom/src/specs/NativeFantomModule.js +++ b/packages/react-native-fantom/src/specs/NativeFantom.js @@ -26,4 +26,6 @@ interface Spec extends TurboModule { getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string; } -export default TurboModuleRegistry.getEnforcing('Fantom') as Spec; +export default TurboModuleRegistry.getEnforcing( + 'NativeFantomCxx', +) as Spec; From 381291938956cb0fe264bc1adf2bdbf374bc904c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 02/10] Use codegen for Fantom native module Differential Revision: D67759729 --- packages/react-native-fantom/src/getFantomRenderedOutput.js | 2 +- packages/react-native-fantom/src/index.js | 2 +- .../src/private/specs/modules}/NativeFantom.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/{react-native-fantom/src/specs => react-native/src/private/specs/modules}/NativeFantom.js (79%) diff --git a/packages/react-native-fantom/src/getFantomRenderedOutput.js b/packages/react-native-fantom/src/getFantomRenderedOutput.js index 45500fde1742..2e7f36cc9a3d 100644 --- a/packages/react-native-fantom/src/getFantomRenderedOutput.js +++ b/packages/react-native-fantom/src/getFantomRenderedOutput.js @@ -9,10 +9,10 @@ * @oncall react_native */ -import NativeFantom from './specs/NativeFantom'; // $FlowExpectedError[untyped-import] import micromatch from 'micromatch'; import * as React from 'react'; +import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom'; export type RenderOutputConfig = { ...FantomRenderedOutputConfig, diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 7b06e4e9a655..67795cc5c67f 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -15,8 +15,8 @@ import type { import type {MixedElement} from 'react'; import getFantomRenderedOutput from './getFantomRenderedOutput'; -import NativeFantom from './specs/NativeFantom'; import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric'; +import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom'; let globalSurfaceIdCounter = 1; diff --git a/packages/react-native-fantom/src/specs/NativeFantom.js b/packages/react-native/src/private/specs/modules/NativeFantom.js similarity index 79% rename from packages/react-native-fantom/src/specs/NativeFantom.js rename to packages/react-native/src/private/specs/modules/NativeFantom.js index a2d2db9fc31c..34c04f2eaebf 100644 --- a/packages/react-native-fantom/src/specs/NativeFantom.js +++ b/packages/react-native/src/private/specs/modules/NativeFantom.js @@ -4,13 +4,13 @@ * 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 + * @flow strict * @format */ -import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; -import {TurboModuleRegistry} from 'react-native'; +import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; // match RenderFormatOptions.h export type RenderFormatOptions = { From c06b7de7e74884eec44694dcbf1dbb819da03f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 03/10] Remove existing logs and warnings from tests Differential Revision: D67602299 --- .../src/__tests__/Fantom-itest.js | 68 ++++++++++++------- .../src/getFantomRenderedOutput.js | 11 ++- .../__tests__/IntersectionObserver-itest.js | 32 +++------ .../__tests__/MutationObserver-itest.js | 16 ++--- 4 files changed, 69 insertions(+), 58 deletions(-) diff --git a/packages/react-native-fantom/src/__tests__/Fantom-itest.js b/packages/react-native-fantom/src/__tests__/Fantom-itest.js index f16d30bd10fb..250c31d4d828 100644 --- a/packages/react-native-fantom/src/__tests__/Fantom-itest.js +++ b/packages/react-native-fantom/src/__tests__/Fantom-itest.js @@ -76,28 +76,30 @@ describe('Fantom', () => { // TODO: when error handling is fixed, this should verify using `toThrow` it('should throw when running a task inside another task', () => { - let lastCallbackExecuted = 0; + let threw = false; + runTask(() => { - lastCallbackExecuted = 1; - runTask(() => { - lastCallbackExecuted = 2; - throw new Error('Recursive runTask should be unreachable'); - }); + // TODO replace with expect(() => { ... }).toThrow() when error handling is fixed + try { + runTask(() => {}); + } catch { + threw = true; + } }); - expect(lastCallbackExecuted).toBe(1); + expect(threw).toBe(true); + + threw = false; runTask(() => { queueMicrotask(() => { - lastCallbackExecuted = 3; - runTask(() => { - lastCallbackExecuted = 4; - throw new Error( - 'Recursive runTask from micro-task should be unreachable', - ); - }); + try { + runTask(() => {}); + } catch { + threw = true; + } }); }); - expect(lastCallbackExecuted).toBe(3); + expect(threw).toBe(true); }); }); @@ -125,16 +127,24 @@ describe('Fantom', () => { runTask(() => { root.render( <> - - + + , ); }); expect(root.getRenderedOutput().toJSX()).toEqual( <> - - + + , ); @@ -233,18 +243,26 @@ describe('Fantom', () => { runTask(() => { root.render( <> - - hello world! - + + hello world! + , ); }); expect(root.getRenderedOutput({props: []}).toJSX()).toEqual( <> - - hello world! - + + hello world! + , ); diff --git a/packages/react-native-fantom/src/getFantomRenderedOutput.js b/packages/react-native-fantom/src/getFantomRenderedOutput.js index 2e7f36cc9a3d..ede6e38285fb 100644 --- a/packages/react-native-fantom/src/getFantomRenderedOutput.js +++ b/packages/react-native-fantom/src/getFantomRenderedOutput.js @@ -152,16 +152,20 @@ function convertRawJsonToJSX( function createJSXElementForTestComparison( type: string, props: mixed, + key?: ?string, ): React.Node { const Tag = type; - return ; + return ; } function rnTypeToTestType(type: string): string { return `rn-${type.substring(0, 1).toLowerCase() + type.substring(1)}`; } -function jsonChildToJSXChild(jsonChild: FantomJsonObject | string): React.Node { +function jsonChildToJSXChild( + jsonChild: FantomJsonObject | string, + index?: ?number, +): React.Node { if (typeof jsonChild === 'string') { return jsonChild; } else { @@ -172,6 +176,7 @@ function jsonChildToJSXChild(jsonChild: FantomJsonObject | string): React.Node { jsxChildren == null ? jsonChild.props : {...jsonChild.props, children: jsxChildren}, + index != null ? String(index) : undefined, ); } } @@ -184,7 +189,7 @@ function jsonChildrenToJSXChildren(jsonChildren: FantomJsonObject['children']) { let allJSXChildrenAreStrings = true; let jsxChildrenString = ''; for (let i = 0; i < jsonChildren.length; i++) { - const jsxChild = jsonChildToJSXChild(jsonChildren[i]); + const jsxChild = jsonChildToJSXChild(jsonChildren[i], i); jsxChildren.push(jsxChild); if (allJSXChildrenAreStrings) { if (typeof jsxChild === 'string') { diff --git a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js index a7ea8e84e8dd..8ad459de8af1 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js @@ -487,7 +487,6 @@ describe('IntersectionObserver', () => { maybeNode = receivedNode; }} /> - , , ); }); @@ -549,7 +548,6 @@ describe('IntersectionObserver', () => { maybeNode = receivedNode; }} /> - , , ); }); @@ -610,7 +608,6 @@ describe('IntersectionObserver', () => { maybeNode = receivedNode; }} /> - , , ); }); @@ -942,7 +939,6 @@ describe('IntersectionObserver', () => { maybeNode = receivedNode; }} /> - , , ); }); @@ -1003,7 +999,6 @@ describe('IntersectionObserver', () => { maybeNode = receivedNode; }} /> - , , ); }); @@ -1314,13 +1309,9 @@ describe('IntersectionObserver', () => { }); expect(node.isConnected).toBe(false); - Fantom.runTask(() => { - observer = new IntersectionObserver(() => {}); - observer.observe(node); - // TODO what happens if this throws an exception? - observer.unobserve(node); - throw new Error('unobserve should not throw'); - }); + observer = new IntersectionObserver(() => {}); + observer.observe(node); + observer.unobserve(node); }); it('should not report the initial state if the target is unobserved before it is delivered', () => { @@ -1497,19 +1488,16 @@ describe('IntersectionObserver', () => { const node = ensureReactNativeElement(maybeNode); - Fantom.runTask(() => { - observer1 = new IntersectionObserver(() => {}); - observer2 = new IntersectionObserver(() => {}); + observer1 = new IntersectionObserver(() => {}); + observer2 = new IntersectionObserver(() => {}); - observer1.observe(node); - observer2.observe(node); + observer1.observe(node); + observer2.observe(node); - observer1.unobserve(node); + observer1.unobserve(node); - // The second call shouldn't log errors (that would make the test fail). - observer2.unobserve(node); - throw new Error('unobserve should not throw'); - }); + // The second call shouldn't log errors (that would make the test fail). + observer2.unobserve(node); }); }); diff --git a/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js b/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js index 25801da127c9..8163f88a15a5 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js +++ b/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js @@ -383,14 +383,14 @@ describe('MutationObserver', () => { { - maybeObservedNode = ensureReactNativeElement(receivedNode); + maybeObservedNode = receivedNode; }}> , ); }); - const observedNode = nullthrows(maybeObservedNode); + const observedNode = ensureReactNativeElement(maybeObservedNode); const observerCallback = jest.fn(); const observer = new MutationObserver(observerCallback); @@ -968,13 +968,13 @@ describe('MutationObserver', () => { { - maybeObservedNode = ensureReactNativeElement(receivedNode); + maybeObservedNode = receivedNode; }} />, ); }); - const observedNode = nullthrows(maybeObservedNode); + const observedNode = ensureReactNativeElement(maybeObservedNode); const observerCallback = jest.fn(); const observer = new MutationObserver(observerCallback); @@ -1016,13 +1016,13 @@ describe('MutationObserver', () => { { - maybeObservedNode = ensureReactNativeElement(receivedNode); + maybeObservedNode = receivedNode; }} />, ); }); - const observedNode = nullthrows(maybeObservedNode); + const observedNode = ensureReactNativeElement(maybeObservedNode); const observerCallback = jest.fn(); const observer = new MutationObserver(observerCallback); @@ -1048,13 +1048,13 @@ describe('MutationObserver', () => { { - maybeObservedNode = ensureReactNativeElement(receivedNode); + maybeObservedNode = receivedNode; }} />, ); }); - const observedNode = nullthrows(maybeObservedNode); + const observedNode = ensureReactNativeElement(maybeObservedNode); Fantom.runTask(() => { root.render(<>); From 38a3c43412400d2b1fb2e8ed48a5bb866961565b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 04/10] Remove unnecessary filter for AppRegistry logs Differential Revision: D67600610 --- packages/react-native-fantom/runner/runner.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index 776ee43957b3..f321e64ac117 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -47,10 +47,7 @@ function parseRNTesterCommandResult(result: ReturnType): { } { const stdout = result.stdout.toString(); - const outputArray = stdout - .trim() - .split('\n') - .filter(log => !log.startsWith('Running "')); // remove AppRegistry logs. + const outputArray = stdout.trim().split('\n'); // The last line should be the test output in JSON format const testResultJSON = outputArray.pop(); From 4702846794e8c2db11dc592f3f7d6e608d3acbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 05/10] Use structured output for Fantom logs and test results Differential Revision: D67600616 --- packages/react-native-fantom/runner/runner.js | 47 ++++++++++++++----- packages/react-native-fantom/runner/utils.js | 28 +++++++++++ packages/react-native-fantom/runtime/setup.js | 8 +++- .../src/private/specs/modules/NativeFantom.js | 1 + 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index f321e64ac117..74c24e82126f 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -10,6 +10,7 @@ */ import type {TestSuiteResult} from '../runtime/setup'; +import type {ConsoleLogMessage} from './utils'; import entrypointTemplate from './entrypoint-template'; import getFantomTestConfig from './getFantomTestConfig'; @@ -22,6 +23,7 @@ import { getBuckModesForPlatform, getDebugInfoFromCommandResult, getShortHash, + printConsoleLogs, runBuck2, symbolicateStackTrace, } from './utils'; @@ -42,27 +44,50 @@ const BUILD_OUTPUT_PATH = fs.mkdtempSync( const PRINT_FANTOM_OUTPUT: false = false; function parseRNTesterCommandResult(result: ReturnType): { - logs: string, + logs: $ReadOnlyArray, testResult: TestSuiteResult, } { const stdout = result.stdout.toString(); - const outputArray = stdout.trim().split('\n'); + const logs = []; + let testResult; - // The last line should be the test output in JSON format - const testResultJSON = outputArray.pop(); + const lines = stdout + .split('\n') + .map(line => line.trim()) + .filter(Boolean); + + for (const line of lines) { + let parsed; + try { + parsed = JSON.parse(line); + } catch {} + + switch (parsed?.type) { + case 'test-result': + testResult = parsed; + break; + case 'console-log': + logs.push(parsed); + break; + default: + logs.push({ + type: 'console-log', + message: line, + level: 'info', + }); + break; + } + } - let testResult; - try { - testResult = JSON.parse(nullthrows(testResultJSON)); - } catch (error) { + if (testResult == null) { throw new Error( - 'Failed to parse test results from RN tester binary result.\n' + + 'Failed to find test results in RN tester binary output.\n' + getDebugInfoFromCommandResult(result), ); } - return {logs: outputArray.join('\n'), testResult}; + return {logs, testResult}; } function generateBytecodeBundle({ @@ -216,7 +241,7 @@ module.exports = async function runTest( const endTime = Date.now(); if (process.env.SANDCASTLE == null) { - console.log(rnTesterParsedOutput.logs); + printConsoleLogs(rnTesterParsedOutput.logs); } const testResults = diff --git a/packages/react-native-fantom/runner/utils.js b/packages/react-native-fantom/runner/utils.js index d5aa6deeac7a..97a486dcebaf 100644 --- a/packages/react-native-fantom/runner/utils.js +++ b/packages/react-native-fantom/runner/utils.js @@ -134,3 +134,31 @@ export function symbolicateStackTrace( }) .join('\n'); } + +export type ConsoleLogMessage = { + type: 'console-log', + level: 'info' | 'warn' | 'error', + message: string, +}; + +export function printConsoleLogs( + logs: $ReadOnlyArray, +): void { + for (const log of logs) { + switch (log.type) { + case 'console-log': + switch (log.level) { + case 'info': + console.log(log.message); + break; + case 'warn': + console.warn(log.message); + break; + case 'error': + console.error(log.message); + break; + } + break; + } + } +} diff --git a/packages/react-native-fantom/runtime/setup.js b/packages/react-native-fantom/runtime/setup.js index 31055e4a9d4a..740f6e20288e 100644 --- a/packages/react-native-fantom/runtime/setup.js +++ b/packages/react-native-fantom/runtime/setup.js @@ -15,6 +15,7 @@ import expect from './expect'; import {createMockFunction} from './mocks'; import {setupSnapshotConfig, snapshotContext} from './snapshotContext'; import nullthrows from 'nullthrows'; +import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom'; export type TestCaseResult = { ancestorTitles: Array, @@ -190,7 +191,12 @@ function executeTests() { } function reportTestSuiteResult(testSuiteResult: TestSuiteResult): void { - console.log(JSON.stringify(testSuiteResult)); + NativeFantom.reportTestSuiteResultsJSON( + JSON.stringify({ + type: 'test-result', + ...testSuiteResult, + }), + ); } global.$$RunTests$$ = () => { diff --git a/packages/react-native/src/private/specs/modules/NativeFantom.js b/packages/react-native/src/private/specs/modules/NativeFantom.js index 34c04f2eaebf..57e8f7fda028 100644 --- a/packages/react-native/src/private/specs/modules/NativeFantom.js +++ b/packages/react-native/src/private/specs/modules/NativeFantom.js @@ -24,6 +24,7 @@ interface Spec extends TurboModule { getMountingManagerLogs: (surfaceId: number) => Array; flushMessageQueue: () => void; getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string; + reportTestSuiteResultsJSON: (results: string) => void; } export default TurboModuleRegistry.getEnforcing( From 3d630c023edfceb0a2968289a17a6231d37e9f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 06/10] Refactor utility to run buck2 commands as a method to run arbitrary commands Differential Revision: D67600611 --- packages/react-native-fantom/runner/runner.js | 8 ++-- packages/react-native-fantom/runner/utils.js | 37 +++++++++++-------- .../runner/warmup/warmup.js | 6 +-- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index 74c24e82126f..eb46e4653a4c 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -24,7 +24,7 @@ import { getDebugInfoFromCommandResult, getShortHash, printConsoleLogs, - runBuck2, + runBuck2Sync, symbolicateStackTrace, } from './utils'; import fs from 'fs'; @@ -43,7 +43,7 @@ const BUILD_OUTPUT_PATH = fs.mkdtempSync( const PRINT_FANTOM_OUTPUT: false = false; -function parseRNTesterCommandResult(result: ReturnType): { +function parseRNTesterCommandResult(result: ReturnType): { logs: $ReadOnlyArray, testResult: TestSuiteResult, } { @@ -99,7 +99,7 @@ function generateBytecodeBundle({ bytecodePath: string, isOptimizedMode: boolean, }): void { - const hermesCompilerCommandResult = runBuck2( + const hermesCompilerCommandResult = runBuck2Sync( [ 'run', ...getBuckModesForPlatform(isOptimizedMode), @@ -202,7 +202,7 @@ module.exports = async function runTest( }); } - const rnTesterCommandResult = runBuck2([ + const rnTesterCommandResult = runBuck2Sync([ 'run', ...getBuckModesForPlatform( testConfig.mode === FantomTestConfigMode.Optimized, diff --git a/packages/react-native-fantom/runner/utils.js b/packages/react-native-fantom/runner/utils.js index 97a486dcebaf..4e426a10cdae 100644 --- a/packages/react-native-fantom/runner/utils.js +++ b/packages/react-native-fantom/runner/utils.js @@ -44,24 +44,17 @@ export function getBuckModesForPlatform( return ['@//xplat/mode/react-force-cxx-platform', osPlatform]; } -type SpawnResultWithOriginalCommand = { +type SyncCommandResult = { ...ReturnType, originalCommand: string, ... }; -export function runBuck2(args: Array): SpawnResultWithOriginalCommand { - // If these tests are already running from withing a buck2 process, e.g. when - // they are scheduled by a `buck2 test` wrapper, calling `buck2` again would - // cause a daemon-level deadlock. - // To prevent this - explicitly pass custom `--isolation-dir`. Reuse the same - // dir across tests (even running in different jest processes) to properly - // employ caching. - if (process.env.BUCK2_WRAPPER != null) { - args.unshift('--isolation-dir', BUCK_ISOLATION_DIR); - } - - const result = spawnSync('buck2', args, { +export function runCommandSync( + command: string, + args: Array, +): SyncCommandResult { + const result = spawnSync(command, args, { encoding: 'utf8', env: { ...process.env, @@ -71,12 +64,12 @@ export function runBuck2(args: Array): SpawnResultWithOriginalCommand { return { ...result, - originalCommand: `buck2 ${args.join(' ')}`, + originalCommand: `${command} ${args.join(' ')}`, }; } export function getDebugInfoFromCommandResult( - commandResult: SpawnResultWithOriginalCommand, + commandResult: SyncCommandResult, ): string { const maybeSignal = commandResult.signal != null ? `, signal: ${commandResult.signal}` : ''; @@ -102,6 +95,20 @@ export function getDebugInfoFromCommandResult( return logLines.join('\n'); } +export function runBuck2Sync(args: Array): SyncCommandResult { + // If these tests are already running from withing a buck2 process, e.g. when + // they are scheduled by a `buck2 test` wrapper, calling `buck2` again would + // cause a daemon-level deadlock. + // To prevent this - explicitly pass custom `--isolation-dir`. Reuse the same + // dir across tests (even running in different jest processes) to properly + // employ caching. + if (process.env.BUCK2_WRAPPER != null) { + args.unshift('--isolation-dir', BUCK_ISOLATION_DIR); + } + + return runCommandSync('buck2', args); +} + export function getShortHash(contents: string): string { return crypto.createHash('md5').update(contents).digest('hex').slice(0, 8); } diff --git a/packages/react-native-fantom/runner/warmup/warmup.js b/packages/react-native-fantom/runner/warmup/warmup.js index a5503c154ad0..93a918e09446 100644 --- a/packages/react-native-fantom/runner/warmup/warmup.js +++ b/packages/react-native-fantom/runner/warmup/warmup.js @@ -12,7 +12,7 @@ import { getBuckModesForPlatform, getDebugInfoFromCommandResult, - runBuck2, + runBuck2Sync, } from '../utils'; // $FlowExpectedError[untyped-import] import fs from 'fs'; @@ -94,7 +94,7 @@ async function warmUpMetro(isOptimizedMode: boolean): Promise { } function warmUpHermesCompiler(isOptimizedMode: boolean): void { - const buildHermesCompilerCommandResult = runBuck2([ + const buildHermesCompilerCommandResult = runBuck2Sync([ 'build', ...getBuckModesForPlatform(isOptimizedMode), '//xplat/hermes/tools/hermesc:hermesc', @@ -108,7 +108,7 @@ function warmUpHermesCompiler(isOptimizedMode: boolean): void { } function warmUpRNTesterCLI(isOptimizedMode: boolean): void { - const buildRNTesterCommandResult = runBuck2([ + const buildRNTesterCommandResult = runBuck2Sync([ 'build', ...getBuckModesForPlatform(isOptimizedMode), '//xplat/ReactNative/react-native-cxx/samples/tester:tester', From 42475f75b4a1501a29f150d3c1516b22322fcbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 07/10] Add async flavors for runCommand and runBuck2 utilities Differential Revision: D67600614 --- packages/react-native-fantom/runner/runner.js | 4 +- packages/react-native-fantom/runner/utils.js | 76 +++++++++++++++++-- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index eb46e4653a4c..754b5a0f7611 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -10,7 +10,7 @@ */ import type {TestSuiteResult} from '../runtime/setup'; -import type {ConsoleLogMessage} from './utils'; +import type {ConsoleLogMessage, SyncCommandResult} from './utils'; import entrypointTemplate from './entrypoint-template'; import getFantomTestConfig from './getFantomTestConfig'; @@ -43,7 +43,7 @@ const BUILD_OUTPUT_PATH = fs.mkdtempSync( const PRINT_FANTOM_OUTPUT: false = false; -function parseRNTesterCommandResult(result: ReturnType): { +function parseRNTesterCommandResult(result: SyncCommandResult): { logs: $ReadOnlyArray, testResult: TestSuiteResult, } { diff --git a/packages/react-native-fantom/runner/utils.js b/packages/react-native-fantom/runner/utils.js index 4e426a10cdae..794f169c01fb 100644 --- a/packages/react-native-fantom/runner/utils.js +++ b/packages/react-native-fantom/runner/utils.js @@ -9,7 +9,7 @@ * @oncall react_native */ -import {spawnSync} from 'child_process'; +import {spawn, spawnSync} from 'child_process'; import crypto from 'crypto'; import fs from 'fs'; import os from 'os'; @@ -44,12 +44,61 @@ export function getBuckModesForPlatform( return ['@//xplat/mode/react-force-cxx-platform', osPlatform]; } -type SyncCommandResult = { - ...ReturnType, +export type AsyncCommandResult = { originalCommand: string, - ... + childProcess: ReturnType, + done: Promise, + pid: number, + status: ?number, + signal: ?string, + error: ?Error, }; +export type SyncCommandResult = { + originalCommand: string, + pid: number, + status: number, + signal: ?string, + error: ?Error, + stdout: string, + stderr: string, +}; + +export function runCommand( + command: string, + args: Array, +): AsyncCommandResult { + const childProcess = spawn(command, args, { + encoding: 'utf8', + env: { + ...process.env, + PATH: `/usr/local/bin:${process.env.PATH ?? ''}`, + }, + }); + + const result: AsyncCommandResult = { + childProcess, + done: new Promise(resolve => { + childProcess.on('close', (code: number, signal: string) => { + result.status = code; + result.signal = signal; + resolve(result); + }); + }), + originalCommand: `${command} ${args.join(' ')}`, + pid: childProcess.pid, + status: null, + signal: null, + error: null, + }; + + childProcess.on('error', error => { + result.error = error; + }); + + return result; +} + export function runCommandSync( command: string, args: Array, @@ -63,8 +112,13 @@ export function runCommandSync( }); return { - ...result, originalCommand: `${command} ${args.join(' ')}`, + pid: result.pid, + status: result.status, + signal: result.signal, + error: result.error, + stdout: result.stdout.toString(), + stderr: result.stderr.toString(), }; } @@ -95,7 +149,15 @@ export function getDebugInfoFromCommandResult( return logLines.join('\n'); } +export function runBuck2(args: Array): AsyncCommandResult { + return runCommand('buck2', processArgsForBuck(args)); +} + export function runBuck2Sync(args: Array): SyncCommandResult { + return runCommandSync('buck2', processArgsForBuck(args)); +} + +function processArgsForBuck(args: Array): Array { // If these tests are already running from withing a buck2 process, e.g. when // they are scheduled by a `buck2 test` wrapper, calling `buck2` again would // cause a daemon-level deadlock. @@ -103,10 +165,10 @@ export function runBuck2Sync(args: Array): SyncCommandResult { // dir across tests (even running in different jest processes) to properly // employ caching. if (process.env.BUCK2_WRAPPER != null) { - args.unshift('--isolation-dir', BUCK_ISOLATION_DIR); + return ['--isolation-dir', BUCK_ISOLATION_DIR].concat(args); } - return runCommandSync('buck2', args); + return args; } export function getShortHash(contents: string): string { From 1e15a3cc07b9f2aa31c52730a741f72fd2ac8fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 08/10] Implement streaming mode for console logs in tests Differential Revision: D67600609 --- packages/react-native-fantom/runner/runner.js | 94 +++++++++++-------- packages/react-native-fantom/runner/utils.js | 48 +++++----- 2 files changed, 84 insertions(+), 58 deletions(-) diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index 754b5a0f7611..1b53dc7f9066 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -10,7 +10,7 @@ */ import type {TestSuiteResult} from '../runtime/setup'; -import type {ConsoleLogMessage, SyncCommandResult} from './utils'; +import type {AsyncCommandResult} from './utils'; import entrypointTemplate from './entrypoint-template'; import getFantomTestConfig from './getFantomTestConfig'; @@ -23,7 +23,8 @@ import { getBuckModesForPlatform, getDebugInfoFromCommandResult, getShortHash, - printConsoleLogs, + printConsoleLog, + runBuck2, runBuck2Sync, symbolicateStackTrace, } from './utils'; @@ -34,6 +35,7 @@ import {SnapshotState, buildSnapshotResolver} from 'jest-snapshot'; import Metro from 'metro'; import nullthrows from 'nullthrows'; import path from 'path'; +import readline from 'readline'; const BUILD_OUTPUT_ROOT = path.resolve(__dirname, '..', 'build'); fs.mkdirSync(BUILD_OUTPUT_ROOT, {recursive: true}); @@ -43,41 +45,71 @@ const BUILD_OUTPUT_PATH = fs.mkdtempSync( const PRINT_FANTOM_OUTPUT: false = false; -function parseRNTesterCommandResult(result: SyncCommandResult): { - logs: $ReadOnlyArray, - testResult: TestSuiteResult, -} { - const stdout = result.stdout.toString(); +async function processRNTesterCommandResult( + result: AsyncCommandResult, +): Promise { + const stdoutChunks = []; + const stderrChunks = []; + + result.childProcess.stdout.on('data', chunk => { + stdoutChunks.push(chunk); + }); + + result.childProcess.stderr.on('data', chunk => { + stderrChunks.push(chunk); + }); - const logs = []; let testResult; - const lines = stdout - .split('\n') - .map(line => line.trim()) - .filter(Boolean); + const rl = readline.createInterface({input: result.childProcess.stdout}); + rl.on('line', (rawLine: string) => { + const line = rawLine.trim(); + if (!line) { + return; + } - for (const line of lines) { let parsed; try { parsed = JSON.parse(line); - } catch {} + } catch { + parsed = { + type: 'console-log', + level: 'info', + message: line, + }; + } switch (parsed?.type) { case 'test-result': testResult = parsed; break; case 'console-log': - logs.push(parsed); + printConsoleLog(parsed); break; default: - logs.push({ + printConsoleLog({ type: 'console-log', - message: line, level: 'info', + message: line, }); break; } + }); + + await result.done; + + const getResultWithOutput = () => ({ + ...result, + stdout: stdoutChunks.join(''), + stderr: stderrChunks.join(''), + }); + + if (result.status !== 0) { + throw new Error(getDebugInfoFromCommandResult(getResultWithOutput())); + } + + if (PRINT_FANTOM_OUTPUT) { + console.log(getDebugInfoFromCommandResult(getResultWithOutput())); } if (testResult == null) { @@ -87,7 +119,7 @@ function parseRNTesterCommandResult(result: SyncCommandResult): { ); } - return {logs, testResult}; + return testResult; } function generateBytecodeBundle({ @@ -202,7 +234,7 @@ module.exports = async function runTest( }); } - const rnTesterCommandResult = runBuck2Sync([ + const rnTesterCommandResult = runBuck2([ 'run', ...getBuckModesForPlatform( testConfig.mode === FantomTestConfigMode.Optimized, @@ -219,19 +251,11 @@ module.exports = async function runTest( PRINT_FANTOM_OUTPUT ? 'info' : 'error', ]); - if (rnTesterCommandResult.status !== 0) { - throw new Error(getDebugInfoFromCommandResult(rnTesterCommandResult)); - } - - if (PRINT_FANTOM_OUTPUT) { - console.log(getDebugInfoFromCommandResult(rnTesterCommandResult)); - } - - const rnTesterParsedOutput = parseRNTesterCommandResult( + const processedResult = await processRNTesterCommandResult( rnTesterCommandResult, ); - const testResultError = rnTesterParsedOutput.testResult.error; + const testResultError = processedResult.error; if (testResultError) { const error = new Error(testResultError.message); error.stack = symbolicateStackTrace(sourceMapPath, testResultError.stack); @@ -240,12 +264,8 @@ module.exports = async function runTest( const endTime = Date.now(); - if (process.env.SANDCASTLE == null) { - printConsoleLogs(rnTesterParsedOutput.logs); - } - const testResults = - nullthrows(rnTesterParsedOutput.testResult.testResults).map(testResult => ({ + nullthrows(processedResult.testResults).map(testResult => ({ ancestorTitles: [] as Array, failureDetails: [] as Array, testFilePath: testPath, @@ -255,9 +275,9 @@ module.exports = async function runTest( ), })) ?? []; - const snapshotResults = nullthrows( - rnTesterParsedOutput.testResult.testResults, - ).map(testResult => testResult.snapshotResults); + const snapshotResults = nullthrows(processedResult.testResults).map( + testResult => testResult.snapshotResults, + ); const snapshotResult = updateSnapshotsAndGetJestSnapshotResult( snapshotState, diff --git a/packages/react-native-fantom/runner/utils.js b/packages/react-native-fantom/runner/utils.js index 794f169c01fb..4ec6299e8ce2 100644 --- a/packages/react-native-fantom/runner/utils.js +++ b/packages/react-native-fantom/runner/utils.js @@ -52,6 +52,8 @@ export type AsyncCommandResult = { status: ?number, signal: ?string, error: ?Error, + stdout: ?string, + stderr: ?string, }; export type SyncCommandResult = { @@ -90,6 +92,8 @@ export function runCommand( status: null, signal: null, error: null, + stdout: null, + stderr: null, }; childProcess.on('error', error => { @@ -123,22 +127,24 @@ export function runCommandSync( } export function getDebugInfoFromCommandResult( - commandResult: SyncCommandResult, + commandResult: SyncCommandResult | AsyncCommandResult, ): string { const maybeSignal = commandResult.signal != null ? `, signal: ${commandResult.signal}` : ''; const resultByStatus = commandResult.status === 0 ? 'succeeded' - : `failed (status code: ${commandResult.status}${maybeSignal})`; + : `failed (status code: ${commandResult.status ?? '(empty)'}${maybeSignal})`; const logLines = [ `Command ${resultByStatus}: ${commandResult.originalCommand}`, '', 'stdout:', + // $FlowExpectedError[sketchy-null-string] commandResult.stdout || '(empty)', '', 'stderr:', + // $FlowExpectedError[sketchy-null-string] commandResult.stderr || '(empty)', ]; @@ -210,24 +216,24 @@ export type ConsoleLogMessage = { message: string, }; -export function printConsoleLogs( - logs: $ReadOnlyArray, -): void { - for (const log of logs) { - switch (log.type) { - case 'console-log': - switch (log.level) { - case 'info': - console.log(log.message); - break; - case 'warn': - console.warn(log.message); - break; - case 'error': - console.error(log.message); - break; - } - break; - } +export function printConsoleLog(log: ConsoleLogMessage): void { + if (process.env.SANDCASTLE != null) { + return; + } + + switch (log.type) { + case 'console-log': + switch (log.level) { + case 'info': + console.log(log.message); + break; + case 'warn': + console.warn(log.message); + break; + case 'error': + console.error(log.message); + break; + } + break; } } From e50dbb0a886f27076740502cf4282468155fa4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:54:08 -0800 Subject: [PATCH 09/10] Add environment variables to print Fantom output and buck commands Differential Revision: D67682750 --- .../runner/EnvironmentOptions.js | 14 ++++++++++++++ packages/react-native-fantom/runner/runner.js | 7 +++---- packages/react-native-fantom/runner/utils.js | 11 +++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 packages/react-native-fantom/runner/EnvironmentOptions.js diff --git a/packages/react-native-fantom/runner/EnvironmentOptions.js b/packages/react-native-fantom/runner/EnvironmentOptions.js new file mode 100644 index 000000000000..dd2cfd7fdc26 --- /dev/null +++ b/packages/react-native-fantom/runner/EnvironmentOptions.js @@ -0,0 +1,14 @@ +/** + * 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 + * @format + * @oncall react_native + */ + +export const printCLIOutput: boolean = Boolean(process.env.FANTOM_PRINT_OUTPUT); + +export const logCommands: boolean = Boolean(process.env.FANTOM_LOG_COMMANDS); diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index 1b53dc7f9066..ef86cd44415c 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -13,6 +13,7 @@ import type {TestSuiteResult} from '../runtime/setup'; import type {AsyncCommandResult} from './utils'; import entrypointTemplate from './entrypoint-template'; +import * as EnvironmentOptions from './EnvironmentOptions'; import getFantomTestConfig from './getFantomTestConfig'; import {FantomTestConfigMode} from './getFantomTestConfig'; import { @@ -43,8 +44,6 @@ const BUILD_OUTPUT_PATH = fs.mkdtempSync( path.join(BUILD_OUTPUT_ROOT, `run-${Date.now()}-`), ); -const PRINT_FANTOM_OUTPUT: false = false; - async function processRNTesterCommandResult( result: AsyncCommandResult, ): Promise { @@ -108,7 +107,7 @@ async function processRNTesterCommandResult( throw new Error(getDebugInfoFromCommandResult(getResultWithOutput())); } - if (PRINT_FANTOM_OUTPUT) { + if (EnvironmentOptions.printCLIOutput) { console.log(getDebugInfoFromCommandResult(getResultWithOutput())); } @@ -248,7 +247,7 @@ module.exports = async function runTest( '--featureFlags', JSON.stringify(testConfig.flags.common), '--minLogLevel', - PRINT_FANTOM_OUTPUT ? 'info' : 'error', + EnvironmentOptions.printCLIOutput ? 'info' : 'error', ]); const processedResult = await processRNTesterCommandResult( diff --git a/packages/react-native-fantom/runner/utils.js b/packages/react-native-fantom/runner/utils.js index 4ec6299e8ce2..868ec44a5734 100644 --- a/packages/react-native-fantom/runner/utils.js +++ b/packages/react-native-fantom/runner/utils.js @@ -9,6 +9,7 @@ * @oncall react_native */ +import * as EnvironmentOptions from './EnvironmentOptions'; import {spawn, spawnSync} from 'child_process'; import crypto from 'crypto'; import fs from 'fs'; @@ -66,10 +67,18 @@ export type SyncCommandResult = { stderr: string, }; +function maybeLogCommand(command: string, args: Array): void { + if (EnvironmentOptions.logCommands) { + console.log(`RUNNING \`${command} ${args.join(' ')}\``); + } +} + export function runCommand( command: string, args: Array, ): AsyncCommandResult { + maybeLogCommand(command, args); + const childProcess = spawn(command, args, { encoding: 'utf8', env: { @@ -107,6 +116,8 @@ export function runCommandSync( command: string, args: Array, ): SyncCommandResult { + maybeLogCommand(command, args); + const result = spawnSync(command, args, { encoding: 'utf8', env: { From 01d1df067b500e7501878a24cec32891b6b21993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 3 Jan 2025 01:59:06 -0800 Subject: [PATCH 10/10] Add environment variable to enable C++ debugging (#48441) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48441 Changelog: [internal] Adds a new environment variable (`FANTOM_ENABLE_CPP_DEBUGGING`) to enable C++ debugging via `fdb`. Reviewed By: RSNara Differential Revision: D67683048 --- .../runner/EnvironmentOptions.js | 4 ++ packages/react-native-fantom/runner/runner.js | 37 +++++++++++-------- packages/react-native-fantom/runner/utils.js | 36 ++++++++++++++++-- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/packages/react-native-fantom/runner/EnvironmentOptions.js b/packages/react-native-fantom/runner/EnvironmentOptions.js index dd2cfd7fdc26..18c9173c6b76 100644 --- a/packages/react-native-fantom/runner/EnvironmentOptions.js +++ b/packages/react-native-fantom/runner/EnvironmentOptions.js @@ -12,3 +12,7 @@ export const printCLIOutput: boolean = Boolean(process.env.FANTOM_PRINT_OUTPUT); export const logCommands: boolean = Boolean(process.env.FANTOM_LOG_COMMANDS); + +export const enableCppDebugging: boolean = Boolean( + process.env.FANTOM_ENABLE_CPP_DEBUGGING, +); diff --git a/packages/react-native-fantom/runner/runner.js b/packages/react-native-fantom/runner/runner.js index ef86cd44415c..3c2703504430 100644 --- a/packages/react-native-fantom/runner/runner.js +++ b/packages/react-native-fantom/runner/runner.js @@ -233,22 +233,27 @@ module.exports = async function runTest( }); } - const rnTesterCommandResult = runBuck2([ - 'run', - ...getBuckModesForPlatform( - testConfig.mode === FantomTestConfigMode.Optimized, - ), - '//xplat/ReactNative/react-native-cxx/samples/tester:tester', - '--', - '--bundlePath', - testConfig.mode === FantomTestConfigMode.DevelopmentWithSource - ? testJSBundlePath - : testBytecodeBundlePath, - '--featureFlags', - JSON.stringify(testConfig.flags.common), - '--minLogLevel', - EnvironmentOptions.printCLIOutput ? 'info' : 'error', - ]); + const rnTesterCommandResult = runBuck2( + [ + 'run', + ...getBuckModesForPlatform( + testConfig.mode === FantomTestConfigMode.Optimized, + ), + '//xplat/ReactNative/react-native-cxx/samples/tester:tester', + '--', + '--bundlePath', + testConfig.mode === FantomTestConfigMode.DevelopmentWithSource + ? testJSBundlePath + : testBytecodeBundlePath, + '--featureFlags', + JSON.stringify(testConfig.flags.common), + '--minLogLevel', + EnvironmentOptions.printCLIOutput ? 'info' : 'error', + ], + { + withFDB: EnvironmentOptions.enableCppDebugging, + }, + ); const processedResult = await processRNTesterCommandResult( rnTesterCommandResult, diff --git a/packages/react-native-fantom/runner/utils.js b/packages/react-native-fantom/runner/utils.js index 868ec44a5734..93873737d3f0 100644 --- a/packages/react-native-fantom/runner/utils.js +++ b/packages/react-native-fantom/runner/utils.js @@ -166,12 +166,40 @@ export function getDebugInfoFromCommandResult( return logLines.join('\n'); } -export function runBuck2(args: Array): AsyncCommandResult { - return runCommand('buck2', processArgsForBuck(args)); +function getCommandAndArgsWithFDB( + command: string, + args: Array, + useFDB: boolean, +) { + if (useFDB) { + return ['fdb', [command].concat(args)]; + } else { + return [command, args]; + } } -export function runBuck2Sync(args: Array): SyncCommandResult { - return runCommandSync('buck2', processArgsForBuck(args)); +export function runBuck2( + args: Array, + options?: {withFDB: boolean}, +): AsyncCommandResult { + const [actualCommand, actualArgs] = getCommandAndArgsWithFDB( + 'buck2', + processArgsForBuck(args), + options?.withFDB ?? false, + ); + return runCommand(actualCommand, actualArgs); +} + +export function runBuck2Sync( + args: Array, + options?: {withFDB: boolean}, +): SyncCommandResult { + const [actualCommand, actualArgs] = getCommandAndArgsWithFDB( + 'buck2', + processArgsForBuck(args), + options?.withFDB ?? false, + ); + return runCommandSync(actualCommand, actualArgs); } function processArgsForBuck(args: Array): Array {