// 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:async'; import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/error_handling_io.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/run.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/pre_run_validator.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/testing.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_devices.dart'; import '../../src/fakes.dart'; import '../../src/test_flutter_command_runner.dart'; import 'utils.dart'; void main() { group('Flutter Command', () { late FakeCache cache; late TestUsage usage; late FakeAnalytics fakeAnalytics; late FakeClock clock; late FakeProcessInfo processInfo; late MemoryFileSystem fileSystem; late Platform platform; late FileSystemUtils fileSystemUtils; late Logger logger; late FakeProcessManager processManager; late PreRunValidator preRunValidator; setUpAll(() { Cache.flutterRoot = '/path/to/sdk/flutter'; }); setUp(() { Cache.disableLocking(); cache = FakeCache(); usage = TestUsage(); clock = FakeClock(); processInfo = FakeProcessInfo(); processInfo.maxRss = 10; fileSystem = MemoryFileSystem.test(); platform = FakePlatform(); fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform); logger = BufferLogger.test(); processManager = FakeProcessManager.empty(); preRunValidator = PreRunValidator(fileSystem: fileSystem); fakeAnalytics = getInitializedFakeAnalyticsInstance( fs: fileSystem, fakeFlutterVersion: FakeFlutterVersion(), ); }); tearDown(() { Cache.enableLocking(); }); testUsingContext('help text contains global options', () { final FakeDeprecatedCommand fake = FakeDeprecatedCommand(); createTestCommandRunner(fake); expect(fake.usage, contains('Global options:\n')); }); testUsingContext('honors shouldUpdateCache false', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); await flutterCommand.run(); expect(cache.artifacts, isEmpty); expect(flutterCommand.deprecated, isFalse); expect(flutterCommand.hidden, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, Cache: () => cache, }); testUsingContext('honors shouldUpdateCache true', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true); await flutterCommand.run(); // First call for universal, second for the rest expect( cache.artifacts, >[ {DevelopmentArtifact.universal}, {}, ], ); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, Cache: () => cache, }); testUsingContext("throws toolExit if flutter_tools source dir doesn't exist", () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); await expectToolExitLater( flutterCommand.run(), contains('Flutter SDK installation appears corrupted'), ); }, overrides: { Cache: () => cache, FileSystem: () => fileSystem, PreRunValidator: () => preRunValidator, ProcessManager: () => processManager, }); testUsingContext('deprecated command should warn', () async { final FakeDeprecatedCommand flutterCommand = FakeDeprecatedCommand(); final CommandRunner runner = createTestCommandRunner(flutterCommand); await runner.run(['deprecated']); expect(testLogger.warningText, contains('The "deprecated" command is deprecated and will be removed in ' 'a future version of Flutter.')); expect(flutterCommand.usage, contains('Deprecated. This command will be removed in a future version ' 'of Flutter.')); expect(flutterCommand.deprecated, isTrue); expect(flutterCommand.hidden, isTrue); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('uses the error handling file system', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async { expect(globals.fs, isA()); return const FlutterCommandResult(ExitStatus.success); } ); await flutterCommand.run(); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('finds the target file with default values', () async { globals.fs.file('lib/main.dart').createSync(recursive: true); final FakeTargetCommand fakeTargetCommand = FakeTargetCommand(); final CommandRunner runner = createTestCommandRunner(fakeTargetCommand); await runner.run(['test']); expect(fakeTargetCommand.cachedTargetFile, 'lib/main.dart'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('finds the target file with specified value', () async { globals.fs.file('lib/foo.dart').createSync(recursive: true); final FakeTargetCommand fakeTargetCommand = FakeTargetCommand(); final CommandRunner runner = createTestCommandRunner(fakeTargetCommand); await runner.run(['test', '-t', 'lib/foo.dart']); expect(fakeTargetCommand.cachedTargetFile, 'lib/foo.dart'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('throws tool exit if specified file does not exist', () async { final FakeTargetCommand fakeTargetCommand = FakeTargetCommand(); final CommandRunner runner = createTestCommandRunner(fakeTargetCommand); expect(() async => runner.run(['test', '-t', 'lib/foo.dart']), throwsToolExit()); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); void testUsingCommandContext(String testName, dynamic Function() testBody) { testUsingContext(testName, testBody, overrides: { FileSystem: () => fileSystem, ProcessInfo: () => processInfo, ProcessManager: () => processManager, SystemClock: () => clock, Usage: () => usage, Analytics: () => fakeAnalytics, }); } testUsingCommandContext('reports command that results in success', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.success); } ); await flutterCommand.run(); expect(usage.events, [ const TestUsageEvent( 'tool-command-result', 'dummy', label: 'success', ), const TestUsageEvent( 'tool-command-max-rss', 'dummy', label: 'success', value: 10, ), ]); expect(fakeAnalytics.sentEvents, contains( Event.flutterCommandResult( commandPath: 'dummy', result: 'success', maxRss: 10, commandHasTerminal: false, ), )); }); testUsingCommandContext('reports command that results in warning', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async { return const FlutterCommandResult(ExitStatus.warning); } ); await flutterCommand.run(); expect(usage.events, [ const TestUsageEvent( 'tool-command-result', 'dummy', label: 'warning', ), const TestUsageEvent( 'tool-command-max-rss', 'dummy', label: 'warning', value: 10, ), ]); expect(fakeAnalytics.sentEvents, contains( Event.flutterCommandResult( commandPath: 'dummy', result: 'warning', maxRss: 10, commandHasTerminal: false, ), )); }); testUsingCommandContext('reports command that results in error', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async { throwToolExit('fail'); }, ); await expectLater( () => flutterCommand.run(), throwsToolExit(), ); expect(usage.events, [ const TestUsageEvent( 'tool-command-result', 'dummy', label: 'fail', ), const TestUsageEvent( 'tool-command-max-rss', 'dummy', label: 'fail', value: 10, ), ]); expect(fakeAnalytics.sentEvents, contains( Event.flutterCommandResult( commandPath: 'dummy', result: 'fail', maxRss: 10, commandHasTerminal: false, ), )); }); test('FlutterCommandResult.success()', () async { expect(FlutterCommandResult.success().exitStatus, ExitStatus.success); }); test('FlutterCommandResult.warning()', () async { expect(FlutterCommandResult.warning().exitStatus, ExitStatus.warning); }); testUsingContext('devToolsServerAddress returns parsed uri', () async { final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions(verboseHelp: false); await createTestCommandRunner(command).run([ 'dummy', '--${FlutterCommand.kDevToolsServerAddress}', 'http://127.0.0.1:9105', ]); expect(command.devToolsServerAddress.toString(), equals('http://127.0.0.1:9105')); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('devToolsServerAddress returns null for bad input', () async { final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions(verboseHelp: false); final CommandRunner runner = createTestCommandRunner(command); await runner.run([ 'dummy', '--${FlutterCommand.kDevToolsServerAddress}', 'hello-world', ]); expect(command.devToolsServerAddress, isNull); await runner.run([ 'dummy', '--${FlutterCommand.kDevToolsServerAddress}', '', ]); expect(command.devToolsServerAddress, isNull); await runner.run([ 'dummy', '--${FlutterCommand.kDevToolsServerAddress}', '9101', ]); expect(command.devToolsServerAddress, isNull); await runner.run([ 'dummy', '--${FlutterCommand.kDevToolsServerAddress}', '127.0.0.1:9101', ]); expect(command.devToolsServerAddress, isNull); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); group('signals tests', () { late FakeIoProcessSignal mockSignal; late ProcessSignal signalUnderTest; late StreamController signalController; setUp(() { mockSignal = FakeIoProcessSignal(); signalUnderTest = ProcessSignal(mockSignal); signalController = StreamController(); mockSignal.stream = signalController.stream; }); testUsingContext('reports command that is killed', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final Completer completer = Completer(); setExitFunctionForTests((int exitCode) { expect(exitCode, 0); restoreExitFunction(); completer.complete(); }); final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async { final Completer c = Completer(); await c.future; throw UnsupportedError('Unreachable'); } ); unawaited(flutterCommand.run()); signalController.add(mockSignal); await completer.future; expect(usage.events, [ const TestUsageEvent( 'tool-command-result', 'dummy', label: 'killed', ), const TestUsageEvent( 'tool-command-max-rss', 'dummy', label: 'killed', value: 10, ), ]); expect(fakeAnalytics.sentEvents, contains( Event.flutterCommandResult( commandPath: 'dummy', result: 'killed', maxRss: 10, commandHasTerminal: false, ), )); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, ProcessInfo: () => processInfo, Signals: () => FakeSignals( subForSigTerm: signalUnderTest, exitSignals: [signalUnderTest], ), SystemClock: () => clock, Usage: () => usage, Analytics: () => fakeAnalytics, }); testUsingContext('command release lock on kill signal', () async { clock.times = [1000, 2000]; final Completer completer = Completer(); setExitFunctionForTests((int exitCode) { expect(exitCode, 0); restoreExitFunction(); completer.complete(); }); final Completer checkLockCompleter = Completer(); final DummyFlutterCommand flutterCommand = DummyFlutterCommand(commandFunction: () async { await globals.cache.lock(); checkLockCompleter.complete(); final Completer c = Completer(); await c.future; throw UnsupportedError('Unreachable'); }); unawaited(flutterCommand.run()); await checkLockCompleter.future; globals.cache.checkLockAcquired(); signalController.add(mockSignal); await completer.future; }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, ProcessInfo: () => processInfo, Signals: () => FakeSignals( subForSigTerm: signalUnderTest, exitSignals: [signalUnderTest], ), Usage: () => usage, }); }); testUsingCommandContext('report execution timing by default', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); await flutterCommand.run(); expect(usage.timings, contains( const TestTimingEvent( 'flutter', 'dummy', Duration(milliseconds: 1000), label: 'fail', ))); expect(fakeAnalytics.sentEvents, contains( Event.timing( workflow: 'flutter', variableName: 'dummy', elapsedMilliseconds: 1000, label: 'fail', ) )); }); testUsingCommandContext('no timing report without usagePath', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final DummyFlutterCommand flutterCommand = DummyFlutterCommand(noUsagePath: true); await flutterCommand.run(); expect(usage.timings, isEmpty); // Iterate through and count all the [Event.timing] instances int timingEventCounts = 0; for (final Event e in fakeAnalytics.sentEvents) { if (e.eventName == DashEvent.timing) { timingEventCounts += 1; } } expect( timingEventCounts, 0, reason: 'There should not be any timing events sent, there may ' 'be other non-timing events', ); }); testUsingCommandContext('report additional FlutterCommandResult data', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final FlutterCommandResult commandResult = FlutterCommandResult( ExitStatus.success, // nulls should be cleaned up. timingLabelParts: ['blah1', 'blah2', null, 'blah3'], endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500), ); final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async => commandResult ); await flutterCommand.run(); expect(usage.timings, contains( const TestTimingEvent( 'flutter', 'dummy', Duration(milliseconds: 500), label: 'success-blah1-blah2-blah3', ))); expect(fakeAnalytics.sentEvents, contains( Event.timing( workflow: 'flutter', variableName: 'dummy', elapsedMilliseconds: 500, label: 'success-blah1-blah2-blah3', ), )); }); testUsingCommandContext('report failed execution timing too', () async { // Crash if called a third time which is unexpected. clock.times = [1000, 2000]; final DummyFlutterCommand flutterCommand = DummyFlutterCommand( commandFunction: () async { throwToolExit('fail'); }, ); await expectLater( () => flutterCommand.run(), throwsToolExit(), ); expect(usage.timings, contains( const TestTimingEvent( 'flutter', 'dummy', Duration(milliseconds: 1000), label: 'fail', ), )); expect(fakeAnalytics.sentEvents, contains( Event.timing( workflow: 'flutter', variableName: 'dummy', elapsedMilliseconds: 1000, label: 'fail', ), )); }); testUsingContext('reports null safety analytics when reportNullSafety is true', () async { globals.fs.file('lib/main.dart') ..createSync(recursive: true) ..writeAsStringSync('// @dart=2.12'); globals.fs.file('pubspec.yaml') .writeAsStringSync('name: example\n'); globals.fs.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync(r''' { "configVersion": 2, "packages": [ { "name": "example", "rootUri": "../", "packageUri": "lib/", "languageVersion": "2.12" } ], "generated": "2020-12-02T19:30:53.862346Z", "generator": "pub", "generatorVersion": "2.12.0-76.0.dev" } '''); final FakeReportingNullSafetyCommand command = FakeReportingNullSafetyCommand(); final CommandRunner runner = createTestCommandRunner(command); await runner.run(['test']); expect(usage.events, containsAll([ const TestUsageEvent( NullSafetyAnalysisEvent.kNullSafetyCategory, 'runtime-mode', label: 'NullSafetyMode.sound', ), TestUsageEvent( NullSafetyAnalysisEvent.kNullSafetyCategory, 'stats', parameters: CustomDimensions.fromMap({ 'cd49': '1', 'cd50': '1', }), ), const TestUsageEvent( NullSafetyAnalysisEvent.kNullSafetyCategory, 'language-version', label: '2.12', ), ])); }, overrides: { Pub: () => FakePub(), Usage: () => usage, FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('use packagesPath to generate BuildInfo', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo'); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.packageConfigPath, 'foo'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('use fileSystemScheme to generate BuildInfo', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemScheme: 'foo'); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.fileSystemScheme, 'foo'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('use fileSystemRoots to generate BuildInfo', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemRoots: ['foo', 'bar']); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.fileSystemRoots, ['foo', 'bar']); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('includes initializeFromDill in BuildInfo', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand()..usesInitializeFromDillOption(hide: false); final CommandRunner runner = createTestCommandRunner(flutterCommand); await runner.run(['dummy', '--initialize-from-dill=/foo/bar.dill']); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.initializeFromDill, '/foo/bar.dill'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('includes assumeInitializeFromDillUpToDate in BuildInfo', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand()..usesInitializeFromDillOption(hide: false); final CommandRunner runner = createTestCommandRunner(flutterCommand); await runner.run(['dummy', '--assume-initialize-from-dill-up-to-date']); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.assumeInitializeFromDillUpToDate, isTrue); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('unsets assumeInitializeFromDillUpToDate in BuildInfo when disabled', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand()..usesInitializeFromDillOption(hide: false); final CommandRunner runner = createTestCommandRunner(flutterCommand); await runner.run(['dummy', '--no-assume-initialize-from-dill-up-to-date']); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.assumeInitializeFromDillUpToDate, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('sets useLocalCanvasKit in BuildInfo', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); final CommandRunner runner = createTestCommandRunner(flutterCommand); fileSystem.directory('engine/src/out/wasm_release').createSync(recursive: true); await runner.run(['--local-web-sdk=wasm_release', '--local-engine-src-path=engine/src', 'dummy']); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.useLocalCanvasKit, isTrue); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('dds options', () async { final FakeDdsCommand ddsCommand = FakeDdsCommand(); final CommandRunner runner = createTestCommandRunner(ddsCommand); await runner.run(['test', '--dds-port=1']); expect(ddsCommand.enableDds, isTrue); expect(ddsCommand.ddsPort, 1); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('dds options --dds', () async { final FakeDdsCommand ddsCommand = FakeDdsCommand(); final CommandRunner runner = createTestCommandRunner(ddsCommand); await runner.run(['test', '--dds']); expect(ddsCommand.enableDds, isTrue); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('dds options --no-dds', () async { final FakeDdsCommand ddsCommand = FakeDdsCommand(); final CommandRunner runner = createTestCommandRunner(ddsCommand); await runner.run(['test', '--no-dds']); expect(ddsCommand.enableDds, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('dds options --disable-dds', () async { final FakeDdsCommand ddsCommand = FakeDdsCommand(); final CommandRunner runner = createTestCommandRunner(ddsCommand); await runner.run(['test', '--disable-dds']); expect(ddsCommand.enableDds, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('dds options --no-disable-dds', () async { final FakeDdsCommand ddsCommand = FakeDdsCommand(); final CommandRunner runner = createTestCommandRunner(ddsCommand); await runner.run(['test', '--no-disable-dds']); expect(ddsCommand.enableDds, isTrue); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('dds options --dds --disable-dds', () async { final FakeDdsCommand ddsCommand = FakeDdsCommand(); final CommandRunner runner = createTestCommandRunner(ddsCommand); await runner.run(['test', '--dds', '--disable-dds']); expect(() => ddsCommand.enableDds, throwsToolExit()); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); group('findTargetDevice', () { final FakeDevice device1 = FakeDevice('device1', 'device1'); final FakeDevice device2 = FakeDevice('device2', 'device2'); testUsingContext('no device found', () async { final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); final Device? device = await flutterCommand.findTargetDevice(); expect(device, isNull); }); testUsingContext('finds single device', () async { testDeviceManager.addAttachedDevice(device1); final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); final Device? device = await flutterCommand.findTargetDevice(); expect(device, device1); }); testUsingContext('finds multiple devices', () async { testDeviceManager.addAttachedDevice(device1); testDeviceManager.addAttachedDevice(device2); testDeviceManager.specifiedDeviceId = 'all'; final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); final Device? device = await flutterCommand.findTargetDevice(); expect(device, isNull); expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDevice)); }); }); group('--dart-define-from-file', () { late FlutterCommand dummyCommand; late CommandRunner dummyCommandRunner; setUp(() { dummyCommand = DummyFlutterCommand()..usesDartDefineOption(); dummyCommandRunner = createTestCommandRunner(dummyCommand); }); testUsingContext('parses values from JSON files and includes them in defines list', () async { fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('config1.json').writeAsString( ''' { "kInt": 1, "kDouble": 1.1, "name": "denghaizhu", "title": "this is title from config json file", "nullValue": null, "containEqual": "sfadsfv=432f" } ''' ); await fileSystem.file('config2.json').writeAsString( ''' { "body": "this is body from config json file" } ''' ); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=config1.json', '--dart-define-from-file=config2.json', ]); final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.dartDefines, containsAll(const [ 'kInt=1', 'kDouble=1.1', 'name=denghaizhu', 'title=this is title from config json file', 'nullValue=null', 'containEqual=sfadsfv=432f', 'body=this is body from config json file', ])); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('has values with identical keys from --dart-define take precedence', () async { fileSystem .file(fileSystem.path.join('lib', 'main.dart')) .createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('.env').writeAsStringSync(''' MY_VALUE=VALUE_FROM_ENV_FILE '''); await dummyCommandRunner.run([ 'dummy', '--dart-define=MY_VALUE=VALUE_FROM_COMMAND', '--dart-define-from-file=.env', ]); final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.dartDefines, containsAll(const [ 'MY_VALUE=VALUE_FROM_ENV_FILE', 'MY_VALUE=VALUE_FROM_COMMAND', ])); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('correctly parses a valid env file', () async { fileSystem .file(fileSystem.path.join('lib', 'main.dart')) .createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('.env').writeAsString(''' # comment kInt=1 kDouble=1.1 # should be double name=piotrfleury title=this is title from config env file empty= doubleQuotes="double quotes 'value'#=" # double quotes singleQuotes='single quotes "value"#=' # single quotes backQuotes=`back quotes "value" '#=` # back quotes hashString="some-#-hash-string-value" # Play around with spaces around the equals sign. spaceBeforeEqual =value spaceAroundEqual = value spaceAfterEqual= value '''); await fileSystem.file('.env2').writeAsString(''' # second comment body=this is body from config env file '''); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=.env', '--dart-define-from-file=.env2', ]); final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.dartDefines, containsAll(const [ 'kInt=1', 'kDouble=1.1', 'name=piotrfleury', 'title=this is title from config env file', 'empty=', "doubleQuotes=double quotes 'value'#=", 'singleQuotes=single quotes "value"#=', 'backQuotes=back quotes "value" \'#=', 'hashString=some-#-hash-string-value', 'spaceBeforeEqual=value', 'spaceAroundEqual=value', 'spaceAfterEqual=value', 'body=this is body from config env file' ])); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('throws a ToolExit when the provided .env file is malformed', () async { fileSystem .file(fileSystem.path.join('lib', 'main.dart')) .createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('.env').writeAsString('what is this'); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=.env', ]); expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug), throwsToolExit(message: 'Unable to parse file provided for ' '--${FlutterOptions.kDartDefineFromFileOption}.\n' 'Invalid property line: what is this')); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('throws a ToolExit when .env file contains a multiline value', () async { fileSystem .file(fileSystem.path.join('lib', 'main.dart')) .createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('.env').writeAsString(''' # single line value name=piotrfleury # multi-line value multiline = """ Welcome to .env demo a simple counter app with .env file support for more info, check out the README.md file Thanks! """ # This is the welcome message that will be displayed on the counter app '''); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=.env', ]); expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug), throwsToolExit(message: 'Multi-line value is not supported: multiline = """ Welcome to .env demo')); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('works with mixed file formats', () async { fileSystem .file(fileSystem.path.join('lib', 'main.dart')) .createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('.env').writeAsString(''' kInt=1 kDouble=1.1 name=piotrfleury title=this is title from config env file '''); await fileSystem.file('config.json').writeAsString(''' { "body": "this is body from config json file" } '''); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=.env', '--dart-define-from-file=config.json', ]); final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.dartDefines, containsAll(const [ 'kInt=1', 'kDouble=1.1', 'name=piotrfleury', 'title=this is title from config env file', 'body=this is body from config json file', ])); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('when files contain entries with duplicate keys, uses the value from the lattermost file', () async { fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('config1.json').writeAsString( ''' { "kInt": 1, "kDouble": 1.1, "name": "denghaizhu", "title": "this is title from config json file" } ''' ); await fileSystem.file('config2.json').writeAsString( ''' { "kInt": "2" } ''' ); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=config1.json', '--dart-define-from-file=config2.json', ]); final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.dartDefines, containsAll(const [ 'kInt=2', 'kDouble=1.1', 'name=denghaizhu', 'title=this is title from config json file' ])); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('throws a ToolExit when the argued path points to a directory', () async { fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); fileSystem.directory('config').createSync(); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=config', ]); expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug), throwsToolExit(message: 'Did not find the file passed to "--dart-define-from-file". Path: config')); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('throws a ToolExit when the given JSON file is malformed', () async { fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); await fileSystem.file('config.json').writeAsString( ''' { "kInt": 1Error json format "kDouble": 1.1, "name": "denghaizhu", "title": "this is title from config json file" } ''' ); await dummyCommandRunner.run([ 'dummy', '--dart-define-from-file=config.json', ]); expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug), throwsToolExit(message: 'Unable to parse the file at path "config.json" due to ' 'a formatting error. Ensure that the file contains valid JSON.\n' 'Error details: FormatException: Missing expected digit (at line 2, character 25)')); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); testUsingContext('throws a ToolExit when the provided file does not exist', () async { fileSystem.directory('config').createSync(); await dummyCommandRunner.run([ 'dummy', '--dart-define=k=v', '--dart-define-from-file=config']); expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug), throwsToolExit(message: 'Did not find the file passed to "--dart-define-from-file". Path: config')); }, overrides: { FileSystem: () => fileSystem, Logger: () => logger, FileSystemUtils: () => fileSystemUtils, Platform: () => platform, ProcessManager: () => processManager, }); }); group('--flavor', () { late _TestDeviceManager testDeviceManager; late Logger logger; late FileSystem fileSystem; setUp(() { logger = BufferLogger.test(); testDeviceManager = _TestDeviceManager(logger: logger); fileSystem = MemoryFileSystem.test(); }); testUsingContext("tool exits when FLUTTER_APP_FLAVOR is already set in user's environment", () async { fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); final FakeDevice device = FakeDevice( 'name', 'id', type: PlatformType.android, supportsFlavors: true, ); testDeviceManager.devices = [device]; final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates(); final CommandRunner runner = createTestCommandRunner(command); expect(runner.run(['run', '--no-pub', '--no-hot', '--flavor=strawberry']), throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.')); }, overrides: { DeviceManager: () => testDeviceManager, Platform: () => FakePlatform( environment: { 'FLUTTER_APP_FLAVOR': 'I was already set' } ), Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('tool exits when FLUTTER_APP_FLAVOR is set in --dart-define or --dart-define-from-file', () async { fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('config.json')..createSync()..writeAsStringSync('{"FLUTTER_APP_FLAVOR": "strawberry"}'); final FakeDevice device = FakeDevice( 'name', 'id', type: PlatformType.android, supportsFlavors: true, ); testDeviceManager.devices = [device]; final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates(); final CommandRunner runner = createTestCommandRunner(command); expect(runner.run(['run', '--dart-define=FLUTTER_APP_FLAVOR=strawberry', '--no-pub', '--no-hot', '--flavor=strawberry']), throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file')); expect(runner.run(['run', '--dart-define-from-file=config.json', '--no-pub', '--no-hot', '--flavor=strawberry']), throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file')); }, overrides: { DeviceManager: () => testDeviceManager, Platform: () => FakePlatform(), Cache: () => Cache.test(processManager: FakeProcessManager.any()), FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('CLI option overrides default flavor from manifest', () async { final File pubspec = fileSystem.file('pubspec.yaml'); await pubspec.create(); await pubspec.writeAsString(''' name: test flutter: default-flavor: foo '''); final DummyFlutterCommand flutterCommand = DummyFlutterCommand(); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.flavor, 'foo'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.empty(), }); testUsingContext('tool loads default flavor from manifest, but cli overrides', () async { final File pubspec = fileSystem.file('pubspec.yaml'); await pubspec.create(); await pubspec.writeAsString(''' name: test flutter: default-flavor: foo '''); final DummyFlutterCommand flutterCommand = DummyFlutterCommand(commandFunction: () async { return FlutterCommandResult.success(); },); flutterCommand.usesFlavorOption(); final CommandRunner runner = createTestCommandRunner(flutterCommand); await runner.run(['dummy', '--flavor', 'bar']); final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug); expect(buildInfo.flavor, 'bar'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => FakeProcessManager.empty(), }); }); }); } class FakeDeprecatedCommand extends FlutterCommand { @override String get description => 'A fake command'; @override String get name => 'deprecated'; @override bool get deprecated => true; @override Future runCommand() async { return FlutterCommandResult.success(); } } class FakeTargetCommand extends FlutterCommand { FakeTargetCommand() { usesTargetOption(); } @override Future runCommand() async { cachedTargetFile = targetFile; return FlutterCommandResult.success(); } String? cachedTargetFile; @override String get description => ''; @override String get name => 'test'; } class FakeReportingNullSafetyCommand extends FlutterCommand { FakeReportingNullSafetyCommand() { argParser.addFlag('debug'); argParser.addFlag('release'); argParser.addFlag('jit-release'); argParser.addFlag('profile'); } @override String get description => 'test'; @override String get name => 'test'; @override bool get shouldRunPub => true; @override bool get reportNullSafety => true; @override Future runCommand() async { return FlutterCommandResult.success(); } } class FakeDdsCommand extends FlutterCommand { FakeDdsCommand() { addDdsOptions(verboseHelp: false); } @override String get description => 'test'; @override String get name => 'test'; @override Future runCommand() async { return FlutterCommandResult.success(); } } class FakeProcessInfo extends Fake implements ProcessInfo { @override int maxRss = 0; } class FakeIoProcessSignal extends Fake implements io.ProcessSignal { late Stream stream; @override Stream watch() => stream; } class FakeCache extends Fake implements Cache { List> artifacts = >[]; @override Future updateAll(Set requiredArtifacts, {bool offline = false}) async { artifacts.add(requiredArtifacts.toSet()); } @override void releaseLock() { } } class FakeSignals implements Signals { FakeSignals({ required this.subForSigTerm, required List exitSignals, }) : delegate = Signals.test(exitSignals: exitSignals); final ProcessSignal subForSigTerm; final Signals delegate; @override Object addHandler(ProcessSignal signal, SignalHandler handler) { if (signal == ProcessSignal.sigterm) { return delegate.addHandler(subForSigTerm, handler); } return delegate.addHandler(signal, handler); } @override Future removeHandler(ProcessSignal signal, Object token) => delegate.removeHandler(signal, token); @override Stream get errors => delegate.errors; } class FakeClock extends Fake implements SystemClock { List times = []; @override DateTime now() { return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0)); } } class FakePub extends Fake implements Pub { @override Future get({ required PubContext context, required FlutterProject project, bool upgrade = false, bool offline = false, String? flutterRootOverride, bool checkUpToDate = false, bool shouldSkipThirdPartyGenerator = true, PubOutputMode outputMode = PubOutputMode.all, }) async { } } class _TestDeviceManager extends DeviceManager { _TestDeviceManager({required super.logger}); List devices = []; @override List get deviceDiscoverers { final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); devices.forEach(discoverer.addDevice); return [discoverer]; } } class _TestRunCommandThatOnlyValidates extends RunCommand { @override Future runCommand() async { return FlutterCommandResult.success(); } }