flutter/packages/flutter_tools/test/general.shard/runner/flutter_command_test.dart
Jonas Uekötter 556ae54358
Fix incorrectly checking for invalid environment variables in the tool (#164101)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

This PR removes a check that looks for the use of Flutter version
related Dart define keys in the environment variables (not Dart defines)
of the user of the Flutter tool. We don't need to check the environment
variables, we only need to check the user defined Dart defines.

Here's how it currently works:

1. User builds a Flutter app via tool
2. tool checks environment variables for Flutter version related Dart
define keys and throws if it finds and (_and_)
3. tool checks list of user defined Dart defines for Flutter version
related Dart define keys and throws if it finds any

This PR removes the check in step 2, since step 3 is what we actually
care about.

The `FLUTTER_GIT_URL` environment variable is set on forks and the Dart
define is used to make that information available to applications at
runtime. However, environment variables don't propagate to be Dart
defines, thus the above mentioned check is not needed.

*List which issues are fixed by this PR. You must list at least one
issue. An issue is not required if the PR fixes something trivial like a
typo.*

Fixes #164093

*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*

## 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
2025-02-28 14:48:15 +00:00

1582 lines
52 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 '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' show RunCommand;
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/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:meta/meta.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 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();
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: <Type, Generator>{
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, <Set<DevelopmentArtifact>>[
<DevelopmentArtifact>{DevelopmentArtifact.universal},
<DevelopmentArtifact>{},
]);
},
overrides: <Type, Generator>{
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: <Type, Generator>{
Cache: () => cache,
FileSystem: () => fileSystem,
PreRunValidator: () => preRunValidator,
ProcessManager: () => processManager,
},
);
testUsingContext(
'deprecated command should warn',
() async {
final FakeDeprecatedCommand flutterCommand = FakeDeprecatedCommand();
final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
await runner.run(<String>['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: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'uses the error handling file system',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
expect(globals.fs, isA<ErrorHandlingFileSystem>());
return const FlutterCommandResult(ExitStatus.success);
},
);
await flutterCommand.run();
},
overrides: <Type, Generator>{
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<void> runner = createTestCommandRunner(fakeTargetCommand);
await runner.run(<String>['test']);
expect(fakeTargetCommand.cachedTargetFile, 'lib/main.dart');
},
overrides: <Type, Generator>{
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<void> runner = createTestCommandRunner(fakeTargetCommand);
await runner.run(<String>['test', '-t', 'lib/foo.dart']);
expect(fakeTargetCommand.cachedTargetFile, 'lib/foo.dart');
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'throws tool exit if specified file does not exist',
() async {
final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);
expect(() async => runner.run(<String>['test', '-t', 'lib/foo.dart']), throwsToolExit());
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
@isTest
void testUsingCommandContext(String testName, dynamic Function() testBody) {
testUsingContext(
testName,
testBody,
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessInfo: () => processInfo,
ProcessManager: () => processManager,
SystemClock: () => clock,
Analytics: () => fakeAnalytics,
},
);
}
testUsingCommandContext('reports command that results in success', () async {
// Crash if called a third time which is unexpected.
clock.times = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.success);
},
);
await flutterCommand.run();
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 = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
return const FlutterCommandResult(ExitStatus.warning);
},
);
await flutterCommand.run();
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 = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
throwToolExit('fail');
},
);
await expectLater(() => flutterCommand.run(), throwsToolExit());
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(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'http://127.0.0.1:9105',
]);
expect(command.devToolsServerAddress.toString(), equals('http://127.0.0.1:9105'));
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'devToolsServerAddress returns null for bad input',
() async {
final DummyFlutterCommand command =
DummyFlutterCommand()..addDevToolsOptions(verboseHelp: false);
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'hello-world',
]);
expect(command.devToolsServerAddress, isNull);
await runner.run(<String>['dummy', '--${FlutterCommand.kDevToolsServerAddress}', '']);
expect(command.devToolsServerAddress, isNull);
await runner.run(<String>['dummy', '--${FlutterCommand.kDevToolsServerAddress}', '9101']);
expect(command.devToolsServerAddress, isNull);
await runner.run(<String>[
'dummy',
'--${FlutterCommand.kDevToolsServerAddress}',
'127.0.0.1:9101',
]);
expect(command.devToolsServerAddress, isNull);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
group('signals tests', () {
late FakeIoProcessSignal mockSignal;
late ProcessSignal signalUnderTest;
late StreamController<io.ProcessSignal> signalController;
setUp(() {
mockSignal = FakeIoProcessSignal();
signalUnderTest = ProcessSignal(mockSignal);
signalController = StreamController<io.ProcessSignal>();
mockSignal.stream = signalController.stream;
});
testUsingContext(
'reports command that is killed',
() async {
// Crash if called a third time which is unexpected.
clock.times = <int>[1000, 2000];
final Completer<void> completer = Completer<void>();
setExitFunctionForTests((int exitCode) {
expect(exitCode, 0);
restoreExitFunction();
completer.complete();
});
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
final Completer<void> c = Completer<void>();
await c.future;
throw UnsupportedError('Unreachable');
},
);
unawaited(flutterCommand.run());
signalController.add(mockSignal);
await completer.future;
expect(
fakeAnalytics.sentEvents,
contains(
Event.flutterCommandResult(
commandPath: 'dummy',
result: 'killed',
maxRss: 10,
commandHasTerminal: false,
),
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
ProcessInfo: () => processInfo,
Signals:
() => FakeSignals(
subForSigTerm: signalUnderTest,
exitSignals: <ProcessSignal>[signalUnderTest],
),
SystemClock: () => clock,
Analytics: () => fakeAnalytics,
},
);
testUsingContext(
'command release lock on kill signal',
() async {
clock.times = <int>[1000, 2000];
final Completer<void> completer = Completer<void>();
setExitFunctionForTests((int exitCode) {
expect(exitCode, 0);
restoreExitFunction();
completer.complete();
});
final Completer<void> checkLockCompleter = Completer<void>();
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
await globals.cache.lock();
checkLockCompleter.complete();
final Completer<void> c = Completer<void>();
await c.future;
throw UnsupportedError('Unreachable');
},
);
unawaited(flutterCommand.run());
await checkLockCompleter.future;
globals.cache.checkLockAcquired();
signalController.add(mockSignal);
await completer.future;
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
ProcessInfo: () => processInfo,
Signals:
() => FakeSignals(
subForSigTerm: signalUnderTest,
exitSignals: <ProcessSignal>[signalUnderTest],
),
},
);
});
testUsingCommandContext('report execution timing by default', () async {
// Crash if called a third time which is unexpected.
clock.times = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
await flutterCommand.run();
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 = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(noUsagePath: true);
await flutterCommand.run();
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 = <int>[1000, 2000];
final FlutterCommandResult commandResult = FlutterCommandResult(
ExitStatus.success,
// nulls should be cleaned up.
timingLabelParts: <String?>['blah1', 'blah2', null, 'blah3'],
endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500),
);
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async => commandResult,
);
await flutterCommand.run();
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 = <int>[1000, 2000];
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
commandFunction: () async {
throwToolExit('fail');
},
);
await expectLater(() => flutterCommand.run(), throwsToolExit());
expect(
fakeAnalytics.sentEvents,
contains(
Event.timing(
workflow: 'flutter',
variableName: 'dummy',
elapsedMilliseconds: 1000,
label: 'fail',
),
),
);
});
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: <Type, Generator>{
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: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'use fileSystemRoots to generate BuildInfo',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
fileSystemRoots: <String>['foo', 'bar'],
);
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.fileSystemRoots, <String>['foo', 'bar']);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'includes initializeFromDill in BuildInfo',
() async {
final DummyFlutterCommand flutterCommand =
DummyFlutterCommand()..usesInitializeFromDillOption(hide: false);
final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
await runner.run(<String>['dummy', '--initialize-from-dill=/foo/bar.dill']);
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.initializeFromDill, '/foo/bar.dill');
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'includes assumeInitializeFromDillUpToDate in BuildInfo',
() async {
final DummyFlutterCommand flutterCommand =
DummyFlutterCommand()..usesInitializeFromDillOption(hide: false);
final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
await runner.run(<String>['dummy', '--assume-initialize-from-dill-up-to-date']);
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.assumeInitializeFromDillUpToDate, isTrue);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'unsets assumeInitializeFromDillUpToDate in BuildInfo when disabled',
() async {
final DummyFlutterCommand flutterCommand =
DummyFlutterCommand()..usesInitializeFromDillOption(hide: false);
final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
await runner.run(<String>['dummy', '--no-assume-initialize-from-dill-up-to-date']);
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.assumeInitializeFromDillUpToDate, isFalse);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'sets useLocalCanvasKit in BuildInfo',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
fileSystem.directory('engine/src/out/wasm_release').createSync(recursive: true);
await runner.run(<String>[
'--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: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'dds options',
() async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
await runner.run(<String>['test', '--dds-port=1']);
expect(ddsCommand.enableDds, isTrue);
expect(ddsCommand.ddsPort, 1);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'dds options --dds',
() async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
await runner.run(<String>['test', '--dds']);
expect(ddsCommand.enableDds, isTrue);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'dds options --no-dds',
() async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
await runner.run(<String>['test', '--no-dds']);
expect(ddsCommand.enableDds, isFalse);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'dds options --disable-dds',
() async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
await runner.run(<String>['test', '--disable-dds']);
expect(ddsCommand.enableDds, isFalse);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'dds options --no-disable-dds',
() async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
await runner.run(<String>['test', '--no-disable-dds']);
expect(ddsCommand.enableDds, isTrue);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
},
);
testUsingContext(
'dds options --dds --disable-dds',
() async {
final FakeDdsCommand ddsCommand = FakeDdsCommand();
final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
await runner.run(<String>['test', '--dds', '--disable-dds']);
expect(() => ddsCommand.enableDds, throwsToolExit());
},
overrides: <Type, Generator>{
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<void> 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(<String>[
'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 <String>[
'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: <Type, Generator>{
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(<String>[
'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 <String>[
'MY_VALUE=VALUE_FROM_ENV_FILE',
'MY_VALUE=VALUE_FROM_COMMAND',
]),
);
},
overrides: <Type, Generator>{
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(<String>[
'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 <String>[
'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: <Type, Generator>{
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(<String>['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: <Type, Generator>{
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(<String>['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: <Type, Generator>{
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(<String>[
'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 <String>[
'kInt=1',
'kDouble=1.1',
'name=piotrfleury',
'title=this is title from config env file',
'body=this is body from config json file',
]),
);
},
overrides: <Type, Generator>{
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(<String>[
'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 <String>[
'kInt=2',
'kDouble=1.1',
'name=denghaizhu',
'title=this is title from config json file',
]),
);
},
overrides: <Type, Generator>{
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(<String>['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: <Type, Generator>{
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(<String>['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: <Type, Generator>{
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(<String>[
'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: <Type, Generator>{
FileSystem: () => fileSystem,
Logger: () => logger,
FileSystemUtils: () => fileSystemUtils,
Platform: () => platform,
ProcessManager: () => processManager,
},
);
});
group('--flavor', () {
late FileSystem fileSystem;
setUp(() {
fileSystem = MemoryFileSystem.test();
});
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: <Type, Generator>{
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<void> runner = createTestCommandRunner(flutterCommand);
await runner.run(<String>['dummy', '--flavor', 'bar']);
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.flavor, 'bar');
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.empty(),
},
);
});
group('Flutter version', () {
for (final String dartDefine in FlutterCommand.flutterVersionDartDefines) {
testUsingContext(
'tool exits when $dartDefine is set in --dart-define or --dart-define-from-file',
() async {
final CommandRunner<void> runner = createTestCommandRunner(
_TestRunCommandThatOnlyValidates(),
);
expect(
runner.run(<String>[
'run',
'--dart-define=$dartDefine=AlreadySet',
'--no-pub',
'--no-hot',
]),
throwsToolExit(
message:
'$dartDefine is used by the framework and cannot be set using --dart-define or --dart-define-from-file. '
'Use FlutterVersion to access it in Flutter code',
),
);
expect(
runner.run(<String>[
'run',
'--dart-define-from-file=config.json',
'--no-pub',
'--no-hot',
]),
throwsToolExit(
message:
'$dartDefine is used by the framework and cannot be set using --dart-define or --dart-define-from-file. '
'Use FlutterVersion to access it in Flutter code',
),
);
},
overrides: <Type, Generator>{
DeviceManager:
() => FakeDeviceManager()..attachedDevices = <Device>[FakeDevice('name', 'id')],
Platform: () => FakePlatform(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
fileSystem.file('config.json')
..createSync()
..writeAsStringSync('{"$dartDefine": "AlreadySet"}');
return fileSystem;
},
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
}
// Regression test for https://github.com/flutter/flutter/issues/164093
testUsingContext(
'tool does not throw when FLUTTER_GIT_URL exists in environment variables',
() async {
final CommandRunner<void> runner = createTestCommandRunner(
_TestRunCommandThatOnlyValidates(),
);
await expectReturnsNormallyLater(runner.run(<String>['run', '--no-pub', '--no-hot']));
},
overrides: <Type, Generator>{
DeviceManager:
() => FakeDeviceManager()..attachedDevices = <Device>[FakeDevice('name', 'id')],
Platform:
() =>
FakePlatform()
..environment = <String, String>{
'FLUTTER_GIT_URL': 'git@example.org:fork_of/flutter.git',
},
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
return fileSystem;
},
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
testUsingContext(
'FLUTTER_VERSION is set in dartDefines',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.dartDefines, contains('FLUTTER_VERSION=0.0.0'));
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
testUsingContext(
'FLUTTER_CHANNEL is set in dartDefines',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.dartDefines, contains('FLUTTER_CHANNEL=master'));
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
testUsingContext(
'FLUTTER_GIT_URL is set in dartDefines',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(
buildInfo.dartDefines,
contains('FLUTTER_GIT_URL=https://github.com/flutter/flutter.git'),
);
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
testUsingContext(
'FLUTTER_FRAMEWORK_REVISION is set in dartDefines',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.dartDefines, contains('FLUTTER_FRAMEWORK_REVISION=11111'));
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
testUsingContext(
'FLUTTER_ENGINE_REVISION is set in dartDefines',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.dartDefines, contains('FLUTTER_ENGINE_REVISION=abcde'));
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
testUsingContext(
'FLUTTER_DART_VERSION is set in dartDefines',
() async {
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
final BuildInfo buildInfo = await flutterCommand.getBuildInfo(
forcedBuildMode: BuildMode.debug,
);
expect(buildInfo.dartDefines, contains('FLUTTER_DART_VERSION=12'));
},
overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FlutterVersion: () => FakeFlutterVersion(),
},
);
});
});
}
class FakeDeprecatedCommand extends FlutterCommand {
@override
String get description => 'A fake command';
@override
String get name => 'deprecated';
@override
bool get deprecated => true;
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
class FakeTargetCommand extends FlutterCommand {
FakeTargetCommand() {
usesTargetOption();
}
@override
Future<FlutterCommandResult> runCommand() async {
cachedTargetFile = targetFile;
return FlutterCommandResult.success();
}
String? cachedTargetFile;
@override
String get description => '';
@override
String get name => 'test';
}
class FakeDdsCommand extends FlutterCommand {
FakeDdsCommand() {
addDdsOptions(verboseHelp: false);
}
@override
String get description => 'test';
@override
String get name => 'test';
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
class FakeProcessInfo extends Fake implements ProcessInfo {
@override
int maxRss = 0;
}
class FakeIoProcessSignal extends Fake implements io.ProcessSignal {
late Stream<io.ProcessSignal> stream;
@override
Stream<io.ProcessSignal> watch() => stream;
}
class FakeCache extends Fake implements Cache {
List<Set<DevelopmentArtifact>> artifacts = <Set<DevelopmentArtifact>>[];
@override
Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts, {bool offline = false}) async {
artifacts.add(requiredArtifacts.toSet());
}
@override
void releaseLock() {}
}
class FakeSignals implements Signals {
FakeSignals({required this.subForSigTerm, required List<ProcessSignal> 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<bool> removeHandler(ProcessSignal signal, Object token) =>
delegate.removeHandler(signal, token);
@override
Stream<Object> get errors => delegate.errors;
}
class FakeClock extends Fake implements SystemClock {
List<int> times = <int>[];
@override
DateTime now() {
return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0));
}
}
class _TestRunCommandThatOnlyValidates extends RunCommand {
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
@override
bool get shouldRunPub => false;
}