diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 78c74f80175..63c3bc68bf0 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -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 runInContext(FutureOr Function() runner, {Map? 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!), diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 3b9b21cfe0b..fed0b16ec70 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -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 '; + } } diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index ff83f640089..162b04aa83d 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -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; } } diff --git a/packages/flutter_tools/lib/src/flutter_features_config.dart b/packages/flutter_tools/lib/src/flutter_features_config.dart new file mode 100644 index 00000000000..b0883243557 --- /dev/null +++ b/packages/flutter_tools/lib/src/flutter_features_config.dart @@ -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})', + ); + } +} diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index 0fe600cc3a0..8024761f861 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart @@ -608,6 +608,9 @@ void _validateFlutter(YamlMap? yaml, List 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': diff --git a/packages/flutter_tools/test/general.shard/features_config_test.dart b/packages/flutter_tools/test/general.shard/features_config_test.dart new file mode 100644 index 00000000000..6bb1414b9fb --- /dev/null +++ b/packages/flutter_tools/test/general.shard/features_config_test.dart @@ -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 environment = const {}, + Map globalConfig = const {}, + String? projectManifest, + }) { + final Config globalConfigReader = Config.test(); + for (final MapEntry(: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: {...environment}), + projectManifest: flutterManifest, + ); + return featuresConfig.isEnabled(feature); + } + + test('returns null if cannot be overriden', () { + expect( + isEnabled( + _noConfigFeature, + environment: {'ENABLE_FLAG': 'true'}, + globalConfig: {'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: {'enable-flag': true}), + isTrue, + ); + }); + + test('global configuration value must be a boolean', () { + expect( + () => isEnabled( + _configOnlyFeature, + globalConfig: {'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: {'enable-flag': false}, + ), + isTrue, + ); + }); + + test('overrides from environment', () { + expect( + isEnabled(_envOnlyFeature, environment: {'ENABLE_FLAG': 'true'}), + isTrue, + ); + }); + + test('overrides from environment are case insensitive', () { + expect( + isEnabled(_envOnlyFeature, environment: {'ENABLE_FLAG': 'tRuE'}), + isTrue, + ); + }); + + test('overrides from environment are lowest priority', () { + expect( + isEnabled( + _configAndEnvFeature, + environment: {'ENABLE_FLAG': 'true'}, + globalConfig: {'enable-flag': false}, + ), + isFalse, + ); + }); +} diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart index 10d8fe3e30b..b3f39cc1791 100644 --- a/packages/flutter_tools/test/general.shard/features_test.dart +++ b/packages/flutter_tools/test/general.shard/features_test.dart @@ -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: {}); - - 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 = {'FLUTTER_WEB': 'hello'}; - - expect(featureFlags.isWebEnabled, false); - - platform.environment = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'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 = {'FLUTTER_WINDOWS': 'true'}; - - expect(featureFlags.isWindowsEnabled, true); - }); - - for (final Feature feature in [ - 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 ['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 = {'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: { + 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 = {'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([ + _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([ + _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: {'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([ + _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([ + _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 cannedResponse = {}; + + @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 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; + } +}