mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
db70b791ba
commit
be9b076087
@@ -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}'));
|
||||
`;
|
||||
|
||||
@@ -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 `<flag_name>:<value>`.
|
||||
*/
|
||||
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 <flag_name>:<value>`,
|
||||
);
|
||||
}
|
||||
|
||||
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<T: boolean | number | string>(
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
@@ -7,6 +7,7 @@
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @oncall react_native
|
||||
* @fantom_flags enableFixForViewCommandRace:true
|
||||
*/
|
||||
|
||||
import '../../../Core/InitializeCore.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);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
-16
@@ -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,
|
||||
});
|
||||
+1
@@ -7,6 +7,7 @@
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @oncall react_native
|
||||
* @fantom_flags enableLongTaskAPI:true
|
||||
*/
|
||||
|
||||
import type {PerformanceObserverCallbackOptions} from '../PerformanceObserver';
|
||||
|
||||
Reference in New Issue
Block a user