mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This moves the tool's `allFeatures` global to `FeatureFlags.allFeatures`. In the future, this will be used by tests to mock the list of feature flags and replace them with test flags in various states. See: https://github.com/flutter/flutter/pull/168437 Part of: https://github.com/flutter/flutter/issues/167668 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
461 lines
14 KiB
Dart
461 lines
14 KiB
Dart
// 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: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', () {
|
|
testWithoutContext('setting has safe defaults', () {
|
|
const FeatureChannelSetting featureSetting = FeatureChannelSetting();
|
|
|
|
expect(featureSetting.available, false);
|
|
expect(featureSetting.enabledByDefault, false);
|
|
});
|
|
|
|
testWithoutContext('has safe defaults', () {
|
|
const Feature feature = Feature(name: 'example');
|
|
|
|
expect(feature.name, 'example');
|
|
expect(feature.environmentOverride, null);
|
|
expect(feature.configSetting, null);
|
|
});
|
|
|
|
testWithoutContext('retrieves the correct setting for each branch', () {
|
|
const FeatureChannelSetting masterSetting = FeatureChannelSetting(available: true);
|
|
const FeatureChannelSetting betaSetting = FeatureChannelSetting(available: true);
|
|
const FeatureChannelSetting stableSetting = FeatureChannelSetting(available: true);
|
|
const Feature feature = Feature(
|
|
name: 'example',
|
|
master: masterSetting,
|
|
beta: betaSetting,
|
|
stable: stableSetting,
|
|
);
|
|
|
|
expect(feature.getSettingForChannel('master'), masterSetting);
|
|
expect(feature.getSettingForChannel('beta'), betaSetting);
|
|
expect(feature.getSettingForChannel('stable'), stableSetting);
|
|
expect(feature.getSettingForChannel('unknown'), masterSetting);
|
|
});
|
|
|
|
testWithoutContext('reads from configuration if available', () {
|
|
const Feature exampleFeature = Feature(
|
|
name: 'example',
|
|
master: FeatureChannelSetting(available: true),
|
|
);
|
|
|
|
final FlutterFeatureFlags flags = FlutterFeatureFlags(
|
|
flutterVersion: FakeFlutterVersion(),
|
|
featuresConfig: _FakeFeaturesConfig()..cannedResponse[exampleFeature] = true,
|
|
platform: FakePlatform(),
|
|
);
|
|
expect(flags.isEnabled(exampleFeature), true);
|
|
});
|
|
|
|
testWithoutContext('returns false if not available', () {
|
|
const Feature exampleFeature = Feature(name: 'example');
|
|
|
|
final FlutterFeatureFlags flags = FlutterFeatureFlags(
|
|
flutterVersion: FakeFlutterVersion(),
|
|
featuresConfig: _FakeFeaturesConfig()..cannedResponse[exampleFeature] = true,
|
|
platform: FakePlatform(),
|
|
);
|
|
expect(flags.isEnabled(exampleFeature), false);
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
testUsingContext(
|
|
'FeatureFlags is influenced by the CWD',
|
|
() {
|
|
// This test intentionally uses Context, as featureFlags is read that way at runtime.
|
|
final FeatureFlags featureFlagsFromContext = featureFlags;
|
|
|
|
// 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',
|
|
);
|
|
|
|
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,
|
|
},
|
|
);
|
|
|
|
testUsingContext('Test feature flags match feature flags', () {
|
|
final FeatureFlags testFeatureFlags = TestFeatureFlags();
|
|
|
|
expect(featureFlags.allFeatures.length, equals(testFeatureFlags.allFeatures.length));
|
|
|
|
final List<String> featureNames =
|
|
featureFlags.allFeatures.map((Feature feature) => feature.name).toList();
|
|
final List<String> testFeatureNames =
|
|
testFeatureFlags.allFeatures.map((Feature feature) => feature.name).toList();
|
|
|
|
expect(featureNames, unorderedEquals(testFeatureNames));
|
|
});
|
|
});
|
|
|
|
group('Linux Destkop', () {
|
|
test('is fully enabled', () {
|
|
expect(flutterLinuxDesktopFeature, _isFullyEnabled);
|
|
});
|
|
|
|
test('can be configured', () {
|
|
expect(flutterLinuxDesktopFeature.configSetting, 'enable-linux-desktop');
|
|
expect(flutterLinuxDesktopFeature.environmentOverride, 'FLUTTER_LINUX');
|
|
});
|
|
|
|
test('forwards to isEnabled', () {
|
|
final _TestIsGetterForwarding checkFlags = _TestIsGetterForwarding(
|
|
shouldInvoke: flutterLinuxDesktopFeature,
|
|
);
|
|
expect(checkFlags.isLinuxEnabled, 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: true),
|
|
_onChannelIs('stable', available: false, enabledByDefault: false),
|
|
_onChannelIs('beta', available: true, enabledByDefault: true),
|
|
]),
|
|
);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
@override
|
|
List<Feature> get allFeatures => throw UnimplementedError();
|
|
}
|