Add flutter: config: {...} section to pubspec.yaml to influence FeatureFlags (#167953)

Closes https://github.com/flutter/flutter/issues/167667.

In this PR:

- `FlutterFeaturesConfig` reads from the global config, current
pubspec.yaml, and environment
- `FeatureFlags` now delegates "was this configured" to
`FlutterFeaturesConfig`
- `featureFlags` (`Context`) now is created with knowledge of the
current `pubpec.yaml`, if any

Once this lands and we play with it a bit (i.e. migrate
`disable-swift-package-manager`), we can publicize it and update website
documentation (it won't make it into this beta/stable release anyway).

I did a significant rewrite of `feature_test`, as lots of what was being
tested no longer made sense.
This commit is contained in:
Matan Lurey 2025-04-28 16:59:21 -07:00 committed by GitHub
parent 2a3e27fdd9
commit f94a82d2e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 796 additions and 392 deletions

View File

@ -41,6 +41,8 @@ import 'flutter_application_package.dart';
import 'flutter_cache.dart';
import 'flutter_device_manager.dart';
import 'flutter_features.dart';
import 'flutter_features_config.dart';
import 'flutter_manifest.dart';
import 'globals.dart' as globals;
import 'ios/ios_workflow.dart';
import 'ios/iproxy.dart';
@ -233,7 +235,15 @@ Future<T> runInContext<T>(FutureOr<T> Function() runner, {Map<Type, Generator>?
FeatureFlags:
() => FlutterFeatureFlags(
flutterVersion: globals.flutterVersion,
config: globals.config,
featuresConfig: FlutterFeaturesConfig(
globalConfig: globals.config,
platform: globals.platform,
projectManifest: FlutterManifest.createFromPath(
globals.fs.path.join(globals.fs.currentDirectory.path, 'pubspec.yaml'),
fileSystem: globals.fs,
logger: globals.logger,
),
),
platform: globals.platform,
),
FlutterVersion: () => FlutterVersion(fs: globals.fs, flutterRoot: Cache.flutterRoot!),

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'base/context.dart';
/// The current [FeatureFlags] implementation.
@ -275,7 +277,8 @@ class Feature {
}
/// A description of the conditions to enable a feature for a particular channel.
class FeatureChannelSetting {
@immutable
final class FeatureChannelSetting {
const FeatureChannelSetting({this.available = false, this.enabledByDefault = false});
/// Whether the feature is available on this channel.
@ -288,4 +291,19 @@ class FeatureChannelSetting {
///
/// If not provided, defaults to `false`.
final bool enabledByDefault;
@override
bool operator ==(Object other) {
return other is FeatureChannelSetting &&
available == other.available &&
enabledByDefault == other.enabledByDefault;
}
@override
int get hashCode => Object.hash(available, enabledByDefault);
@override
String toString() {
return 'FeatureChannelSetting <available: $available, enabledByDefault: $enabledByDefault>';
}
}

View File

@ -2,23 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'base/config.dart';
import 'package:meta/meta.dart';
import 'base/platform.dart';
import 'features.dart';
import 'flutter_features_config.dart';
import 'version.dart';
class FlutterFeatureFlags implements FeatureFlags {
FlutterFeatureFlags({
required FlutterVersion flutterVersion,
required Config config,
required Platform platform,
}) : _flutterVersion = flutterVersion,
_config = config,
_platform = platform;
final FlutterVersion _flutterVersion;
final Config _config;
final Platform _platform;
@visibleForTesting
mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags {
@protected
Platform get platform;
@override
bool get isLinuxEnabled => isEnabled(flutterLinuxDesktopFeature);
@ -46,7 +40,7 @@ class FlutterFeatureFlags implements FeatureFlags {
@override
bool get isCliAnimationEnabled {
if (_platform.environment['TERM'] == 'dumb') {
if (platform.environment['TERM'] == 'dumb') {
return false;
}
return isEnabled(cliAnimation);
@ -60,26 +54,34 @@ class FlutterFeatureFlags implements FeatureFlags {
@override
bool get isExplicitPackageDependenciesEnabled => isEnabled(explicitPackageDependencies);
}
interface class FlutterFeatureFlags with FlutterFeatureFlagsIsEnabled implements FeatureFlags {
FlutterFeatureFlags({
required FlutterVersion flutterVersion,
required FlutterFeaturesConfig featuresConfig,
required this.platform,
}) : _flutterVersion = flutterVersion,
_featuresConfig = featuresConfig;
final FlutterVersion _flutterVersion;
final FlutterFeaturesConfig _featuresConfig;
@override
@protected
final Platform platform;
@override
bool isEnabled(Feature feature) {
final String currentChannel = _flutterVersion.channel;
final FeatureChannelSetting featureSetting = feature.getSettingForChannel(currentChannel);
// If unavailable, then no setting can enable this feature.
if (!featureSetting.available) {
return false;
}
bool isEnabled = featureSetting.enabledByDefault;
if (feature.configSetting != null) {
final bool? configOverride = _config.getValue(feature.configSetting!) as bool?;
if (configOverride != null) {
isEnabled = configOverride;
}
}
if (feature.environmentOverride != null) {
if (_platform.environment[feature.environmentOverride]?.toLowerCase() == 'true') {
isEnabled = true;
}
}
return isEnabled;
// Otherwise, read it from environment variable > project manifest > global config
return _featuresConfig.isEnabled(feature) ?? featureSetting.enabledByDefault;
}
}

View File

@ -0,0 +1,168 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'base/common.dart';
import 'base/config.dart';
import 'base/platform.dart';
import 'features.dart';
import 'flutter_manifest.dart';
/// Reads configuration flags to possibly override the default flag value.
///
/// See [isEnabled] for details on how feature flag values are resolved.
interface class FlutterFeaturesConfig {
/// Creates a feature configuration reader from the provided sources.
///
/// [globalConfig] reads values stored by the `flutter config` tool, which
/// are normally in the user's `%HOME` directory (varies by system), while
/// [projectManifest] reads values from the _current_ Flutter project's
/// `pubspec.yaml`
const FlutterFeaturesConfig({
required Config globalConfig,
required Platform platform,
required FlutterManifest? projectManifest,
}) : _globalConfig = globalConfig,
_platform = platform,
_projectManifest = projectManifest;
final Config _globalConfig;
final Platform _platform;
// Can be null if no manifest file exists in the current directory.
final FlutterManifest? _projectManifest;
/// Returns whether [feature] has been turned on/off from configuration.
///
/// If the feature was not configured, or cannot be configured, returns `null`.
///
/// The value is resolved, if possible, in the following order, where if a
/// step resolves to a boolean value, no further steps are attempted:
///
///
/// ## 1. Local Project Configuration
///
/// If [Feature.configSetting] is `null`, this step is skipped.
///
/// If the value defined by the key `$configSetting` is set in `pubspec.yaml`,
/// it is returned as a boolean value.
///
/// Assuming there is a setting where `configSetting: 'enable-foo'`:
///
/// ```yaml
/// # true
/// flutter:
/// config:
/// enable-foo: true
///
/// # false
/// flutter:
/// config:
/// enable-foo: false
/// ```
///
/// ## 2. Global Tool Configuration
///
/// If [Feature.configSetting] is `null`, this step is skipped.
///
/// If the value defined by the key `$configSetting` is set in the global
/// (platform dependent) configuration file, it is returned as a boolean
/// value.
///
/// Assuming there is a setting where `configSetting: 'enable-foo'`:
///
/// ```sh
/// # future runs will treat the value as true
/// flutter config --enable-foo
///
/// # future runs will treat the value as false
/// flutter config --no-enable-foo
/// ```
///
/// ## 3. Environment Variable
///
/// If [Feature.environmentOverride] is `null`, this step is skipped.
///
/// If the value defined by the key `$environmentOverride` is equal to the
/// string `'true'` (case insensitive), returns `true`, or `false` otherwise.
///
/// Assuming there is a flag where `environmentOverride: 'ENABLE_FOO'`:
///
/// ```sh
/// # true
/// ENABLE_FOO=true flutter some-command
///
/// # true
/// ENABLE_FOO=TRUE flutter some-command
///
/// # false
/// ENABLE_FOO=false flutter some-command
///
/// # false
/// ENABLE_FOO=any-other-value flutter some-command
/// ```
bool? isEnabled(Feature feature) {
return _isEnabledByConfigValue(feature) ?? _isEnabledByPlatformEnvironment(feature);
}
bool? _isEnabledByConfigValue(Feature feature) {
// If the feature cannot be configured by local/global config settings, return null.
final String? featureName = feature.configSetting;
if (featureName == null) {
return null;
}
return _isEnabledAtProjectLevel(featureName) ?? _isEnabledByGlobalConfig(featureName);
}
bool? _isEnabledByPlatformEnvironment(Feature feature) {
// If the feature cannot be configured by an environment variable, return null.
final String? environmentName = feature.environmentOverride;
if (environmentName == null) {
return null;
}
final Object? environmentValue = _platform.environment[environmentName]?.toLowerCase();
if (environmentValue == null) {
return null;
}
return environmentValue == 'true';
}
bool? _isEnabledAtProjectLevel(String featureName) {
final Object? configSection = _projectManifest?.flutterDescriptor['config'];
if (configSection == null) {
return null;
}
if (configSection is! Map) {
throwToolExit(
'The "config" property of "flutter" in pubspec.yaml must be a map, but '
'got $configSection (${configSection.runtimeType})',
);
}
return _requireBoolOrNull(
configSection[featureName],
featureName: featureName,
source: '"flutter: config:" in pubspec.yaml',
);
}
bool? _isEnabledByGlobalConfig(String featureName) {
return _requireBoolOrNull(
_globalConfig.getValue(featureName),
featureName: featureName,
source: '"${_globalConfig.configPath}"',
);
}
static bool? _requireBoolOrNull(
Object? value, {
required String featureName,
required String source,
}) {
if (value is bool?) {
return value;
}
throwToolExit(
'The "$featureName" property in $source must be a boolean, but got $value (${value.runtimeType})',
);
}
}

View File

@ -608,6 +608,9 @@ void _validateFlutter(YamlMap? yaml, List<String> errors) {
errors.addAll(pluginErrors);
case 'generate':
break;
case 'config':
// Futher validation is defined in FlutterFeaturesConfig.
break;
case 'deferred-components':
_validateDeferredComponents(kvp, errors);
case 'disable-swift-package-manager':

View File

@ -0,0 +1,169 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_features_config.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import '../src/common.dart';
const Feature _noConfigFeature = Feature(name: 'example');
const Feature _configOnlyFeature = Feature(name: 'example', configSetting: 'enable-flag');
const Feature _envOnlyFeature = Feature(name: 'example', environmentOverride: 'ENABLE_FLAG');
const Feature _configAndEnvFeature = Feature(
name: 'example',
configSetting: 'enable-flag',
environmentOverride: 'ENABLE_FLAG',
);
void main() {
bool? isEnabled(
Feature feature, {
Map<String, String> environment = const <String, String>{},
Map<String, Object> globalConfig = const <String, Object>{},
String? projectManifest,
}) {
final Config globalConfigReader = Config.test();
for (final MapEntry<String, Object>(:String key, :Object value) in globalConfig.entries) {
globalConfigReader.setValue(key, value);
}
final BufferLogger logger = BufferLogger.test();
final FlutterManifest? flutterManifest =
projectManifest != null
? FlutterManifest.createFromString(projectManifest, logger: logger)
: FlutterManifest.empty(logger: logger);
if (flutterManifest == null) {
fail(logger.errorText);
}
final FlutterFeaturesConfig featuresConfig = FlutterFeaturesConfig(
globalConfig: globalConfigReader,
platform: FakePlatform(environment: <String, String>{...environment}),
projectManifest: flutterManifest,
);
return featuresConfig.isEnabled(feature);
}
test('returns null if cannot be overriden', () {
expect(
isEnabled(
_noConfigFeature,
environment: <String, String>{'ENABLE_FLAG': 'true'},
globalConfig: <String, Object>{'enable-flag': true},
projectManifest: '''
flutter:
config:
enable-flag: true
''',
),
isNull,
);
});
test('returns null if every source is omitted', () {
expect(isEnabled(_configAndEnvFeature), isNull);
});
test('overrides from local manifest', () {
expect(
isEnabled(
_configOnlyFeature,
projectManifest: '''
flutter:
config:
enable-flag: true
''',
),
isTrue,
);
});
test('local manifest config must be a map', () {
expect(
() => isEnabled(
_configOnlyFeature,
projectManifest: '''
flutter:
config: true
''',
),
throwsToolExit(message: 'must be a map'),
);
});
test('local manifest value must be a boolean', () {
expect(
() => isEnabled(
_configOnlyFeature,
projectManifest: '''
flutter:
config:
enable-flag: NOT-A-BOOLEAN
''',
),
throwsToolExit(message: 'must be a boolean'),
);
});
test('overrides from global configuration', () {
expect(
isEnabled(_configOnlyFeature, globalConfig: <String, Object>{'enable-flag': true}),
isTrue,
);
});
test('global configuration value must be a boolean', () {
expect(
() => isEnabled(
_configOnlyFeature,
globalConfig: <String, Object>{'enable-flag': 'NOT-A-BOOLEAN'},
),
throwsToolExit(message: 'must be a boolean'),
);
});
test('overrides from local manifest take precedence over global configuration', () {
expect(
isEnabled(
_configOnlyFeature,
projectManifest: '''
flutter:
config:
enable-flag: true
''',
globalConfig: <String, Object>{'enable-flag': false},
),
isTrue,
);
});
test('overrides from environment', () {
expect(
isEnabled(_envOnlyFeature, environment: <String, String>{'ENABLE_FLAG': 'true'}),
isTrue,
);
});
test('overrides from environment are case insensitive', () {
expect(
isEnabled(_envOnlyFeature, environment: <String, String>{'ENABLE_FLAG': 'tRuE'}),
isTrue,
);
});
test('overrides from environment are lowest priority', () {
expect(
isEnabled(
_configAndEnvFeature,
environment: <String, String>{'ENABLE_FLAG': 'true'},
globalConfig: <String, Object>{'enable-flag': false},
),
isFalse,
);
});
}

View File

@ -2,43 +2,19 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/base/config.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_features.dart';
import 'package:flutter_tools/src/flutter_features_config.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
void main() {
group('Features', () {
late Config testConfig;
late FakePlatform platform;
late FlutterFeatureFlags featureFlags;
setUp(() {
testConfig = Config.test();
platform = FakePlatform(environment: <String, String>{});
for (final Feature feature in allConfigurableFeatures) {
testConfig.setValue(feature.configSetting!, false);
}
featureFlags = FlutterFeatureFlags(
flutterVersion: FakeFlutterVersion(),
config: testConfig,
platform: platform,
);
});
FeatureFlags createFlags(String channel) {
return FlutterFeatureFlags(
flutterVersion: FakeFlutterVersion(branch: channel),
config: testConfig,
platform: platform,
);
}
testWithoutContext('setting has safe defaults', () {
const FeatureChannelSetting featureSetting = FeatureChannelSetting();
@ -71,358 +47,416 @@ void main() {
expect(feature.getSettingForChannel('unknown'), masterSetting);
});
testWithoutContext('env variables are only enabled with "true" string', () {
platform.environment = <String, String>{'FLUTTER_WEB': 'hello'};
expect(featureFlags.isWebEnabled, false);
platform.environment = <String, String>{'FLUTTER_WEB': 'true'};
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web help string', () {
expect(flutterWebFeature.generateHelpMessage(), 'Enable or disable Flutter for web.');
});
testWithoutContext('Flutter macOS desktop help string', () {
expect(
flutterMacOSDesktopFeature.generateHelpMessage(),
'Enable or disable support for desktop on macOS.',
);
});
testWithoutContext('Flutter Linux desktop help string', () {
expect(
flutterLinuxDesktopFeature.generateHelpMessage(),
'Enable or disable support for desktop on Linux.',
);
});
testWithoutContext('Flutter Windows desktop help string', () {
expect(
flutterWindowsDesktopFeature.generateHelpMessage(),
'Enable or disable support for desktop on Windows.',
);
});
testWithoutContext('help string on multiple channels', () {
const Feature testWithoutContextFeature = Feature(
testWithoutContext('reads from configuration if available', () {
const Feature exampleFeature = Feature(
name: 'example',
master: FeatureChannelSetting(available: true),
beta: FeatureChannelSetting(available: true),
stable: FeatureChannelSetting(available: true),
configSetting: 'foo',
);
expect(testWithoutContextFeature.generateHelpMessage(), 'Enable or disable example.');
final FlutterFeatureFlags flags = FlutterFeatureFlags(
flutterVersion: FakeFlutterVersion(),
featuresConfig: _FakeFeaturesConfig()..cannedResponse[exampleFeature] = true,
platform: FakePlatform(),
);
expect(flags.isEnabled(exampleFeature), true);
});
/// Flutter Web
testWithoutContext('returns false if not available', () {
const Feature exampleFeature = Feature(name: 'example');
testWithoutContext('Flutter web off by default on master', () {
final FeatureFlags featureFlags = createFlags('master');
expect(featureFlags.isWebEnabled, false);
final FlutterFeatureFlags flags = FlutterFeatureFlags(
flutterVersion: FakeFlutterVersion(),
featuresConfig: _FakeFeaturesConfig()..cannedResponse[exampleFeature] = true,
platform: FakePlatform(),
);
expect(flags.isEnabled(exampleFeature), false);
});
testWithoutContext('Flutter web enabled with config on master', () {
final FeatureFlags featureFlags = createFlags('master');
testConfig.setValue('enable-web', true);
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web enabled with environment variable on master', () {
final FeatureFlags featureFlags = createFlags('master');
platform.environment = <String, String>{'FLUTTER_WEB': 'true'};
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web off by default on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
expect(featureFlags.isWebEnabled, false);
});
testWithoutContext('Flutter web enabled with config on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
testConfig.setValue('enable-web', true);
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web not enabled with environment variable on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
platform.environment = <String, String>{'FLUTTER_WEB': 'true'};
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web on by default on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
testConfig.removeValue('enable-web');
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web enabled with config on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
testConfig.setValue('enable-web', true);
expect(featureFlags.isWebEnabled, true);
});
testWithoutContext('Flutter web not enabled with environment variable on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
platform.environment = <String, String>{'FLUTTER_WEB': 'enabled'};
expect(featureFlags.isWebEnabled, false);
});
/// Flutter macOS desktop.
testWithoutContext('Flutter macos desktop off by default on master', () {
final FeatureFlags featureFlags = createFlags('master');
expect(featureFlags.isMacOSEnabled, false);
});
testWithoutContext('Flutter macos desktop enabled with config on master', () {
final FeatureFlags featureFlags = createFlags('master');
testConfig.setValue('enable-macos-desktop', true);
expect(featureFlags.isMacOSEnabled, true);
});
testWithoutContext('Flutter macos desktop enabled with environment variable on master', () {
final FeatureFlags featureFlags = createFlags('master');
platform.environment = <String, String>{'FLUTTER_MACOS': 'true'};
expect(featureFlags.isMacOSEnabled, true);
});
testWithoutContext('Flutter macos desktop off by default on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
expect(featureFlags.isMacOSEnabled, false);
});
testWithoutContext('Flutter macos desktop enabled with config on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
testConfig.setValue('enable-macos-desktop', true);
expect(featureFlags.isMacOSEnabled, true);
});
testWithoutContext('Flutter macos desktop enabled with environment variable on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
platform.environment = <String, String>{'FLUTTER_MACOS': 'true'};
expect(featureFlags.isMacOSEnabled, true);
});
testWithoutContext('Flutter macos desktop off by default on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
expect(featureFlags.isMacOSEnabled, false);
});
testWithoutContext('Flutter macos desktop enabled with config on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
testConfig.setValue('enable-macos-desktop', true);
expect(featureFlags.isMacOSEnabled, true);
});
testWithoutContext('Flutter macos desktop enabled with environment variable on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
platform.environment = <String, String>{'FLUTTER_MACOS': 'true'};
expect(featureFlags.isMacOSEnabled, true);
});
/// Flutter Linux Desktop
testWithoutContext('Flutter linux desktop off by default on master', () {
final FeatureFlags featureFlags = createFlags('stable');
expect(featureFlags.isLinuxEnabled, false);
});
testWithoutContext('Flutter linux desktop enabled with config on master', () {
final FeatureFlags featureFlags = createFlags('master');
testConfig.setValue('enable-linux-desktop', true);
expect(featureFlags.isLinuxEnabled, true);
});
testWithoutContext('Flutter linux desktop enabled with environment variable on master', () {
final FeatureFlags featureFlags = createFlags('master');
platform.environment = <String, String>{'FLUTTER_LINUX': 'true'};
expect(featureFlags.isLinuxEnabled, true);
});
testWithoutContext('Flutter linux desktop off by default on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
expect(featureFlags.isLinuxEnabled, false);
});
testWithoutContext('Flutter linux desktop enabled with config on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
testConfig.setValue('enable-linux-desktop', true);
expect(featureFlags.isLinuxEnabled, true);
});
testWithoutContext('Flutter linux desktop enabled with environment variable on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
platform.environment = <String, String>{'FLUTTER_LINUX': 'true'};
expect(featureFlags.isLinuxEnabled, true);
});
testWithoutContext('Flutter linux desktop off by default on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
expect(featureFlags.isLinuxEnabled, false);
});
testWithoutContext('Flutter linux desktop enabled with config on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
testConfig.setValue('enable-linux-desktop', true);
expect(featureFlags.isLinuxEnabled, true);
});
testWithoutContext('Flutter linux desktop enabled with environment variable on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
platform.environment = <String, String>{'FLUTTER_LINUX': 'true'};
expect(featureFlags.isLinuxEnabled, true);
});
/// Flutter Windows desktop.
testWithoutContext('Flutter Windows desktop off by default on master', () {
final FeatureFlags featureFlags = createFlags('master');
expect(featureFlags.isWindowsEnabled, false);
});
testWithoutContext('Flutter Windows desktop enabled with config on master', () {
final FeatureFlags featureFlags = createFlags('master');
testConfig.setValue('enable-windows-desktop', true);
expect(featureFlags.isWindowsEnabled, true);
});
testWithoutContext('Flutter Windows desktop enabled with environment variable on master', () {
final FeatureFlags featureFlags = createFlags('master');
platform.environment = <String, String>{'FLUTTER_WINDOWS': 'true'};
expect(featureFlags.isWindowsEnabled, true);
});
testWithoutContext('Flutter Windows desktop off by default on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
expect(featureFlags.isWindowsEnabled, false);
});
testWithoutContext('Flutter Windows desktop enabled with config on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
testConfig.setValue('enable-windows-desktop', true);
expect(featureFlags.isWindowsEnabled, true);
});
testWithoutContext('Flutter Windows desktop enabled with environment variable on beta', () {
final FeatureFlags featureFlags = createFlags('beta');
platform.environment = <String, String>{'FLUTTER_WINDOWS': 'true'};
expect(featureFlags.isWindowsEnabled, true);
});
testWithoutContext('Flutter Windows desktop off by default on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
expect(featureFlags.isWindowsEnabled, false);
});
testWithoutContext('Flutter Windows desktop enabled with config on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
testConfig.setValue('enable-windows-desktop', true);
expect(featureFlags.isWindowsEnabled, true);
});
testWithoutContext('Flutter Windows desktop enabled with environment variable on stable', () {
final FeatureFlags featureFlags = createFlags('stable');
platform.environment = <String, String>{'FLUTTER_WINDOWS': 'true'};
expect(featureFlags.isWindowsEnabled, true);
});
for (final Feature feature in <Feature>[
flutterWindowsDesktopFeature,
flutterMacOSDesktopFeature,
flutterLinuxDesktopFeature,
]) {
test('${feature.name} available and enabled by default on master', () {
expect(feature.master.enabledByDefault, true);
expect(feature.master.available, true);
});
test('${feature.name} available and enabled by default on beta', () {
expect(feature.beta.enabledByDefault, true);
expect(feature.beta.available, true);
});
test('${feature.name} available and enabled by default on stable', () {
expect(feature.stable.enabledByDefault, true);
expect(feature.stable.available, true);
});
FileSystem createFsWithPubspec() {
final FileSystem fs = MemoryFileSystem.test();
fs.currentDirectory.childFile('pubspec.yaml').writeAsStringSync('''
flutter:
config:
enable-foo: true
enable-bar: false
enable-baz: true
''');
return fs;
}
// Custom devices on all channels
for (final String channel in <String>['master', 'beta', 'stable']) {
testWithoutContext('Custom devices are enabled with flag on $channel', () {
final FeatureFlags featureFlags = createFlags(channel);
testConfig.setValue('enable-custom-devices', true);
expect(featureFlags.areCustomDevicesEnabled, true);
});
testUsingContext(
'FeatureFlags is influenced by the CWD',
() {
// This test intentionally uses Context, as featureFlags is read that way at runtime.
final FeatureFlags featureFlagsFromContext = featureFlags;
testWithoutContext('Custom devices are enabled with environment variable on $channel', () {
final FeatureFlags featureFlags = createFlags(channel);
platform.environment = <String, String>{'FLUTTER_CUSTOM_DEVICES': 'true'};
expect(featureFlags.areCustomDevicesEnabled, true);
});
}
// Try a few flags that don't actually exist, but we want to check configuration more e2e-y.
expect(
featureFlagsFromContext.isEnabled(
const Feature(
name: 'foo',
configSetting: 'enable-foo',
master: FeatureChannelSetting(available: true),
),
),
isTrue,
reason: 'enable-foo: true is in pubspec.yaml',
);
test('${nativeAssets.name} availability and default enabled', () {
expect(nativeAssets.master.enabledByDefault, false);
expect(nativeAssets.master.available, true);
expect(nativeAssets.beta.enabledByDefault, false);
expect(nativeAssets.beta.available, false);
expect(nativeAssets.stable.enabledByDefault, false);
expect(nativeAssets.stable.available, false);
expect(
featureFlagsFromContext.isEnabled(
const Feature.fullyEnabled(name: 'bar', configSetting: 'enable-bar'),
),
isFalse,
reason: 'enable-bar: false is in pubspec.yaml',
);
expect(
featureFlagsFromContext.isEnabled(
const Feature(name: 'baz', configSetting: 'enable-baz'),
),
isFalse,
reason: 'Is not available',
);
},
overrides: <Type, Generator>{
ProcessManager: FakeProcessManager.empty,
FileSystem: createFsWithPubspec,
},
);
});
group('Linux Destkop', () {
test('is fully enabled', () {
expect(flutterLinuxDesktopFeature, _isFullyEnabled);
});
group('Swift Package Manager feature', () {
test('availability and default enabled', () {
expect(swiftPackageManager.master.enabledByDefault, false);
expect(swiftPackageManager.master.available, true);
expect(swiftPackageManager.beta.enabledByDefault, false);
expect(swiftPackageManager.beta.available, true);
expect(swiftPackageManager.stable.enabledByDefault, false);
expect(swiftPackageManager.stable.available, true);
});
test('can be configured', () {
expect(flutterLinuxDesktopFeature.configSetting, 'enable-linux-desktop');
expect(flutterLinuxDesktopFeature.environmentOverride, 'FLUTTER_LINUX');
});
test('can be enabled', () {
platform.environment = <String, String>{'FLUTTER_SWIFT_PACKAGE_MANAGER': 'true'};
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterLinuxDesktopFeature,
);
expect(checkFlags.isLinuxEnabled, isTrue);
});
});
expect(featureFlags.isSwiftPackageManagerEnabled, isTrue);
});
group('MacOS Desktop', () {
test('is fully enabled', () {
expect(flutterMacOSDesktopFeature, _isFullyEnabled);
});
test('can be configured', () {
expect(flutterMacOSDesktopFeature.configSetting, 'enable-macos-desktop');
expect(flutterMacOSDesktopFeature.environmentOverride, 'FLUTTER_MACOS');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterMacOSDesktopFeature,
);
expect(checkFlags.isMacOSEnabled, isTrue);
});
});
group('Windows Desktop', () {
test('is fully enabled', () {
expect(flutterWindowsDesktopFeature, _isFullyEnabled);
});
test('can be configured', () {
expect(flutterWindowsDesktopFeature.configSetting, 'enable-windows-desktop');
expect(flutterWindowsDesktopFeature.environmentOverride, 'FLUTTER_WINDOWS');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterWindowsDesktopFeature,
);
expect(checkFlags.isWindowsEnabled, isTrue);
});
});
group('Web', () {
test('is fully enabled', () {
expect(flutterWebFeature, _isFullyEnabled);
});
test('can be configured', () {
expect(flutterWebFeature.configSetting, 'enable-web');
expect(flutterWebFeature.environmentOverride, 'FLUTTER_WEB');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterWebFeature,
);
expect(checkFlags.isWebEnabled, isTrue);
});
});
group('Android', () {
test('is fully enabled', () {
expect(flutterAndroidFeature, _isFullyEnabled);
});
test('can be configured', () {
expect(flutterAndroidFeature.configSetting, 'enable-android');
expect(flutterAndroidFeature.environmentOverride, isNull);
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterAndroidFeature,
);
expect(checkFlags.isAndroidEnabled, isTrue);
});
});
group('iOS', () {
test('is fully enabled', () {
expect(flutterIOSFeature, _isFullyEnabled);
});
test('can be configured', () {
expect(flutterIOSFeature.configSetting, 'enable-ios');
expect(flutterIOSFeature.environmentOverride, isNull);
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterIOSFeature,
);
expect(checkFlags.isIOSEnabled, isTrue);
});
});
group('Fuchsia', () {
test('is only available on master', () {
expect(
flutterFuchsiaFeature,
allOf(<Matcher>[
_onChannelIs('master', available: true, enabledByDefault: false),
_onChannelIs('stable', available: false, enabledByDefault: false),
_onChannelIs('beta', available: false, enabledByDefault: false),
]),
);
});
test('can be configured', () {
expect(flutterFuchsiaFeature.configSetting, 'enable-fuchsia');
expect(flutterFuchsiaFeature.environmentOverride, 'FLUTTER_FUCHSIA');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterFuchsiaFeature,
);
expect(checkFlags.isFuchsiaEnabled, isTrue);
});
});
group('Custom Devices', () {
test('is always available but not enabled by default', () {
expect(
flutterCustomDevicesFeature,
allOf(<Matcher>[
_onChannelIs('master', available: true, enabledByDefault: false),
_onChannelIs('stable', available: true, enabledByDefault: false),
_onChannelIs('beta', available: true, enabledByDefault: false),
]),
);
});
test('can be configured', () {
expect(flutterCustomDevicesFeature.configSetting, 'enable-custom-devices');
expect(flutterCustomDevicesFeature.environmentOverride, 'FLUTTER_CUSTOM_DEVICES');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: flutterCustomDevicesFeature,
);
expect(checkFlags.areCustomDevicesEnabled, isTrue);
});
});
group('CLI Animations', () {
test('is always enabled', () {
expect(cliAnimation, _isFullyEnabled);
});
test('can be disabled by TERM=dumb', () {
final FlutterFeatureFlags features = FlutterFeatureFlags(
flutterVersion: FakeFlutterVersion(),
featuresConfig: _FakeFeaturesConfig(),
platform: FakePlatform(environment: <String, String>{'TERM': 'dumb'}),
);
expect(features.isCliAnimationEnabled, isFalse);
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: cliAnimation,
);
expect(checkFlags.isCliAnimationEnabled, isTrue);
});
});
group('Native Assets', () {
test('is available on master', () {
expect(
nativeAssets,
allOf(<Matcher>[
_onChannelIs('master', available: true, enabledByDefault: false),
_onChannelIs('stable', available: false, enabledByDefault: false),
_onChannelIs('beta', available: false, enabledByDefault: false),
]),
);
});
test('can be configured', () {
expect(nativeAssets.configSetting, 'enable-native-assets');
expect(nativeAssets.environmentOverride, 'FLUTTER_NATIVE_ASSETS');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: nativeAssets,
);
expect(checkFlags.isNativeAssetsEnabled, isTrue);
});
});
group('Swift Package Manager', () {
test('is available on all channels', () {
expect(
swiftPackageManager,
allOf(<Matcher>[
_onChannelIs('master', available: true, enabledByDefault: false),
_onChannelIs('stable', available: true, enabledByDefault: false),
_onChannelIs('beta', available: true, enabledByDefault: false),
]),
);
});
test('can be configured', () {
expect(swiftPackageManager.configSetting, 'enable-swift-package-manager');
expect(swiftPackageManager.environmentOverride, 'FLUTTER_SWIFT_PACKAGE_MANAGER');
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: swiftPackageManager,
);
expect(checkFlags.isSwiftPackageManagerEnabled, isTrue);
});
});
group('Explicit Package Dependencies', () {
test('is fully enabled', () {
expect(explicitPackageDependencies, _isFullyEnabled);
});
test('can be configured', () {
expect(explicitPackageDependencies.configSetting, 'explicit-package-dependencies');
expect(explicitPackageDependencies.environmentOverride, isNull);
});
test('forwards to isEnabled', () {
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
shouldInvoke: explicitPackageDependencies,
);
expect(checkFlags.isExplicitPackageDependenciesEnabled, isTrue);
});
});
}
final class _FakeFeaturesConfig implements FlutterFeaturesConfig {
final Map<Feature, bool?> cannedResponse = <Feature, bool?>{};
@override
bool? isEnabled(Feature feature) => cannedResponse[feature];
}
Matcher _onChannelIs(String channel, {required bool available, required bool enabledByDefault}) {
return _FeaturesMatcher(
channel: channel,
available: available,
enabledByDefault: enabledByDefault,
);
}
Matcher get _isFullyEnabled {
return allOf(const <_FeaturesMatcher>[
_FeaturesMatcher(channel: 'master', available: true, enabledByDefault: true),
_FeaturesMatcher(channel: 'stable', available: true, enabledByDefault: true),
_FeaturesMatcher(channel: 'beta', available: true, enabledByDefault: true),
]);
}
final class _FeaturesMatcher extends Matcher {
const _FeaturesMatcher({
required this.channel,
required this.available,
required this.enabledByDefault,
});
final String channel;
final bool available;
final bool enabledByDefault;
@override
Description describe(Description description) {
description = description.add('feature on the "$channel" channel ');
if (available) {
description = description.add('is available ');
} else {
description = description.add('is not available');
}
description = description.add('and is ');
if (enabledByDefault) {
description = description.add('is enabled by default');
} else {
description = description.add('is not enabled by default');
}
return description;
}
@override
bool matches(Object? item, Map<Object?, Object?> matchState) {
if (item is! Feature) {
return false;
}
final FeatureChannelSetting setting = switch (channel) {
'master' => item.master,
'stable' => item.stable,
'beta' => item.beta,
_ => throw StateError('Invalid channel: "$channel"'),
};
if (setting.available != available) {
return false;
}
if (setting.enabledByDefault != enabledByDefault) {
return false;
}
return true;
}
}
final class _TestIsGetterForwarding with FlutterFeatureFlagsIsEnabled {
_TestIsGetterForwarding({required this.shouldInvoke});
final Feature shouldInvoke;
@override
final Platform platform = FakePlatform();
@override
bool isEnabled(Feature feature) {
return feature == shouldInvoke;
}
}