From be9b076087d9f809b73f5653ee025340a42fb161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 5 Dec 2024 17:06:11 -0800 Subject: [PATCH] Add support for specifying feature flags in pragmas (#48097) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48097 Changelog: [internal] As per title, this allows us to specify both common and JS-only feature flags for tests in the docblock as pragmas (in the same pragma separated by spaces, or in different pragmas). E.g.: ``` /** * fantom_flags commonTestFlag:true * fantom_flags jsOnlyTestFlag:true */ ``` The feature flags are overridden automatically for us before the tests start. Reviewed By: javache Differential Revision: D66760121 fbshipit-source-id: 7e227e0035a170dab81b1e6ce39600a01a748867 --- .../integration/runner/entrypoint-template.js | 17 +++ .../integration/runner/getFantomTestConfig.js | 116 ++++++++++++++++-- jest/integration/runner/runner.js | 8 ++ .../TextInput/__tests__/TextInput-itest.js | 1 + .../__tests__/FantomFeatureFlags-itest.js | 23 ++++ .../__tests__/ReactNativeElement-itest.js | 2 +- .../dom/nodes/__tests__/ReadOnlyText-itest.js | 2 +- .../dom/nodes/__tests__/setUpFeatureFlags.js | 16 --- .../__tests__/LongTaskAPI-itest.js | 1 + 9 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 packages/react-native/src/private/__tests__/FantomFeatureFlags-itest.js delete mode 100644 packages/react-native/src/private/webapis/dom/nodes/__tests__/setUpFeatureFlags.js diff --git a/jest/integration/runner/entrypoint-template.js b/jest/integration/runner/entrypoint-template.js index ad582c4dbcf..872b21ea1f8 100644 --- a/jest/integration/runner/entrypoint-template.js +++ b/jest/integration/runner/entrypoint-template.js @@ -9,12 +9,18 @@ * @oncall react_native */ +import type {FantomTestConfigJsOnlyFeatureFlags} from './getFantomTestConfig'; + module.exports = function entrypointTemplate({ testPath, setupModulePath, + featureFlagsModulePath, + featureFlags, }: { testPath: string, setupModulePath: string, + featureFlagsModulePath: string, + featureFlags: FantomTestConfigJsOnlyFeatureFlags, }): string { return `/** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -29,6 +35,17 @@ module.exports = function entrypointTemplate({ */ import {registerTest} from '${setupModulePath}'; +${ + Object.keys(featureFlags).length > 0 + ? `import * as ReactNativeFeatureFlags from '${featureFlagsModulePath}'; + +ReactNativeFeatureFlags.override({ +${Object.entries(featureFlags) + .map(([name, value]) => ` ${name}: () => ${JSON.stringify(value)},`) + .join('\n')} +});` + : '' +} registerTest(() => require('${testPath}')); `; diff --git a/jest/integration/runner/getFantomTestConfig.js b/jest/integration/runner/getFantomTestConfig.js index 98a63ce3bf1..c7d662045d7 100644 --- a/jest/integration/runner/getFantomTestConfig.js +++ b/jest/integration/runner/getFantomTestConfig.js @@ -9,17 +9,37 @@ * @oncall react_native */ +import ReactNativeFeatureFlags from '../../../packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config'; import fs from 'fs'; // $FlowExpectedError[untyped-import] import {extract, parse} from 'jest-docblock'; +type CommonFeatureFlags = (typeof ReactNativeFeatureFlags)['common']; +type JsOnlyFeatureFlags = (typeof ReactNativeFeatureFlags)['jsOnly']; + type DocblockPragmas = {[key: string]: string | string[]}; -type FantomTestMode = 'dev' | 'opt'; -type FantomTestConfig = { - mode: FantomTestMode, + +export type FantomTestConfigMode = 'dev' | 'opt'; + +export type FantomTestConfigCommonFeatureFlags = Partial<{ + [key in keyof CommonFeatureFlags]: CommonFeatureFlags[key]['defaultValue'], +}>; + +export type FantomTestConfigJsOnlyFeatureFlags = Partial<{ + [key in keyof JsOnlyFeatureFlags]: JsOnlyFeatureFlags[key]['defaultValue'], +}>; + +export type FantomTestConfig = { + mode: FantomTestConfigMode, + flags: { + common: FantomTestConfigCommonFeatureFlags, + jsOnly: FantomTestConfigJsOnlyFeatureFlags, + }, }; -const DEFAULT_MODE: FantomTestMode = 'dev'; +const DEFAULT_MODE: FantomTestConfigMode = 'dev'; + +const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/; /** * Extracts the Fantom configuration from the test file, specified as part of @@ -28,11 +48,18 @@ const DEFAULT_MODE: FantomTestMode = 'dev'; * ``` * /** * * @flow strict-local - * * @fantom mode:opt + * * @fantom_mode opt + * * @fantom_flags commonTestFlag:true + * * @fantom_flags jsOnlyTestFlag:true * * * ``` * - * So far the only supported option is `mode`, which can be 'dev' or 'opt'. + * The supported options are: + * - `fantom_mode`: specifies the level of optimization to compile the test + * with. Valid values are `dev` and `opt`. + * - `fantom_flags`: specifies the configuration for common and JS-only feature + * flags. They can be specified in the same pragma or in different ones, and + * the format is `:`. */ export default function getFantomTestConfig( testPath: string, @@ -40,8 +67,12 @@ export default function getFantomTestConfig( const docblock = extract(fs.readFileSync(testPath, 'utf8')); const pragmas = parse(docblock) as DocblockPragmas; - const config = { + const config: FantomTestConfig = { mode: DEFAULT_MODE, + flags: { + common: {}, + jsOnly: {}, + }, }; const maybeMode = pragmas.fantom_mode; @@ -60,5 +91,76 @@ export default function getFantomTestConfig( } } + const maybeRawFlagConfig = pragmas.fantom_flags; + + if (maybeRawFlagConfig != null) { + const rawFlagConfigs = ( + Array.isArray(maybeRawFlagConfig) + ? maybeRawFlagConfig + : [maybeRawFlagConfig] + ).flatMap(value => value.split(/\s+/g)); + + for (const rawFlagConfig of rawFlagConfigs) { + const matches = FANTOM_FLAG_FORMAT.exec(rawFlagConfig); + if (matches == null) { + throw new Error( + `Invalid format for Fantom feature flag: ${rawFlagConfig}. Expected :`, + ); + } + + const [, name, rawValue] = matches; + + if (ReactNativeFeatureFlags.common[name]) { + const flagConfig = ReactNativeFeatureFlags.common[name]; + const value = parseFeatureFlagValue(flagConfig.defaultValue, rawValue); + config.flags.common[name] = value; + } else if (ReactNativeFeatureFlags.jsOnly[name]) { + const flagConfig = ReactNativeFeatureFlags.jsOnly[name]; + const value = parseFeatureFlagValue(flagConfig.defaultValue, rawValue); + config.flags.jsOnly[name] = value; + } else { + const validKeys = Object.keys(ReactNativeFeatureFlags.common) + .concat(Object.keys(ReactNativeFeatureFlags.jsOnly)) + .join(', '); + + throw new Error( + `Invalid Fantom feature flag: ${name}. Valid flags are: ${validKeys}`, + ); + } + } + } + return config; } + +function parseFeatureFlagValue( + defaultValue: T, + value: string, +): T { + switch (typeof defaultValue) { + case 'boolean': + if (value === 'true') { + // $FlowExpectedError[incompatible-return] at this point we know T is a boolean + return true; + } else if (value === 'false') { + // $FlowExpectedError[incompatible-return] at this point we know T is a boolean + return false; + } else { + throw new Error(`Invalid value for boolean flag: ${value}`); + } + case 'number': + const parsed = Number(value); + + if (Number.isNaN(parsed)) { + throw new Error(`Invalid value for number flag: ${value}`); + } + + // $FlowExpectedError[incompatible-return] at this point we know T is a number + return parsed; + case 'string': + // $FlowExpectedError[incompatible-return] at this point we know T is a string + return value; + default: + throw new Error(`Unsupported feature flag type: ${typeof defaultValue}`); + } +} diff --git a/jest/integration/runner/runner.js b/jest/integration/runner/runner.js index 4247edb5461..81f3502635b 100644 --- a/jest/integration/runner/runner.js +++ b/jest/integration/runner/runner.js @@ -104,10 +104,16 @@ module.exports = async function runTest( }); const setupModulePath = path.resolve(__dirname, '../runtime/setup.js'); + const featureFlagsModulePath = path.resolve( + __dirname, + '../../../packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js', + ); const entrypointContents = entrypointTemplate({ testPath: `${path.relative(BUILD_OUTPUT_PATH, testPath)}`, setupModulePath: `${path.relative(BUILD_OUTPUT_PATH, setupModulePath)}`, + featureFlagsModulePath: `${path.relative(BUILD_OUTPUT_PATH, featureFlagsModulePath)}`, + featureFlags: testConfig.flags.jsOnly, }); const entrypointPath = path.join( @@ -151,6 +157,8 @@ module.exports = async function runTest( '--', '--bundlePath', testBundlePath, + '--featureFlags', + JSON.stringify(testConfig.flags.common), ]); if (rnTesterCommandResult.status !== 0) { diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js index 504d69fd766..59c47cf7bc4 100644 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js @@ -7,6 +7,7 @@ * @flow strict-local * @format * @oncall react_native + * @fantom_flags enableFixForViewCommandRace:true */ import '../../../Core/InitializeCore.js'; diff --git a/packages/react-native/src/private/__tests__/FantomFeatureFlags-itest.js b/packages/react-native/src/private/__tests__/FantomFeatureFlags-itest.js new file mode 100644 index 00000000000..bf590c8dc60 --- /dev/null +++ b/packages/react-native/src/private/__tests__/FantomFeatureFlags-itest.js @@ -0,0 +1,23 @@ +/** + * 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 + * @fantom_flags commonTestFlag:true jsOnlyTestFlag:true + */ + +import * as ReactNativeFeatureFlags from '../featureflags/ReactNativeFeatureFlags'; + +describe('FantomFeatureFlags', () => { + it('allows overridding common feature flags', () => { + expect(ReactNativeFeatureFlags.commonTestFlag()).toBe(true); + }); + + it('allows overridding JS-only feature flags', () => { + expect(ReactNativeFeatureFlags.jsOnlyTestFlag()).toBe(true); + }); +}); diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js index ad8d0a1bccf..8f25bfa9d8e 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js @@ -7,9 +7,9 @@ * @flow strict-local * @format * @oncall react_native + * @fantom_flags enableAccessToHostTreeInFabric:true */ -import './setUpFeatureFlags'; import '../../../../../../Libraries/Core/InitializeCore.js'; import * as ReactNativeTester from '../../../../__tests__/ReactNativeTester'; diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js index 1d74bfb76c3..c30b444b025 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js @@ -7,9 +7,9 @@ * @flow strict-local * @format * @oncall react_native + * @fantom_flags enableAccessToHostTreeInFabric:true */ -import './setUpFeatureFlags'; import '../../../../../../Libraries/Core/InitializeCore.js'; import {NativeText} from '../../../../../../Libraries/Text/TextNativeComponent'; diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/setUpFeatureFlags.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/setUpFeatureFlags.js deleted file mode 100644 index 6b2553b76b6..00000000000 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/setUpFeatureFlags.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 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 * as ReactNativeFeatureFlags from '../../../../featureflags/ReactNativeFeatureFlags'; - -ReactNativeFeatureFlags.override({ - enableAccessToHostTreeInFabric: () => true, -}); diff --git a/packages/react-native/src/private/webapis/performance/__tests__/LongTaskAPI-itest.js b/packages/react-native/src/private/webapis/performance/__tests__/LongTaskAPI-itest.js index 41c862e86f3..28086c38f38 100644 --- a/packages/react-native/src/private/webapis/performance/__tests__/LongTaskAPI-itest.js +++ b/packages/react-native/src/private/webapis/performance/__tests__/LongTaskAPI-itest.js @@ -7,6 +7,7 @@ * @flow strict-local * @format * @oncall react_native + * @fantom_flags enableLongTaskAPI:true */ import type {PerformanceObserverCallbackOptions} from '../PerformanceObserver';