blob: 8004fdaae8381025f0e8b9538c81e8d067be8290 [file]
// 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 'dart:convert';
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/experimental/extension_manager.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools_core/flutter_tools_core.dart';
import 'package:flutter_tools_extension/flutter_tools_extension.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
import '../../src/fakes.dart' as fakes;
import '../../src/test_flutter_command_runner.dart';
void main() {
late Java fakeJava;
late FakeAndroidStudio fakeAndroidStudio;
late FakeAndroidSdk fakeAndroidSdk;
late FakeFlutterVersion fakeFlutterVersion;
late FakeAnalytics fakeAnalytics;
late MemoryFileSystem fs;
late Config defaultConfig;
late BufferLogger testLogger;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
fs = MemoryFileSystem.test();
defaultConfig = Config.test(directory: fs.directory('/'));
testLogger = BufferLogger.test();
fakeJava = fakes.FakeJava(javaHome: 'path/to/jdk');
fakeAndroidStudio = FakeAndroidStudio();
fakeAndroidSdk = FakeAndroidSdk(fileSystem: fs);
fakeFlutterVersion = FakeFlutterVersion();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: fakes.FakeFlutterVersion(),
);
});
ConfigCommand createConfigCommand({
Config? config,
Logger? logger,
Platform? platform,
FileSystem? fileSystem,
ProcessManager? processManager,
ProcessUtils? processUtils,
AnsiTerminal? terminal,
FlutterVersion? flutterVersion,
AndroidSdk? androidSdk,
AndroidStudio? androidStudio,
Java? java,
FeatureFlags? featureFlags,
bool verboseHelp = false,
ExtensionManager? extensionManager,
}) {
final FileSystem resolvedFs = fileSystem ?? fs;
final Logger resolvedLogger = logger ?? testLogger;
final Platform resolvedPlatform = platform ?? FakePlatform();
final ProcessManager resolvedProcessManager = processManager ?? FakeProcessManager.any();
return ConfigCommand(
verboseHelp: verboseHelp,
featureFlags: featureFlags ?? TestFeatureFlags(),
extensionManager: extensionManager,
androidContext: FakeAndroidContext(
androidSdk: androidSdk,
androidStudio: androidStudio,
java: java,
),
toolContext: FakeToolContext(
config:
config ??
(fileSystem != null
? Config.test(directory: resolvedFs.directory('/'))
: defaultConfig),
logger: resolvedLogger,
flutterVersion: flutterVersion ?? fakeFlutterVersion,
platform: resolvedPlatform,
fs: resolvedFs,
processManager: resolvedProcessManager,
processUtils:
processUtils ??
ProcessUtils(logger: resolvedLogger, processManager: resolvedProcessManager),
terminal: terminal ?? AnsiTerminal(stdio: FakeStdio(), platform: resolvedPlatform),
),
);
}
CommandRunner<void> createRunner(ConfigCommand command, [Analytics? analytics]) {
return createTestCommandRunner(command, analytics ?? fakeAnalytics);
}
group('config', () {
testWithoutContext('machine flag displays values in json format', () async {
final ConfigCommand configCommand = createConfigCommand(
androidStudio: fakeAndroidStudio,
androidSdk: fakeAndroidSdk,
java: fakeJava,
);
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--machine']);
expect(
json.decode(testLogger.statusText),
equals(<String, Object?>{
'android-studio-dir': 'path/to/android/studio',
'android-sdk': 'path/to/android/sdk',
'jdk-dir': 'path/to/jdk',
}),
);
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('machine flag outputs empty json when tools are not found', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--machine']);
expect(json.decode(testLogger.statusText), equals(<String, Object?>{}));
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('machine flag includes custom values in output', () async {
final ConfigCommand configCommand = createConfigCommand(
androidStudio: fakeAndroidStudio,
androidSdk: fakeAndroidSdk,
java: fakeJava,
);
final CommandRunner<void> commandRunner = createRunner(configCommand);
defaultConfig.setValue('android-studio-dir', 'dummy/dir/to/android/studio');
defaultConfig.setValue('android-sdk', 'dummy/dir/to/android/sdk');
defaultConfig.setValue('jdk-dir', 'dummy/dir/to/jdk');
await commandRunner.run(<String>['config', '--machine']);
expect(
json.decode(testLogger.statusText),
equals(<String, Object?>{
'android-studio-dir': 'dummy/dir/to/android/studio',
'android-sdk': 'dummy/dir/to/android/sdk',
'jdk-dir': 'dummy/dir/to/jdk',
}),
);
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('Can set build-dir', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--build-dir=foo']);
expect(defaultConfig.getValue('build-dir'), 'foo');
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('throws error on absolute path to build-dir', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
expect(() => commandRunner.run(<String>['config', '--build-dir=/foo']), throwsToolExit());
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('allows setting and removing feature flags', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>[
'config',
'--enable-android',
'--enable-ios',
'--enable-web',
'--enable-linux-desktop',
'--enable-windows-desktop',
'--enable-macos-desktop',
]);
expect(defaultConfig.getValue('enable-android'), true);
expect(defaultConfig.getValue('enable-ios'), true);
expect(defaultConfig.getValue('enable-web'), true);
expect(defaultConfig.getValue('enable-linux-desktop'), true);
expect(defaultConfig.getValue('enable-windows-desktop'), true);
expect(defaultConfig.getValue('enable-macos-desktop'), true);
await commandRunner.run(<String>[
'config',
'--no-enable-android',
'--no-enable-ios',
'--no-enable-web',
'--no-enable-linux-desktop',
'--no-enable-windows-desktop',
'--no-enable-macos-desktop',
]);
expect(defaultConfig.getValue('enable-android'), false);
expect(defaultConfig.getValue('enable-ios'), false);
expect(defaultConfig.getValue('enable-web'), false);
expect(defaultConfig.getValue('enable-linux-desktop'), false);
expect(defaultConfig.getValue('enable-windows-desktop'), false);
expect(defaultConfig.getValue('enable-macos-desktop'), false);
await commandRunner.run(<String>['config', '--clear-features']);
expect(defaultConfig.getValue('enable-android'), null);
expect(defaultConfig.getValue('enable-ios'), null);
expect(defaultConfig.getValue('enable-web'), null);
expect(defaultConfig.getValue('enable-linux-desktop'), null);
expect(defaultConfig.getValue('enable-windows-desktop'), null);
expect(defaultConfig.getValue('enable-macos-desktop'), null);
expect(
testLogger.statusText,
contains('You may need to restart any open editors for them to read new settings.'),
);
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('displays notice when setting is changed', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--enable-web']);
expect(
testLogger.statusText,
containsIgnoringWhitespace('You may need to restart any open editors'),
);
});
testWithoutContext('warns when Swift Package Manager is disabled', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--no-enable-swift-package-manager']);
expect(testLogger.warningText, contains(kSwiftPackageManagerDisabledWarning));
});
testWithoutContext('displays which config settings are available on stable', () async {
fakeFlutterVersion.channel = 'stable';
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>[
'config',
'--enable-web',
'--enable-linux-desktop',
'--enable-windows-desktop',
'--enable-macos-desktop',
]);
await commandRunner.run(<String>['config', '--list']);
expect(testLogger.statusText, containsIgnoringWhitespace('enable-web: true'));
expect(testLogger.statusText, containsIgnoringWhitespace('enable-linux-desktop: true'));
expect(testLogger.statusText, containsIgnoringWhitespace('enable-windows-desktop: true'));
expect(testLogger.statusText, containsIgnoringWhitespace('enable-macos-desktop: true'));
expect(fakeAnalytics.sentEvents, isEmpty);
});
testWithoutContext('analytics flag enables/disables analytics', () async {
final ConfigCommand configCommand = createConfigCommand();
final CommandRunner<void> commandRunner = createRunner(configCommand);
expect(fakeAnalytics.telemetryEnabled, true);
await commandRunner.run(<String>['config', '--no-analytics']);
expect(fakeAnalytics.telemetryEnabled, false);
await commandRunner.run(<String>['config', '--analytics']);
expect(fakeAnalytics.telemetryEnabled, true);
});
testWithoutContext('analytics reported with help usages', () async {
final ConfigCommand configCommand = createConfigCommand();
createRunner(configCommand);
await fakeAnalytics.setTelemetry(false);
expect(
configCommand.usage,
containsIgnoringWhitespace('Analytics reporting is currently disabled'),
);
await fakeAnalytics.setTelemetry(true);
expect(
configCommand.usage,
containsIgnoringWhitespace('Analytics reporting is currently enabled'),
);
});
testWithoutContext('resolves dependencies from injected ToolContext on write', () async {
final fakeInjectedConfig = Config.test(name: 'injected', directory: fs.directory('/'));
final fakeInjectedLogger = BufferLogger.test();
final ConfigCommand configCommand = createConfigCommand(
config: fakeInjectedConfig,
logger: fakeInjectedLogger,
);
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--enable-web']);
expect(fakeInjectedConfig.getValue('enable-web'), true);
expect(defaultConfig.getValue('enable-web'), isNull);
expect(fakeInjectedLogger.statusText, contains('Setting "enable-web" value to "true"'));
expect(testLogger.statusText, isNot(contains('Setting "enable-web" value to "true"')));
});
testWithoutContext('resolves dependencies from injected ToolContext on read', () async {
final fakeLocalConfig = FakeConfig();
final fakeLocalLogger = BufferLogger.test();
final ConfigCommand configCommand = createConfigCommand(
config: fakeLocalConfig,
logger: fakeLocalLogger,
);
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--list']);
expect(fakeLocalLogger.statusText, contains('All Settings:'));
expect(fakeLocalConfig.keysQueried, isTrue);
});
testWithoutContext(
'custom configuration options and feature flags are registered in parser',
() async {
final mockExtension = FakeConfigurationExtension(
title: 'My Extension',
featureFlags: <FeatureFlag>[
const FeatureFlag(
name: 'my-ext-feature',
help: 'My extension feature help',
enabledByDefault: true,
),
],
configOptions: <ConfigOption>[
const ConfigOption(
name: 'my-ext-option',
help: 'My extension option help',
value: 'default-value',
),
],
);
final fakeExtensionManager = FakeExtensionManager(
extensions: <ConfigurationExtension>[mockExtension],
);
final ConfigCommand configCommand = createConfigCommand(
extensionManager: fakeExtensionManager,
featureFlags: fakes.TestFeatureFlags(isToolExtensionsEnabled: true),
);
createRunner(configCommand);
await configCommand.initializeDynamicOptions();
final ArgParser parser = configCommand.argParser;
expect(parser.options, contains('my-ext-feature'));
expect(parser.options, contains('my-ext-option'));
expect(parser.options['my-ext-feature']!.isFlag, true);
expect(parser.options['my-ext-feature']!.help, 'My extension feature help');
expect(parser.options['my-ext-feature']!.defaultsTo, true);
expect(parser.options['my-ext-option']!.isFlag, false);
expect(parser.options['my-ext-option']!.help, 'My extension option help');
expect(parser.options['my-ext-option']!.defaultsTo, 'default-value');
},
);
testWithoutContext(
'custom configuration options and feature flags can be set/cleared via runCommand',
() async {
final mockExtension = FakeConfigurationExtension(
title: 'My Extension',
featureFlags: <FeatureFlag>[
const FeatureFlag(name: 'my-ext-feature', help: 'My extension feature help'),
],
configOptions: <ConfigOption>[
const ConfigOption(name: 'my-ext-option', help: 'My extension option help'),
],
);
final fakeExtensionManager = FakeExtensionManager(
extensions: <ConfigurationExtension>[mockExtension],
);
final ConfigCommand configCommand = createConfigCommand(
extensionManager: fakeExtensionManager,
featureFlags: fakes.TestFeatureFlags(isToolExtensionsEnabled: true),
);
await configCommand.initializeDynamicOptions();
final CommandRunner<void> commandRunner = createRunner(configCommand);
await commandRunner.run(<String>['config', '--my-ext-feature', '--my-ext-option=my-value']);
expect(defaultConfig.getValue('my-ext-feature'), true);
expect(defaultConfig.getValue('my-ext-option'), 'my-value');
// Test disabling flag
await commandRunner.run(<String>['config', '--no-my-ext-feature']);
expect(defaultConfig.getValue('my-ext-feature'), false);
await commandRunner.run(<String>['config', '--clear-features']);
expect(defaultConfig.getValue('my-ext-feature'), null);
await commandRunner.run(<String>['config', '--my-ext-option=']);
expect(defaultConfig.getValue('my-ext-option'), null);
},
);
testWithoutContext(
'conflicting extension options and feature flags are skipped without throwing',
() async {
final mockExtension = FakeConfigurationExtension(
title: 'Conflicting Extension',
featureFlags: <FeatureFlag>[
const FeatureFlag(name: 'analytics', help: 'Conflicting analytics flag'),
],
configOptions: <ConfigOption>[
const ConfigOption(name: 'android-sdk', help: 'Conflicting android-sdk option'),
],
);
final fakeExtensionManager = FakeExtensionManager(
extensions: <ConfigurationExtension>[mockExtension],
);
final ConfigCommand configCommand = createConfigCommand(
extensionManager: fakeExtensionManager,
featureFlags: fakes.TestFeatureFlags(isToolExtensionsEnabled: true),
);
await configCommand.initializeDynamicOptions();
final ArgParser parser = configCommand.argParser;
expect(parser.options.containsKey('analytics'), isTrue);
expect(parser.options.containsKey('android-sdk'), isTrue);
},
);
});
}
class FakeAndroidStudio extends Fake implements AndroidStudio, Comparable<AndroidStudio> {
@override
String get directory => 'path/to/android/studio';
@override
String? get javaPath => 'path/to/android/studio/jbr';
}
class FakeAndroidSdk extends Fake implements AndroidSdk {
FakeAndroidSdk({required this.fileSystem});
final FileSystem fileSystem;
@override
Directory get directory => fileSystem.directory('path/to/android/sdk');
}
class FakeFlutterVersion extends Fake implements FlutterVersion {
@override
String channel = 'stable';
@override
void ensureVersionFile() {}
@override
Future<void> checkFlutterVersionFreshness() async {}
}
class FakeConfig extends Fake implements Config {
bool keysQueried = false;
@override
Iterable<String> get keys {
keysQueried = true;
return const <String>[];
}
@override
Object? getValue(String key) => null;
}
class FakeExtensionManager extends Fake implements ExtensionManager {
FakeExtensionManager({this.extensions = const <ConfigurationExtension>[]});
final List<ConfigurationExtension> extensions;
@override
List<ConfigurationExtension> get configurationExtensions => extensions;
@override
Future<void> ensureInitialized() async {}
}
class FakeConfigurationExtension extends Fake implements ConfigurationExtension {
FakeConfigurationExtension({
required this.title,
required this.featureFlags,
required this.configOptions,
});
@override
final String title;
final List<FeatureFlag> featureFlags;
final List<ConfigOption> configOptions;
@override
Future<List<FeatureFlag>> getFeatureFlags() async => featureFlags;
@override
Future<List<ConfigOption>> getConfigurations() async => configOptions;
}