blob: b812679f9862d502c0300794a4eb24307e590abc [file] [log] [blame]
// 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 show IOSink, ProcessSignal, Stdout, StdoutException;
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/bot_detector.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/os.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:test/fake.dart';
/// Environment with DYLD_LIBRARY_PATH=/path/to/libraries
class FakeDyldEnvironmentArtifact extends ArtifactSet {
FakeDyldEnvironmentArtifact() : super(DevelopmentArtifact.iOS);
@override
Map<String, String> get environment => <String, String>{
'DYLD_LIBRARY_PATH': '/path/to/libraries',
};
@override
Future<bool> isUpToDate(FileSystem fileSystem) => Future<bool>.value(true);
@override
String get name => 'fake';
@override
Future<void> update(ArtifactUpdater artifactUpdater, Logger logger, FileSystem fileSystem, OperatingSystemUtils operatingSystemUtils, {bool offline = false}) async {
}
}
/// A fake process implementation which can be provided all necessary values.
class FakeProcess implements Process {
FakeProcess({
this.pid = 1,
Future<int>? exitCode,
IOSink? stdin,
this.stdout = const Stream<List<int>>.empty(),
this.stderr = const Stream<List<int>>.empty(),
}) : exitCode = exitCode ?? Future<int>.value(0),
stdin = stdin ?? MemoryIOSink();
@override
final int pid;
@override
final Future<int> exitCode;
@override
final io.IOSink stdin;
@override
final Stream<List<int>> stdout;
@override
final Stream<List<int>> stderr;
@override
bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
return true;
}
}
/// An IOSink that completes a future with the first line written to it.
class CompleterIOSink extends MemoryIOSink {
CompleterIOSink({
this.throwOnAdd = false,
});
final bool throwOnAdd;
final Completer<List<int>> _completer = Completer<List<int>>();
Future<List<int>> get future => _completer.future;
@override
void add(List<int> data) {
if (!_completer.isCompleted) {
// When throwOnAdd is true, complete with empty so any expected output
// doesn't appear.
_completer.complete(throwOnAdd ? <int>[] : data);
}
if (throwOnAdd) {
throw Exception('CompleterIOSink Error');
}
super.add(data);
}
}
/// An IOSink that collects whatever is written to it.
class MemoryIOSink implements IOSink {
@override
Encoding encoding = utf8;
final List<List<int>> writes = <List<int>>[];
@override
void add(List<int> data) {
writes.add(data);
}
@override
Future<void> addStream(Stream<List<int>> stream) {
final Completer<void> completer = Completer<void>();
late StreamSubscription<List<int>> sub;
sub = stream.listen(
(List<int> data) {
try {
add(data);
// Catches all exceptions to propagate them to the completer.
} catch (err, stack) { // ignore: avoid_catches_without_on_clauses
sub.cancel();
completer.completeError(err, stack);
}
},
onError: completer.completeError,
onDone: completer.complete,
cancelOnError: true,
);
return completer.future;
}
@override
void writeCharCode(int charCode) {
add(<int>[charCode]);
}
@override
void write(Object? obj) {
add(encoding.encode('$obj'));
}
@override
void writeln([ Object? obj = '' ]) {
add(encoding.encode('$obj\n'));
}
@override
void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
bool addSeparator = false;
for (final dynamic object in objects) {
if (addSeparator) {
write(separator);
}
write(object);
addSeparator = true;
}
}
@override
void addError(dynamic error, [ StackTrace? stackTrace ]) {
throw UnimplementedError();
}
@override
Future<void> get done => close();
@override
Future<void> close() async { }
@override
Future<void> flush() async { }
void clear() {
writes.clear();
}
String getAndClear() {
final String result = utf8.decode(writes.expand((List<int> l) => l).toList());
clear();
return result;
}
}
class MemoryStdout extends MemoryIOSink implements io.Stdout {
@override
bool get hasTerminal => _hasTerminal;
set hasTerminal(bool value) {
_hasTerminal = value;
}
bool _hasTerminal = true;
@override
// ignore: override_on_non_overriding_member
String get lineTerminator => '\n';
@override
// ignore: override_on_non_overriding_member
set lineTerminator(String value) {
throw UnimplementedError('Setting the line terminator is not supported');
}
@override
io.IOSink get nonBlocking => this;
@override
bool get supportsAnsiEscapes => _supportsAnsiEscapes;
set supportsAnsiEscapes(bool value) {
_supportsAnsiEscapes = value;
}
bool _supportsAnsiEscapes = true;
@override
int get terminalColumns {
if (_terminalColumns != null) {
return _terminalColumns!;
}
throw const io.StdoutException('unspecified mock value');
}
set terminalColumns(int value) => _terminalColumns = value;
int? _terminalColumns;
@override
int get terminalLines {
if (_terminalLines != null) {
return _terminalLines!;
}
throw const io.StdoutException('unspecified mock value');
}
set terminalLines(int value) => _terminalLines = value;
int? _terminalLines;
}
/// A Stdio that collects stdout and supports simulated stdin.
class FakeStdio extends Stdio {
final MemoryStdout _stdout = MemoryStdout()..terminalColumns = 80;
final MemoryIOSink _stderr = MemoryIOSink();
final FakeStdin _stdin = FakeStdin();
@override
MemoryStdout get stdout => _stdout;
@override
MemoryIOSink get stderr => _stderr;
@override
Stream<List<int>> get stdin => _stdin;
void simulateStdin(String line) {
_stdin.controller.add(utf8.encode('$line\n'));
}
@override
bool hasTerminal = true;
List<String> get writtenToStdout => _stdout.writes.map<String>(_stdout.encoding.decode).toList();
List<String> get writtenToStderr => _stderr.writes.map<String>(_stderr.encoding.decode).toList();
}
class FakeStdin extends Fake implements Stdin {
final StreamController<List<int>> controller = StreamController<List<int>>();
void Function(bool mode)? echoModeCallback;
bool _echoMode = true;
@override
bool get echoMode => _echoMode;
@override
set echoMode(bool mode) {
_echoMode = mode;
if (echoModeCallback != null) {
echoModeCallback!(mode);
}
}
@override
bool lineMode = true;
@override
Stream<S> transform<S>(StreamTransformer<List<int>, S> transformer) {
return controller.stream.transform(transformer);
}
@override
StreamSubscription<List<int>> listen(
void Function(List<int> event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return controller.stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
}
class FakePlistParser implements PlistParser {
FakePlistParser([Map<String, Object>? underlyingValues]):
_underlyingValues = underlyingValues ?? <String, Object>{};
final Map<String, Object> _underlyingValues;
void setProperty(String key, Object value) {
_underlyingValues[key] = value;
}
@override
String? plistXmlContent(String plistFilePath) => throw UnimplementedError();
@override
String? plistJsonContent(String filePath, {bool sorted = false}) {
throw UnimplementedError();
}
@override
Map<String, Object> parseFile(String plistFilePath) {
return _underlyingValues;
}
@override
T? getValueFromFile<T>(String plistFilePath, String key) {
return _underlyingValues[key] as T?;
}
@override
bool replaceKey(String plistFilePath, {required String key, String? value}) {
if (value == null) {
_underlyingValues.remove(key);
return true;
}
setProperty(key, value);
return true;
}
}
class FakeBotDetector implements BotDetector {
const FakeBotDetector(bool isRunningOnBot)
: _isRunningOnBot = isRunningOnBot;
@override
Future<bool> get isRunningOnBot async => _isRunningOnBot;
final bool _isRunningOnBot;
}
class FakeFlutterVersion implements FlutterVersion {
FakeFlutterVersion({
this.branch = 'master',
this.dartSdkVersion = '12',
this.devToolsVersion = '2.8.0',
this.engineRevision = 'abcdefghijklmnopqrstuvwxyz',
this.engineRevisionShort = 'abcde',
this.repositoryUrl = 'https://github.com/flutter/flutter.git',
this.frameworkVersion = '0.0.0',
this.frameworkRevision = '11111111111111111111',
this.frameworkRevisionShort = '11111',
this.frameworkAge = '0 hours ago',
this.frameworkCommitDate = '12/01/01',
this.gitTagVersion = const GitTagVersion.unknown(),
this.flutterRoot = '/path/to/flutter',
this.nextFlutterVersion,
});
final String branch;
bool get didFetchTagsAndUpdate => _didFetchTagsAndUpdate;
bool _didFetchTagsAndUpdate = false;
/// Will be returned by [fetchTagsAndGetVersion] if not null.
final FlutterVersion? nextFlutterVersion;
@override
FlutterVersion fetchTagsAndGetVersion({
SystemClock clock = const SystemClock(),
}) {
_didFetchTagsAndUpdate = true;
return nextFlutterVersion ?? this;
}
bool get didCheckFlutterVersionFreshness => _didCheckFlutterVersionFreshness;
bool _didCheckFlutterVersionFreshness = false;
@override
String get channel {
if (kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) {
return branch;
}
return kUserBranch;
}
@override
final String flutterRoot;
@override
final String devToolsVersion;
@override
final String dartSdkVersion;
@override
final String engineRevision;
@override
final String engineRevisionShort;
@override
final String? repositoryUrl;
@override
final String frameworkVersion;
@override
final String frameworkRevision;
@override
final String frameworkRevisionShort;
@override
final String frameworkAge;
@override
final String frameworkCommitDate;
@override
final GitTagVersion gitTagVersion;
@override
FileSystem get fs => throw UnimplementedError('FakeFlutterVersion.fs is not implemented');
@override
Future<void> checkFlutterVersionFreshness() async {
_didCheckFlutterVersionFreshness = true;
}
@override
Future<void> ensureVersionFile() async { }
@override
String getBranchName({bool redactUnknownBranches = false}) {
if (!redactUnknownBranches || kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) {
return branch;
}
return kUserBranch;
}
@override
String getVersionString({bool redactUnknownBranches = false}) {
return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevision';
}
@override
Map<String, Object> toJson() {
return <String, Object>{};
}
}
// A test implementation of [FeatureFlags] that allows enabling without reading
// config. If not otherwise specified, all values default to false.
class TestFeatureFlags implements FeatureFlags {
TestFeatureFlags({
this.isLinuxEnabled = false,
this.isMacOSEnabled = false,
this.isWebEnabled = false,
this.isWindowsEnabled = false,
this.isAndroidEnabled = true,
this.isIOSEnabled = true,
this.isFuchsiaEnabled = false,
this.areCustomDevicesEnabled = false,
this.isCliAnimationEnabled = true,
this.isNativeAssetsEnabled = false,
this.isPreviewDeviceEnabled = false,
this.isSwiftPackageManagerEnabled = false,
});
@override
final bool isLinuxEnabled;
@override
final bool isMacOSEnabled;
@override
final bool isWebEnabled;
@override
final bool isWindowsEnabled;
@override
final bool isAndroidEnabled;
@override
final bool isIOSEnabled;
@override
final bool isFuchsiaEnabled;
@override
final bool areCustomDevicesEnabled;
@override
final bool isCliAnimationEnabled;
@override
final bool isNativeAssetsEnabled;
@override
final bool isPreviewDeviceEnabled;
@override
final bool isSwiftPackageManagerEnabled;
@override
bool isEnabled(Feature feature) {
return switch (feature) {
flutterWebFeature => isWebEnabled,
flutterLinuxDesktopFeature => isLinuxEnabled,
flutterMacOSDesktopFeature => isMacOSEnabled,
flutterWindowsDesktopFeature => isWindowsEnabled,
flutterAndroidFeature => isAndroidEnabled,
flutterIOSFeature => isIOSEnabled,
flutterFuchsiaFeature => isFuchsiaEnabled,
flutterCustomDevicesFeature => areCustomDevicesEnabled,
cliAnimation => isCliAnimationEnabled,
nativeAssets => isNativeAssetsEnabled,
_ => false,
};
}
}
class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
FakeOperatingSystemUtils({this.hostPlatform = HostPlatform.linux_x64});
final List<List<String>> chmods = <List<String>>[];
@override
void makeExecutable(File file) { }
@override
HostPlatform hostPlatform = HostPlatform.linux_x64;
@override
void chmod(FileSystemEntity entity, String mode) {
chmods.add(<String>[entity.path, mode]);
}
@override
File? which(String execName) => null;
@override
List<File> whichAll(String execName) => <File>[];
@override
int? getDirectorySize(Directory directory) => 10000000; // 10 MB / 9.5 MiB
@override
void unzip(File file, Directory targetDirectory) { }
@override
void unpack(File gzippedTarFile, Directory targetDirectory) { }
@override
Stream<List<int>> gzipLevel1Stream(Stream<List<int>> stream) => stream;
@override
String get name => 'fake OS name and version';
@override
String get pathVarSeparator => ';';
@override
Future<int> findFreePort({bool ipv6 = false}) async => 12345;
}
class FakeStopwatch implements Stopwatch {
@override
bool get isRunning => _isRunning;
bool _isRunning = false;
@override
void start() => _isRunning = true;
@override
void stop() => _isRunning = false;
@override
Duration elapsed = Duration.zero;
@override
int get elapsedMicroseconds => elapsed.inMicroseconds;
@override
int get elapsedMilliseconds => elapsed.inMilliseconds;
@override
int get elapsedTicks => elapsed.inMilliseconds;
@override
int get frequency => 1000;
@override
void reset() {
_isRunning = false;
elapsed = Duration.zero;
}
@override
String toString() => '$runtimeType $elapsed $isRunning';
}
class FakeStopwatchFactory implements StopwatchFactory {
FakeStopwatchFactory({
Stopwatch? stopwatch,
Map<String, Stopwatch>? stopwatches
}) : stopwatches = <String, Stopwatch>{
if (stopwatches != null) ...stopwatches,
if (stopwatch != null) '': stopwatch,
};
Map<String, Stopwatch> stopwatches;
@override
Stopwatch createStopwatch([String name = '']) {
return stopwatches[name] ?? FakeStopwatch();
}
}
class FakeFlutterProjectFactory implements FlutterProjectFactory {
@override
FlutterProject fromDirectory(Directory directory) {
return FlutterProject.fromDirectoryTest(directory);
}
@override
Map<String, FlutterProject> get projects => throw UnimplementedError();
}
class FakeAndroidSdk extends Fake implements AndroidSdk {
@override
late bool platformToolsAvailable;
@override
late bool licensesAvailable;
@override
AndroidSdkVersion? latestVersion;
}
class FakeAndroidStudio extends Fake implements AndroidStudio {
@override
String get javaPath => 'java';
}
class FakeJava extends Fake implements Java {
FakeJava({
this.javaHome = '/android-studio/jbr',
String binary = '/android-studio/jbr/bin/java',
Version? version,
bool canRun = true,
}): binaryPath = binary,
version = version ?? const Version.withText(19, 0, 2, 'openjdk 19.0.2 2023-01-17'),
_environment = <String, String>{
if (javaHome != null) Java.javaHomeEnvironmentVariable: javaHome,
'PATH': '/android-studio/jbr/bin',
},
_canRun = canRun;
@override
String? javaHome;
@override
String binaryPath;
final Map<String, String> _environment;
final bool _canRun;
@override
Map<String, String> get environment => _environment;
@override
Version? version;
@override
bool canRun() {
return _canRun;
}
}
class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
FakeDevtoolsLauncher({DevToolsServerAddress? serverAddress})
: _serverAddress = serverAddress;
@override
Future<void> get processStart => _processStarted.future;
final Completer<void> _processStarted = Completer<void>();
@override
Future<void> get ready => readyCompleter.future;
Completer<void> readyCompleter = Completer<void>()..complete();
@override
DevToolsServerAddress? activeDevToolsServer;
@override
Uri? devToolsUrl;
@override
Uri? dtdUri;
@override
bool printDtdUri = false;
final DevToolsServerAddress? _serverAddress;
@override
Future<DevToolsServerAddress?> serve() async => _serverAddress;
@override
Future<void> launch(Uri vmServiceUri, {List<String>? additionalArguments}) {
_processStarted.complete();
return Completer<void>().future;
}
bool closed = false;
@override
Future<void> close() async {
closed = true;
}
}