flutter/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
Andrew Kolos 016eb85177
Support conditional bundling of assets based on --flavor (#132985)
Provides support for conditional bundling of assets through the existing `--flavor` option for `flutter build` and `flutter run`. Closes https://github.com/flutter/flutter/issues/21682. Resolves https://github.com/flutter/flutter/issues/136092

## Change
Within the `assets` section pubspec.yaml, the user can now specify one or more `flavors` that an asset belongs to. Consider this example:

```yaml
# pubspec.yaml
flutter:
  assets:
    - assets/normal-asset.png
    - path: assets/vanilla/ice-cream.png
      flavors: 
        - vanilla
    - path: assets/strawberry/ice-cream.png
      flavors:
        - strawberry
```

With this pubspec,
* `flutter run --flavor vanilla` will not include `assets/strawberry/ice-cream.png` in the build output.
* `flutter run --flavor strawberry` will not include `assets/vanilla/ice-cream.png`.
* `flutter run` will only include `assets/normal-asset.png`.

## Open questions

* Should this be supported for all platforms, or should this change be limited to ones with documented `--flavor` support (Android, iOS, and (implicitly) MacOS)? This PR currently only enables this feature for officially supported platforms.

## Design thoughts, what this PR does not do, etc.

### This does not provide an automatic mapping/resolution of asset keys/paths to others based on flavor at runtime.

The implementation in this PR represents a simplest approach. Notably, it does not give Flutter the ability to dynamically choose an asset based on flavor using a single asset key. For example, one can't use `Image.asset('config.json')` to dynamically choose between different "flavors" of `config.json` (such as `dev-flavor/config.json` or `prod-flavor/config.json`). However, a user could always implement such a mechanism in their project or in a library by examining the flavor at runtime.

### When multiple entries affect the same file and 1) at least one of these entries have a `flavors` list provided and 2) these lists are not equivalent, we always consider the manifest to be ambiguous and will throw a `ToolExit`. 

<details>
For example, these manifests would all be considered ambiguous:

```yaml
assets:
  - assets/
  - path: assets/vanilla.png
    flavors: 
      - vanilla

assets:
  - path: assets/vanilla/
    flavors: 
      - vanilla
  - path: assets/vanilla/cherry.png
     flavor:
      - cherry

# Thinking towards the future where we might add glob/regex support and more conditions other than flavor:
assets:
  - path: assets/vanilla/**
    flavors:
      - vanilla
  - path: assets/**/ios/**
    platforms: 
       - ios

# Ambiguous in the case of assets like "assets/vanilla/ios/icon.svg" since we 
# don't know if flavor `vanilla` and platform `ios` should be combined using or-logic or and-logic.
```

See [this review comment thread](https://github.com/flutter/flutter/pull/132985#discussion_r1381909942) for the full story on how I arrived at this decision.
</details>

### This does not support Android's multidimensional flavors feature (in an intuitive way)

<details>

Conder this excerpt from a Flutter project's android/app/build.gradle file:

```groovy
android {
    // ...

    flavorDimensions "mode", "api"

    productFlavors {
        free {
            dimension "mode"
            applicationIdSuffix ".free"
        }

        premium {
            dimension "mode"
            applicationIdSuffix ".premium"
        }

        minApi23 {
            dimension "api"
            versionNameSuffix "-minApi23"
        }

        minApi21 {
            dimension "api"
            versionNameSuffix "-minApi21"
        }
    }
}
```

In this setup, the following values are valid `--flavor` are valid `freeMinApi21`, `freeMinApi23`, `premiumMinApi21`, and `premiumMinApi23`. We call these values "flavor combinations". Consider the following from the Android documentation[^1]:

> In addition to the source set directories you can create for each individual product flavor and build variant, you can also create source set directories for each combination of product flavors. For example, you can create and add Java sources to the src/demoMinApi24/java/ directory, and Gradle uses those sources only when building a variant that combines those two product flavors.
> 
> Source sets you create for product flavor combinations have a higher priority than source sets that belong to each individual product flavor. To learn more about source sets and how Gradle merges resources, read the section about how to [create source sets](https://developer.android.com/build/build-variants#sourcesets).

This feature will not behave in this way. If a user utilizes this feature and also Android's multidimensional flavors feature, they will have to list out all flavor combinations that contain the flavor they want to limit an asset to:

```yaml
assets:
  - assets/free/
    flavors:
      - freeMinApi21
      - freeMinApi23
```

This is mostly due to a technical limitation in the hot-reload feature of `flutter run`. During a hot reload, the tool will try to update the asset bundle on the device, but the tool does not know the flavors contained within the flavor combination (that the user passes to `--flavor`). Gradle is the source of truth of what flavors were involved in the build, and `flutter run` currently does not access to that information since it's an implementation detail of the build process. We could bubble up this information, but it would require a nontrivial amount of engineering work, and it's unclear how desired this functionality is. It might not be worth implementing.

</details>

See https://flutter.dev/go/flavor-specific-assets for the (outdated) design document. 

<summary>Pre-launch Checklist</summary>

</details>

[^1]: https://developer.android.com/build/build-variants#flavor-dimensions
2023-12-07 23:50:00 +00:00

1600 lines
57 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 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_device.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.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/terminal.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/daemon.dart';
import 'package:flutter_tools/src/commands/run.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:flutter_tools/src/web/compile.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart' as analytics;
import 'package:vm_service/vm_service.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';
void main() {
setUpAll(() {
Cache.disableLocking();
});
group('run', () {
late TestDeviceManager testDeviceManager;
late FileSystem fileSystem;
setUp(() {
testDeviceManager = TestDeviceManager(logger: BufferLogger.test());
fileSystem = MemoryFileSystem.test();
});
testUsingContext('fails when target not found', () async {
final RunCommand command = RunCommand();
expect(
() => createTestCommandRunner(command).run(<String>['run', '-t', 'abc123', '--no-pub']),
throwsA(isA<ToolExit>().having((ToolExit error) => error.exitCode, 'exitCode', anyOf(isNull, 1))),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
});
testUsingContext('does not support --no-sound-null-safety by default', () async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--use-application-binary=app/bar/faz',
'--no-sound-null-safety',
]),
throwsA(isException.having(
(Exception exception) => exception.toString(),
'toString',
contains('Could not find an option named "no-sound-null-safety"'),
)),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
});
testUsingContext('supports --no-sound-null-safety with an overridden NonNullSafeBuilds', () async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
final FakeDevice device = FakeDevice(isLocalEmulator: true, platformType: PlatformType.android);
testDeviceManager.devices = <Device>[device];
final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates();
await createTestCommandRunner(command).run(const <String>[
'run',
'--use-application-binary=app/bar/faz',
'--no-sound-null-safety',
]);
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fileSystem,
Logger: () => BufferLogger.test(),
NonNullSafeBuilds: () => NonNullSafeBuilds.allowed,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('does not support "--use-application-binary" and "--fast-start"', () async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--use-application-binary=app/bar/faz',
'--fast-start',
'--no-pub',
'--show-test-device',
]),
throwsA(isException.having(
(Exception exception) => exception.toString(),
'toString',
isNot(contains('--fast-start is not supported with --use-application-binary')),
)),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
});
testUsingContext('Walks upward looking for a pubspec.yaml and succeeds if found', () async {
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages')
.writeAsStringSync('\n');
fileSystem.file('lib/main.dart')
.createSync(recursive: true);
fileSystem.currentDirectory = fileSystem.directory('a/b/c')
..createSync(recursive: true);
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]),
throwsToolExit(),
);
final BufferLogger bufferLogger = globals.logger as BufferLogger;
expect(
bufferLogger.statusText,
containsIgnoringWhitespace('Changing current working directory to:'),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
});
testUsingContext('Walks upward looking for a pubspec.yaml and exits if missing', () async {
fileSystem.currentDirectory = fileSystem.directory('a/b/c')
..createSync(recursive: true);
fileSystem.file('lib/main.dart')
.createSync(recursive: true);
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]),
throwsToolExit(message: 'No pubspec.yaml file found'),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
});
group('run app', () {
late MemoryFileSystem fs;
late Artifacts artifacts;
late TestUsage usage;
late FakeAnsiTerminal fakeTerminal;
late analytics.FakeAnalytics fakeAnalytics;
setUpAll(() {
Cache.disableLocking();
});
setUp(() {
fakeTerminal = FakeAnsiTerminal();
artifacts = Artifacts.test();
usage = TestUsage();
fs = MemoryFileSystem.test();
fs.currentDirectory.childFile('pubspec.yaml')
.writeAsStringSync('name: flutter_app');
fs.currentDirectory.childFile('.packages')
.writeAsStringSync('# Generated by pub on 2019-11-25 12:38:01.801784.');
final Directory libDir = fs.currentDirectory.childDirectory('lib');
libDir.createSync();
final File mainFile = libDir.childFile('main.dart');
mainFile.writeAsStringSync('void main() {}');
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fs,
fakeFlutterVersion: FakeFlutterVersion(),
);
});
testUsingContext('exits with a user message when no supported devices attached', () async {
final RunCommand command = RunCommand();
testDeviceManager.devices = <Device>[];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
]),
throwsA(isA<ToolExit>().having((ToolExit error) => error.message, 'message', isNull)),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace('No supported devices connected.'),
);
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('exits and lists available devices when specified device not found', () async {
final RunCommand command = RunCommand();
final FakeDevice device = FakeDevice(isLocalEmulator: true);
testDeviceManager
..devices = <Device>[device]
..specifiedDeviceId = 'invalid-device-id';
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'-d',
'invalid-device-id',
'--no-pub',
'--no-hot',
]),
throwsToolExit(),
);
expect(testLogger.statusText, contains("No supported devices found with name or id matching 'invalid-device-id'"));
expect(testLogger.statusText, contains('The following devices were found:'));
expect(testLogger.statusText, contains('FakeDevice (mobile) • fake_device • ios • (simulator)'));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('fails when targeted device is not Android with --device-user', () async {
final FakeDevice device = FakeDevice(isLocalEmulator: true);
testDeviceManager.devices = <Device>[device];
final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates();
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--device-user',
'10',
]), throwsToolExit(message: '--device-user is only supported for Android. At least one Android device is required.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
Stdio: () => FakeStdio(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('succeeds when targeted device is an Android device with --device-user', () async {
final FakeDevice device = FakeDevice(isLocalEmulator: true, platformType: PlatformType.android);
testDeviceManager.devices = <Device>[device];
final TestRunCommandThatOnlyValidates command = TestRunCommandThatOnlyValidates();
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--device-user',
'10',
]);
// Finishes normally without error.
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
Stdio: () => FakeStdio(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('fails when v1 FlutterApplication is detected', () async {
fs.file('pubspec.yaml').createSync();
fs.file('android/AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync('''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.v1">
<application
android:name="io.flutter.app.FlutterApplication">
</application>
</manifest>
''', flush: true);
fs.file('.packages').writeAsStringSync('\n');
fs.file('lib/main.dart').createSync(recursive: true);
final AndroidDevice device = AndroidDevice('1234',
modelID: 'TestModel',
logger: testLogger,
platform: FakePlatform(),
androidSdk: FakeAndroidSdk(),
fileSystem: fs,
processManager: FakeProcessManager.any(),
);
testDeviceManager.devices = <Device>[device];
final RunCommand command = RunCommand();
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--pub',
]), throwsToolExit(message: 'Build failed due to use of deprecated Android v1 embedding.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
Stdio: () => FakeStdio(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('fails when v1 metadata is detected', () async {
fs.file('pubspec.yaml').createSync();
fs.file('android/AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync('''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.v1">
<application >
<meta-data
android:name="flutterEmbedding"
android:value="1" />
</application>
</manifest>
''', flush: true);
fs.file('.packages').writeAsStringSync('\n');
fs.file('lib/main.dart').createSync(recursive: true);
final AndroidDevice device = AndroidDevice('1234',
modelID: 'TestModel',
logger: testLogger,
platform: FakePlatform(),
androidSdk: FakeAndroidSdk(),
fileSystem: fs,
processManager: FakeProcessManager.any(),
);
testDeviceManager.devices = <Device>[device];
final RunCommand command = RunCommand();
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--pub',
]), throwsToolExit(message: 'Build failed due to use of deprecated Android v1 embedding.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
DeviceManager: () => testDeviceManager,
Stdio: () => FakeStdio(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('shows unsupported devices when no supported devices are found', () async {
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(
targetPlatform: TargetPlatform.android_arm,
isLocalEmulator: true,
sdkNameAndVersion: 'api-14',
isSupported: false,
);
testDeviceManager.devices = <Device>[mockDevice];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
]),
throwsA(isA<ToolExit>().having((ToolExit error) => error.message, 'message', isNull)),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace('No supported devices connected.'),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace('The following devices were found, but are not supported by this project:'),
);
expect(
testLogger.statusText,
containsIgnoringWhitespace(
userMessages.flutterMissPlatformProjects(
Device.devicesPlatformTypes(<Device>[mockDevice]),
),
),
);
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
});
testUsingContext('forwards --uninstall-only to DebuggingOptions', () async {
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(
sdkNameAndVersion: 'iOS 13',
)..startAppSuccess = false;
testDeviceManager.devices = <Device>[mockDevice];
// Causes swift to be detected in the analytics.
fs.currentDirectory.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true);
await expectToolExitLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--uninstall-first',
]), isNull);
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.uninstallFirst, isTrue);
}, overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Usage: () => usage,
});
testUsingContext('passes device target platform to usage', () async {
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(sdkNameAndVersion: 'iOS 13')
..startAppSuccess = false;
testDeviceManager.devices = <Device>[mockDevice];
// Causes swift to be detected in the analytics.
fs.currentDirectory.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true);
await expectToolExitLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
]), isNull);
expect(usage.commands, contains(
TestUsageCommand('run', parameters: CustomDimensions.fromMap(<String, String>{
'cd3': 'false', 'cd4': 'ios', 'cd22': 'iOS 13',
'cd23': 'debug', 'cd18': 'false', 'cd15': 'swift', 'cd31': 'true',
'cd57': 'usb',
'cd58': 'false',
})
)));
expect(
fakeAnalytics.sentEvents,
contains(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: globals.stdio.hasTerminal,
runIsEmulator: false,
runTargetName: 'ios',
runTargetOsVersion: 'iOS 13',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: 'swift',
runIOSInterfaceType: 'usb',
runIsTest: false,
),
),
);
}, overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
Usage: () => usage,
analytics.Analytics: () => fakeAnalytics,
});
testUsingContext('correctly reports tests to usage', () async {
fs.currentDirectory.childDirectory('test').childFile('widget_test.dart').createSync(recursive: true);
fs.currentDirectory.childDirectory('ios').childFile('AppDelegate.swift').createSync(recursive: true);
final RunCommand command = RunCommand();
final FakeDevice mockDevice = FakeDevice(sdkNameAndVersion: 'iOS 13')
..startAppSuccess = false;
testDeviceManager.devices = <Device>[mockDevice];
await expectToolExitLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'test/widget_test.dart',
]), isNull);
expect(usage.commands, contains(
TestUsageCommand('run', parameters: CustomDimensions.fromMap(<String, String>{
'cd3': 'false', 'cd4': 'ios', 'cd22': 'iOS 13',
'cd23': 'debug', 'cd18': 'false', 'cd15': 'swift', 'cd31': 'true',
'cd57': 'usb',
'cd58': 'true',
})),
));
expect(
fakeAnalytics.sentEvents,
contains(
analytics.Event.commandUsageValues(
workflow: 'run',
commandHasTerminal: globals.stdio.hasTerminal,
runIsEmulator: false,
runTargetName: 'ios',
runTargetOsVersion: 'iOS 13',
runModeName: 'debug',
runProjectModule: false,
runProjectHostLanguage: 'swift',
runIOSInterfaceType: 'usb',
runIsTest: true,
),
),
);
}, overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Stdio: () => FakeStdio(),
Usage: () => usage,
analytics.Analytics: () => fakeAnalytics,
});
group('--machine', () {
testUsingContext('enables multidex by default', () async {
final DaemonCapturingRunCommand command = DaemonCapturingRunCommand();
final FakeDevice device = FakeDevice();
testDeviceManager.devices = <Device>[device];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--machine',
'-d',
device.id,
]),
throwsToolExit(),
);
expect(command.appDomain.multidexEnabled, isTrue);
}, overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Usage: () => usage,
Stdio: () => FakeStdio(),
Logger: () => AppRunLogger(parent: BufferLogger.test()),
});
testUsingContext('can disable multidex with --no-multidex', () async {
final DaemonCapturingRunCommand command = DaemonCapturingRunCommand();
final FakeDevice device = FakeDevice();
testDeviceManager.devices = <Device>[device];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-multidex',
'--machine',
'-d',
device.id,
]),
throwsToolExit(),
);
expect(command.appDomain.multidexEnabled, isFalse);
}, overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Usage: () => usage,
Stdio: () => FakeStdio(),
Logger: () => AppRunLogger(parent: BufferLogger.test()),
});
testUsingContext('can pass --device-user', () async {
final DaemonCapturingRunCommand command = DaemonCapturingRunCommand();
final FakeDevice device = FakeDevice(platformType: PlatformType.android);
testDeviceManager.devices = <Device>[device];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--machine',
'--device-user',
'10',
'-d',
device.id,
]),
throwsToolExit(),
);
expect(command.appDomain.userIdentifier, '10');
}, overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Usage: () => usage,
Stdio: () => FakeStdio(),
Logger: () => AppRunLogger(parent: BufferLogger.test()),
});
testUsingContext('can disable devtools with --no-devtools', () async {
final DaemonCapturingRunCommand command = DaemonCapturingRunCommand();
final FakeDevice device = FakeDevice();
testDeviceManager.devices = <Device>[device];
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-devtools',
'--machine',
'-d',
device.id,
]),
throwsToolExit(),
);
expect(command.appDomain.enableDevTools, isFalse);
}, overrides: <Type, Generator>{
Artifacts: () => artifacts,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
DeviceManager: () => testDeviceManager,
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
Usage: () => usage,
Stdio: () => FakeStdio(),
Logger: () => AppRunLogger(parent: BufferLogger.test()),
});
});
});
group('Fatal Logs', () {
late TestRunCommandWithFakeResidentRunner command;
late MemoryFileSystem fs;
setUp(() {
command = TestRunCommandWithFakeResidentRunner()
..fakeResidentRunner = FakeResidentRunner();
fs = MemoryFileSystem.test();
});
testUsingContext("doesn't fail if --fatal-warnings specified and no warnings occur", () async {
try {
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--${FlutterOptions.kFatalWarnings}',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("doesn't fail if --fatal-warnings not specified", () async {
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
try {
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
]);
} on Exception {
fail('Unexpected exception thrown');
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and warnings emitted', () async {
testLogger.printWarning('Warning: Mild annoyance Will Robinson!');
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received warning output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails if --fatal-warnings specified and errors emitted', () async {
testLogger.printError('Error: Danger Will Robinson!');
await expectLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--no-hot',
'--${FlutterOptions.kFatalWarnings}',
]), throwsToolExit(message: 'Logger received error output during the run, and "--${FlutterOptions.kFatalWarnings}" is enabled.'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
testUsingContext('should only request artifacts corresponding to connected devices', () async {
testDeviceManager.devices = <Device>[FakeDevice(targetPlatform: TargetPlatform.android_arm)];
expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.androidGenSnapshot,
}));
testDeviceManager.devices = <Device>[FakeDevice()];
expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.iOS,
}));
testDeviceManager.devices = <Device>[
FakeDevice(),
FakeDevice(targetPlatform: TargetPlatform.android_arm),
];
expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.iOS,
DevelopmentArtifact.androidGenSnapshot,
}));
testDeviceManager.devices = <Device>[
FakeDevice(targetPlatform: TargetPlatform.web_javascript),
];
expect(await RunCommand().requiredArtifacts, unorderedEquals(<DevelopmentArtifact>{
DevelopmentArtifact.universal,
DevelopmentArtifact.web,
}));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
group('usageValues', () {
testUsingContext('with only non-iOS usb device', () async {
final List<Device> devices = <Device>[
FakeDevice(targetPlatform: TargetPlatform.android_arm, platformType: PlatformType.android),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final CustomDimensions dimensions = await command.usageValues;
expect(dimensions, const CustomDimensions(
commandRunIsEmulator: false,
commandRunTargetName: 'android-arm',
commandRunTargetOsVersion: '',
commandRunModeName: 'debug',
commandRunProjectModule: false,
commandRunProjectHostLanguage: '',
commandRunIsTest: false,
));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('with only iOS usb device', () async {
final List<Device> devices = <Device>[
FakeIOSDevice(sdkNameAndVersion: 'iOS 16.2'),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final CustomDimensions dimensions = await command.usageValues;
expect(dimensions, const CustomDimensions(
commandRunIsEmulator: false,
commandRunTargetName: 'ios',
commandRunTargetOsVersion: 'iOS 16.2',
commandRunModeName: 'debug',
commandRunProjectModule: false,
commandRunProjectHostLanguage: '',
commandRunIOSInterfaceType: 'usb',
commandRunIsTest: false,
));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('with only iOS wireless device', () async {
final List<Device> devices = <Device>[
FakeIOSDevice(
connectionInterface: DeviceConnectionInterface.wireless,
sdkNameAndVersion: 'iOS 16.2',
),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final CustomDimensions dimensions = await command.usageValues;
expect(dimensions, const CustomDimensions(
commandRunIsEmulator: false,
commandRunTargetName: 'ios',
commandRunTargetOsVersion: 'iOS 16.2',
commandRunModeName: 'debug',
commandRunProjectModule: false,
commandRunProjectHostLanguage: '',
commandRunIOSInterfaceType: 'wireless',
commandRunIsTest: false,
));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('with both iOS usb and wireless devices', () async {
final List<Device> devices = <Device>[
FakeIOSDevice(
connectionInterface: DeviceConnectionInterface.wireless,
sdkNameAndVersion: 'iOS 16.2',
),
FakeIOSDevice(sdkNameAndVersion: 'iOS 16.2'),
];
final TestRunCommandForUsageValues command = TestRunCommandForUsageValues(devices: devices);
final CommandRunner<void> runner = createTestCommandRunner(command);
try {
// run the command so that CLI args are parsed
await runner.run(<String>['run']);
} on ToolExit catch (error) {
// we can ignore the ToolExit, as we are only interested in
// command.usageValues.
expect(
error,
isA<ToolExit>().having(
(ToolExit exception) => exception.message,
'message',
contains('No pubspec.yaml file found'),
),
);
}
final CustomDimensions dimensions = await command.usageValues;
expect(dimensions, const CustomDimensions(
commandRunIsEmulator: false,
commandRunTargetName: 'multiple',
commandRunTargetOsVersion: 'multiple',
commandRunModeName: 'debug',
commandRunProjectModule: false,
commandRunProjectHostLanguage: '',
commandRunIOSInterfaceType: 'wireless',
commandRunIsTest: false,
));
}, overrides: <Type, Generator>{
DeviceManager: () => testDeviceManager,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
});
group('--web-header', () {
setUp(() {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
final FakeDevice device = FakeDevice(isLocalEmulator: true, platformType: PlatformType.android);
testDeviceManager.devices = <Device>[device];
});
testUsingContext('can accept simple, valid values', () async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub', '--no-hot',
'--web-header', 'foo = bar',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webHeaders, <String, String>{'foo': 'bar'});
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
DeviceManager: () => testDeviceManager,
});
testUsingContext('throws a ToolExit when no value is provided', () async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub', '--no-hot',
'--web-header',
'foo',
]), throwsToolExit(message: 'Invalid web headers: foo'));
await expectLater(
() => command.createDebuggingOptions(true),
throwsToolExit(),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
DeviceManager: () => testDeviceManager,
});
testUsingContext('throws a ToolExit when value includes delimiter characters', () async {
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.packages').createSync();
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub', '--no-hot',
'--web-header', 'hurray/headers=flutter',
]), throwsToolExit());
await expectLater(
() => command.createDebuggingOptions(true),
throwsToolExit(message: 'Invalid web headers: hurray/headers=flutter'),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
DeviceManager: () => testDeviceManager,
});
testUsingContext('accepts headers with commas in them', () async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub', '--no-hot',
'--web-header', 'hurray=flutter,flutter=hurray',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webHeaders, <String, String>{
'hurray': 'flutter,flutter=hurray'
});
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
DeviceManager: () => testDeviceManager,
});
});
});
group('dart-defines and web-renderer options', () {
late List<String> dartDefines;
setUp(() {
dartDefines = <String>[];
});
test('auto web-renderer with no dart-defines', () {
dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.auto);
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=true']);
});
test('canvaskit web-renderer with no dart-defines', () {
dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.canvaskit);
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']);
});
test('html web-renderer with no dart-defines', () {
dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.html);
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']);
});
test('auto web-renderer with existing dart-defines', () {
dartDefines = <String>['FLUTTER_WEB_USE_SKIA=false'];
dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.auto);
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=true']);
});
test('canvaskit web-renderer with no dart-defines', () {
dartDefines = <String>['FLUTTER_WEB_USE_SKIA=false'];
dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.canvaskit);
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=true']);
});
test('html web-renderer with no dart-defines', () {
dartDefines = <String>['FLUTTER_WEB_USE_SKIA=true'];
dartDefines = FlutterCommand.updateDartDefines(dartDefines, WebRendererMode.html);
expect(dartDefines, <String>['FLUTTER_WEB_AUTO_DETECT=false','FLUTTER_WEB_USE_SKIA=false']);
});
});
group('terminal', () {
late FakeAnsiTerminal fakeTerminal;
setUp(() {
fakeTerminal = FakeAnsiTerminal();
});
testUsingContext('Flutter run sets terminal singleCharMode to false on exit', () async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]);
// The sync completer where we initially set `terminal.singleCharMode` to
// `true` does not execute in unit tests, so explicitly check the
// `setSingleCharModeHistory` that the finally block ran, setting this
// back to `false`.
expect(fakeTerminal.setSingleCharModeHistory, contains(false));
}, overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Flutter run catches StdinException while setting terminal singleCharMode to false', () async {
fakeTerminal.hasStdin = false;
final FakeResidentRunner residentRunner = FakeResidentRunner();
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
try {
await createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]);
} catch (err) { // ignore: avoid_catches_without_on_clauses
fail('Expected no error, got $err');
}
expect(fakeTerminal.setSingleCharModeHistory, isEmpty);
}, overrides: <Type, Generator>{
AnsiTerminal: () => fakeTerminal,
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
});
testUsingContext('Flutter run catches service has disappear errors and throws a tool exit', () async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError('flutter._listViews', RPCErrorCodes.kServiceDisappeared, '');
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectToolExitLater(createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]), contains('Lost connection to device.'));
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Flutter run does not catch other RPC errors', () async {
final FakeResidentRunner residentRunner = FakeResidentRunner();
residentRunner.rpcError = RPCError('flutter._listViews', RPCErrorCodes.kInvalidParams, '');
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
command.fakeResidentRunner = residentRunner;
await expectLater(() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]), throwsA(isA<RPCError>()));
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Passes sksl bundle info the build options', () async {
final TestRunCommandWithFakeResidentRunner command = TestRunCommandWithFakeResidentRunner();
await expectLater(() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
'--bundle-sksl-path=foo.json',
]), throwsToolExit(message: 'No SkSL shader bundle found at foo.json'));
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Configures web connection options to use web sockets by default', () async {
final RunCommand command = RunCommand();
await expectLater(() => createTestCommandRunner(command).run(<String>[
'run',
'--no-pub',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webUseSseForDebugBackend, false);
expect(options.webUseSseForDebugProxy, false);
expect(options.webUseSseForInjectedClient, false);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('flags propagate to debugging options', () async {
final RunCommand command = RunCommand();
await expectLater(() => createTestCommandRunner(command).run(<String>[
'run',
'--start-paused',
'--disable-service-auth-codes',
'--use-test-fonts',
'--trace-skia',
'--trace-systrace',
'--trace-to-file=path/to/trace.binpb',
'--verbose-system-logs',
'--null-assertions',
'--native-null-assertions',
'--enable-impeller',
'--enable-vulkan-validation',
'--impeller-force-gl',
'--trace-systrace',
'--enable-software-rendering',
'--skia-deterministic-rendering',
'--enable-embedder-api',
'--ci',
]), throwsToolExit());
final DebuggingOptions options = await command.createDebuggingOptions(false);
expect(options.startPaused, true);
expect(options.disableServiceAuthCodes, true);
expect(options.useTestFonts, true);
expect(options.traceSkia, true);
expect(options.traceSystrace, true);
expect(options.traceToFile, 'path/to/trace.binpb');
expect(options.verboseSystemLogs, true);
expect(options.nullAssertions, true);
expect(options.nativeNullAssertions, true);
expect(options.traceSystrace, true);
expect(options.enableImpeller, ImpellerStatus.enabled);
expect(options.enableVulkanValidation, true);
expect(options.impellerForceGL, true);
expect(options.enableSoftwareRendering, true);
expect(options.skiaDeterministicRendering, true);
expect(options.usingCISystem, true);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('fails when "--web-launch-url" is not supported', () async {
final RunCommand command = RunCommand();
await expectLater(
() => createTestCommandRunner(command).run(<String>[
'run',
'--web-launch-url=http://flutter.dev',
]),
throwsA(isException.having(
(Exception exception) => exception.toString(),
'toString',
isNot(contains('web-launch-url')),
)),
);
final DebuggingOptions options = await command.createDebuggingOptions(true);
expect(options.webLaunchUrl, 'http://flutter.dev');
final RegExp pattern = RegExp(r'^((http)?:\/\/)[^\s]+');
expect(pattern.hasMatch(options.webLaunchUrl!), true);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger.test(),
});
}
class TestDeviceManager extends DeviceManager {
TestDeviceManager({required super.logger});
List<Device> devices = <Device>[];
@override
List<DeviceDiscovery> get deviceDiscoverers {
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
devices.forEach(discoverer.addDevice);
return <DeviceDiscovery>[discoverer];
}
}
class FakeAndroidSdk extends Fake implements AndroidSdk {
@override
String get adbPath => 'adb';
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device {
FakeDevice({
bool isLocalEmulator = false,
TargetPlatform targetPlatform = TargetPlatform.ios,
String sdkNameAndVersion = '',
PlatformType platformType = PlatformType.ios,
bool isSupported = true,
}): _isLocalEmulator = isLocalEmulator,
_targetPlatform = targetPlatform,
_sdkNameAndVersion = sdkNameAndVersion,
_platformType = platformType,
_isSupported = isSupported;
static const int kSuccess = 1;
static const int kFailure = -1;
final TargetPlatform _targetPlatform;
final bool _isLocalEmulator;
final String _sdkNameAndVersion;
final PlatformType _platformType;
final bool _isSupported;
@override
Category get category => Category.mobile;
@override
String get id => 'fake_device';
Never _throwToolExit(int code) => throwToolExit('FakeDevice tool exit', exitCode: code);
@override
Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator);
@override
bool supportsRuntimeMode(BuildMode mode) => true;
@override
Future<bool> get supportsHardwareRendering async => true;
@override
bool supportsHotReload = false;
@override
bool get supportsHotRestart => true;
@override
bool get supportsFastStart => false;
@override
bool get ephemeral => true;
@override
bool get isConnected => true;
@override
DeviceConnectionInterface get connectionInterface =>
DeviceConnectionInterface.attached;
bool supported = true;
@override
bool isSupportedForProject(FlutterProject flutterProject) => _isSupported;
@override
bool isSupported() => supported;
@override
Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion);
@override
Future<String> get targetPlatformDisplayName async =>
getNameForTargetPlatform(await targetPlatform);
@override
DeviceLogReader getLogReader({
ApplicationPackage? app,
bool includePastLogs = false,
}) {
return FakeDeviceLogReader();
}
@override
String get name => 'FakeDevice';
@override
Future<TargetPlatform> get targetPlatform async => _targetPlatform;
@override
PlatformType get platformType => _platformType;
late bool startAppSuccess;
@override
DevFSWriter? createDevFSWriter(
ApplicationPackage? app,
String? userIdentifier,
) {
return null;
}
@override
Future<LaunchResult> startApp(
ApplicationPackage? package, {
String? mainPath,
String? route,
required DebuggingOptions debuggingOptions,
Map<String, Object?> platformArgs = const <String, Object?>{},
bool prebuiltApplication = false,
bool usesTerminalUi = true,
bool ipv6 = false,
String? userIdentifier,
}) async {
if (!startAppSuccess) {
return LaunchResult.failed();
}
if (startAppSuccess) {
return LaunchResult.succeeded();
}
final String dartFlags = debuggingOptions.dartFlags;
// In release mode, --dart-flags should be set to the empty string and
// provided flags should be dropped. In debug and profile modes,
// --dart-flags should not be empty.
if (debuggingOptions.buildInfo.isRelease) {
if (dartFlags.isNotEmpty) {
_throwToolExit(kFailure);
}
_throwToolExit(kSuccess);
} else {
if (dartFlags.isEmpty) {
_throwToolExit(kFailure);
}
_throwToolExit(kSuccess);
}
}
}
// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeIOSDevice extends Fake implements IOSDevice {
FakeIOSDevice({
this.connectionInterface = DeviceConnectionInterface.attached,
bool isLocalEmulator = false,
String sdkNameAndVersion = '',
}): _isLocalEmulator = isLocalEmulator,
_sdkNameAndVersion = sdkNameAndVersion;
final bool _isLocalEmulator;
final String _sdkNameAndVersion;
@override
Future<bool> get isLocalEmulator => Future<bool>.value(_isLocalEmulator);
@override
Future<String> get sdkNameAndVersion => Future<String>.value(_sdkNameAndVersion);
@override
final DeviceConnectionInterface connectionInterface;
@override
bool get isWirelesslyConnected =>
connectionInterface == DeviceConnectionInterface.wireless;
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
}
class TestRunCommandForUsageValues extends RunCommand {
TestRunCommandForUsageValues({
List<Device>? devices,
}) {
this.devices = devices;
}
@override
Future<BuildInfo> getBuildInfo({ BuildMode? forcedBuildMode, File? forcedTargetFile }) async {
return const BuildInfo(BuildMode.debug, null, treeShakeIcons: false);
}
}
class TestRunCommandWithFakeResidentRunner extends RunCommand {
late FakeResidentRunner fakeResidentRunner;
@override
Future<ResidentRunner> createRunner({
required bool hotMode,
required List<FlutterDevice> flutterDevices,
required String? applicationBinaryPath,
required FlutterProject flutterProject,
}) async {
return fakeResidentRunner;
}
@override
// ignore: must_call_super
Future<void> validateCommand() async {
devices = <Device>[FakeDevice()..supportsHotReload = true];
}
}
class TestRunCommandThatOnlyValidates extends RunCommand {
@override
Future<FlutterCommandResult> runCommand() async {
return FlutterCommandResult.success();
}
}
class FakeResidentRunner extends Fake implements ResidentRunner {
RPCError? rpcError;
@override
Future<int> run({
Completer<DebugConnectionInfo>? connectionInfoCompleter,
Completer<void>? appStartedCompleter,
bool enableDevTools = false,
String? route,
}) async {
await null;
if (rpcError != null) {
throw rpcError!;
}
return 0;
}
}
class DaemonCapturingRunCommand extends RunCommand {
late Daemon daemon;
late CapturingAppDomain appDomain;
@override
Daemon createMachineDaemon() {
daemon = super.createMachineDaemon();
appDomain = daemon.appDomain = CapturingAppDomain(daemon);
daemon.registerDomain(appDomain);
return daemon;
}
}
class CapturingAppDomain extends AppDomain {
CapturingAppDomain(super.daemon);
bool? multidexEnabled;
String? userIdentifier;
bool? enableDevTools;
@override
Future<AppInstance> startApp(
Device device,
String projectDirectory,
String target,
String? route,
DebuggingOptions options,
bool enableHotReload, {
File? applicationBinary,
required bool trackWidgetCreation,
String? projectRootPath,
String? packagesFilePath,
String? dillOutputPath,
bool ipv6 = false,
bool multidexEnabled = false,
String? isolateFilter,
bool machine = true,
String? userIdentifier,
bool enableDevTools = true,
String? flavor,
}) async {
this.multidexEnabled = multidexEnabled;
this.userIdentifier = userIdentifier;
this.enableDevTools = enableDevTools;
throwToolExit('');
}
}
class FakeAnsiTerminal extends Fake implements AnsiTerminal {
/// Setting to false will cause operations to Stdin to throw a [StdinException].
bool hasStdin = true;
@override
bool usesTerminalUi = false;
/// A list of all the calls to the [singleCharMode] setter.
List<bool> setSingleCharModeHistory = <bool>[];
@override
set singleCharMode(bool value) {
if (!hasStdin) {
throw const StdinException('Error setting terminal line mode', OSError('The handle is invalid', 6));
}
setSingleCharModeHistory.add(value);
}
@override
bool get singleCharMode => setSingleCharModeHistory.last;
}