mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This reverts commit 7cdc23b3e1
.
The failure in the `native_assets_test` integration test on Windows was caused by the DevTools process not being shutdown by the `ColdRunner` when running the profile mode portion of the test. This resulted in the test being unable to clean up the project created by the test as DevTools was still holding onto a handle within the directory. This PR adds back the mistakenly removed DevTools shutdown logic in the `ColdRunner`.
1350 lines
50 KiB
Dart
1350 lines
50 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 'dart:typed_data';
|
|
|
|
import 'package:fake_async/fake_async.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_workflow.dart';
|
|
import 'package:flutter_tools/src/application_package.dart';
|
|
import 'package:flutter_tools/src/artifacts.dart';
|
|
import 'package:flutter_tools/src/base/dds.dart';
|
|
import 'package:flutter_tools/src/base/logger.dart';
|
|
import 'package:flutter_tools/src/base/utils.dart';
|
|
import 'package:flutter_tools/src/build_info.dart';
|
|
import 'package:flutter_tools/src/commands/daemon.dart';
|
|
import 'package:flutter_tools/src/daemon.dart';
|
|
import 'package:flutter_tools/src/device.dart';
|
|
import 'package:flutter_tools/src/features.dart';
|
|
import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
|
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
|
import 'package:flutter_tools/src/ios/ios_workflow.dart';
|
|
import 'package:flutter_tools/src/preview_device.dart';
|
|
import 'package:flutter_tools/src/resident_runner.dart';
|
|
import 'package:flutter_tools/src/vmservice.dart';
|
|
import 'package:flutter_tools/src/windows/windows_workflow.dart';
|
|
import 'package:test/fake.dart';
|
|
|
|
import '../../src/common.dart';
|
|
import '../../src/context.dart';
|
|
import '../../src/fake_devices.dart';
|
|
import '../../src/fakes.dart';
|
|
|
|
/// Runs a callback using FakeAsync.run while continually pumping the
|
|
/// microtask queue. This avoids a deadlock when tests `await` a Future
|
|
/// which queues a microtask that will not be processed unless the queue
|
|
/// is flushed.
|
|
Future<T> _runFakeAsync<T>(Future<T> Function(FakeAsync time) f) async {
|
|
return FakeAsync().run((FakeAsync time) async {
|
|
bool pump = true;
|
|
final Future<T> future = f(time).whenComplete(() => pump = false);
|
|
while (pump) {
|
|
time.flushMicrotasks();
|
|
}
|
|
return future;
|
|
});
|
|
}
|
|
|
|
class FakeDaemonStreams implements DaemonStreams {
|
|
final StreamController<DaemonMessage> inputs = StreamController<DaemonMessage>();
|
|
final StreamController<DaemonMessage> outputs = StreamController<DaemonMessage>();
|
|
|
|
@override
|
|
Stream<DaemonMessage> get inputStream {
|
|
return inputs.stream;
|
|
}
|
|
|
|
@override
|
|
void send(Map<String, Object?> message, [ List<int>? binary ]) {
|
|
outputs.add(DaemonMessage(message, binary != null ? Stream<List<int>>.value(binary) : null));
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
await inputs.close();
|
|
// In some tests, outputs have no listeners. We don't wait for outputs to close.
|
|
unawaited(outputs.close());
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
late Daemon daemon;
|
|
late NotifyingLogger notifyingLogger;
|
|
|
|
group('daemon', () {
|
|
late FakeDaemonStreams daemonStreams;
|
|
late DaemonConnection daemonConnection;
|
|
setUp(() {
|
|
BufferLogger bufferLogger;
|
|
bufferLogger = BufferLogger.test();
|
|
notifyingLogger = NotifyingLogger(verbose: false, parent: bufferLogger);
|
|
daemonStreams = FakeDaemonStreams();
|
|
daemonConnection = DaemonConnection(
|
|
daemonStreams: daemonStreams,
|
|
logger: bufferLogger,
|
|
);
|
|
});
|
|
|
|
tearDown(() async {
|
|
await daemon.shutdown();
|
|
notifyingLogger.dispose();
|
|
await daemonConnection.dispose();
|
|
});
|
|
|
|
testUsingContext('daemon.version command should succeed', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'daemon.version'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['result'], isNotEmpty);
|
|
expect(response.data['result'], isA<String>());
|
|
});
|
|
|
|
testUsingContext('daemon.getSupportedPlatforms command should succeed', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
// Use the flutter_gallery project which has a known set of supported platforms.
|
|
final String projectPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'flutter_gallery');
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'daemon.getSupportedPlatforms',
|
|
'params': <String, Object>{'projectRoot': projectPath},
|
|
}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['result'], isNotEmpty);
|
|
expect(
|
|
response.data['result']! as Map<String, Object?>,
|
|
const <String, Object>{
|
|
'platforms': <String>['macos', 'windows', 'windowsPreview'],
|
|
'platformTypes': <String, Map<String, Object>>{
|
|
'web': <String, Object>{
|
|
'isSupported': false,
|
|
'reasons': <Map<String, String>>[
|
|
<String, String>{
|
|
'reasonText': 'the Web feature is not enabled',
|
|
'fixText': 'Run "flutter config --enable-web"',
|
|
'fixCode': 'config',
|
|
},
|
|
],
|
|
},
|
|
'android': <String, Object>{
|
|
'isSupported': false,
|
|
'reasons': <Map<String, String>>[
|
|
<String, String>{
|
|
'reasonText': 'the Android feature is not enabled',
|
|
'fixText': 'Run "flutter config --enable-android"',
|
|
'fixCode': 'config',
|
|
},
|
|
],
|
|
},
|
|
'ios': <String, Object>{
|
|
'isSupported': false,
|
|
'reasons': <Map<String, String>>[
|
|
<String, String>{
|
|
'reasonText': 'the iOS feature is not enabled',
|
|
'fixText': 'Run "flutter config --enable-ios"',
|
|
'fixCode': 'config',
|
|
},
|
|
],
|
|
},
|
|
'linux': <String, Object>{
|
|
'isSupported': false,
|
|
'reasons': <Map<String, String>>[
|
|
<String, String>{
|
|
'reasonText': 'the Linux feature is not enabled',
|
|
'fixText': 'Run "flutter config --enable-linux-desktop"',
|
|
'fixCode': 'config',
|
|
},
|
|
],
|
|
},
|
|
'macos': <String, bool>{'isSupported': true},
|
|
'windows': <String, bool>{'isSupported': true},
|
|
'fuchsia': <String, Object>{
|
|
'isSupported': false,
|
|
'reasons': <Map<String, String>>[
|
|
<String, String>{
|
|
'reasonText': 'the Fuchsia feature is not enabled',
|
|
'fixText': 'Run "flutter config --enable-fuchsia"',
|
|
'fixCode': 'config',
|
|
},
|
|
<String, String>{
|
|
'reasonText': 'the Fuchsia platform is not enabled for this project',
|
|
'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory',
|
|
'fixCode': 'create',
|
|
},
|
|
],
|
|
},
|
|
'custom': <String, Object>{
|
|
'isSupported': false,
|
|
'reasons': <Map<String, String>>[
|
|
<String, String>{
|
|
'reasonText': 'the custom devices feature is not enabled',
|
|
'fixText': 'Run "flutter config --enable-custom-devices"',
|
|
'fixCode': 'config',
|
|
},
|
|
],
|
|
},
|
|
'windowsPreview': <String, bool>{'isSupported': true},
|
|
},
|
|
},
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
// Disable Android/iOS and enable macOS to make sure result is consistent and defaults are tested off.
|
|
FeatureFlags: () => TestFeatureFlags(
|
|
isAndroidEnabled: false,
|
|
isIOSEnabled: false,
|
|
isMacOSEnabled: true,
|
|
isPreviewDeviceEnabled: true,
|
|
isWindowsEnabled: true,
|
|
),
|
|
});
|
|
|
|
testUsingContext('printError should send daemon.logMessage event', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
globals.printError('daemon.logMessage test');
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
|
|
return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'error';
|
|
});
|
|
expect(response.data['id'], isNull);
|
|
expect(response.data['event'], 'daemon.logMessage');
|
|
final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
|
|
expect(logMessage['level'], 'error');
|
|
expect(logMessage['message'], 'daemon.logMessage test');
|
|
}, overrides: <Type, Generator>{
|
|
Logger: () => notifyingLogger,
|
|
});
|
|
|
|
testUsingContext('printWarning should send daemon.logMessage event', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
globals.printWarning('daemon.logMessage test');
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
|
|
return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'warning';
|
|
});
|
|
expect(response.data['id'], isNull);
|
|
expect(response.data['event'], 'daemon.logMessage');
|
|
final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
|
|
expect(logMessage['level'], 'warning');
|
|
expect(logMessage['message'], 'daemon.logMessage test');
|
|
}, overrides: <Type, Generator>{
|
|
Logger: () => notifyingLogger,
|
|
});
|
|
|
|
testUsingContext('printStatus should log to stdout when logToStdout is enabled', () async {
|
|
final StringBuffer buffer = await capturedConsolePrint(() {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
logToStdout: true,
|
|
);
|
|
globals.printStatus('daemon.logMessage test');
|
|
return Future<void>.value();
|
|
});
|
|
|
|
expect(buffer.toString().trim(), 'daemon.logMessage test');
|
|
}, overrides: <Type, Generator>{
|
|
Logger: () => notifyingLogger,
|
|
});
|
|
|
|
testUsingContext('printBox should log to stdout when logToStdout is enabled', () async {
|
|
final StringBuffer buffer = await capturedConsolePrint(() {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
logToStdout: true,
|
|
);
|
|
globals.printBox('This is the box message', title: 'Sample title');
|
|
return Future<void>.value();
|
|
});
|
|
|
|
expect(buffer.toString().trim(), contains('Sample title: This is the box message'));
|
|
}, overrides: <Type, Generator>{
|
|
Logger: () => notifyingLogger,
|
|
});
|
|
|
|
testUsingContext('printTrace should send daemon.logMessage event when notifyVerbose is enabled', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
notifyingLogger.notifyVerbose = false;
|
|
globals.printTrace('daemon.logMessage test 1');
|
|
notifyingLogger.notifyVerbose = true;
|
|
globals.printTrace('daemon.logMessage test 2');
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage message) {
|
|
return message.data['event'] == 'daemon.logMessage' && (message.data['params']! as Map<String, Object?>)['level'] == 'trace';
|
|
});
|
|
expect(response.data['id'], isNull);
|
|
expect(response.data['event'], 'daemon.logMessage');
|
|
final Map<String, String> logMessage = castStringKeyedMap(response.data['params'])!.cast<String, String>();
|
|
expect(logMessage['level'], 'trace');
|
|
expect(logMessage['message'], 'daemon.logMessage test 2');
|
|
}, overrides: <Type, Generator>{
|
|
Logger: () => notifyingLogger,
|
|
});
|
|
|
|
testUsingContext('daemon.setNotifyVerbose command should update the notify verbose status to true', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
expect(notifyingLogger.notifyVerbose, false);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'daemon.setNotifyVerbose',
|
|
'params': <String, Object?>{
|
|
'verbose': true,
|
|
},
|
|
}));
|
|
await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(notifyingLogger.notifyVerbose, true);
|
|
});
|
|
|
|
testUsingContext('daemon.setNotifyVerbose command should update the notify verbose status to false', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
notifyingLogger.notifyVerbose = false;
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'daemon.setNotifyVerbose',
|
|
'params': <String, Object?>{
|
|
'verbose': false,
|
|
},
|
|
}));
|
|
await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(notifyingLogger.notifyVerbose, false);
|
|
});
|
|
|
|
testUsingContext('daemon.shutdown command should stop daemon', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'daemon.shutdown'}));
|
|
return daemon.onExit.then<void>((int code) async {
|
|
await daemonStreams.inputs.close();
|
|
expect(code, 0);
|
|
});
|
|
});
|
|
|
|
testUsingContext('app.restart without an appId should report an error', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'app.restart'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['error'], contains('appId is required'));
|
|
});
|
|
|
|
testUsingContext('ext.flutter.debugPaint via service extension without an appId should report an error', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'app.callServiceExtension',
|
|
'params': <String, String>{
|
|
'methodName': 'ext.flutter.debugPaint',
|
|
},
|
|
}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['error'], contains('appId is required'));
|
|
});
|
|
|
|
testUsingContext('app.stop without appId should report an error', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'app.stop'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['error'], contains('appId is required'));
|
|
});
|
|
|
|
testUsingContext('device.getDevices should respond with list', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.getDevices'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['result'], isList);
|
|
});
|
|
|
|
testUsingContext('device.getDevices reports available devices', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
discoverer.addDevice(FakeAndroidDevice());
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.getDevices'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
final Object? result = response.data['result'];
|
|
expect(result, isList);
|
|
expect(result, isNotEmpty);
|
|
});
|
|
|
|
testUsingContext('should send device.added event when device is discovered', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
discoverer.addDevice(FakeAndroidDevice());
|
|
final MemoryFileSystem fs = MemoryFileSystem.test();
|
|
discoverer.addDevice(PreviewDevice(
|
|
processManager: FakeProcessManager.empty(),
|
|
logger: BufferLogger.test(),
|
|
fileSystem: fs,
|
|
previewBinary: fs.file(r'preview_device.exe'),
|
|
artifacts: Artifacts.test(fileSystem: fs),
|
|
builderFactory: () => throw UnimplementedError('TODO implement builder factory'),
|
|
));
|
|
|
|
final List<Map<String, Object?>> names = <Map<String, Object?>>[];
|
|
await daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).take(2).forEach((DaemonMessage response) async {
|
|
expect(response.data['event'], 'device.added');
|
|
expect(response.data['params'], isMap);
|
|
|
|
final Map<String, Object?> params = castStringKeyedMap(response.data['params'])!;
|
|
names.add(params);
|
|
});
|
|
await daemonStreams.outputs.close();
|
|
expect(
|
|
names,
|
|
containsAll(const <Map<String, Object?>>[
|
|
<String, Object?>{
|
|
'id': 'device',
|
|
'name': 'android device',
|
|
'platform': 'android-arm',
|
|
'emulator': false,
|
|
'category': 'mobile',
|
|
'platformType': 'android',
|
|
'ephemeral': false,
|
|
'emulatorId': 'device',
|
|
'sdk': 'Android 12',
|
|
'isConnected': true,
|
|
'connectionInterface': 'attached',
|
|
'capabilities': <String, Object?>{
|
|
'hotReload': true,
|
|
'hotRestart': true,
|
|
'screenshot': true,
|
|
'fastStart': true,
|
|
'flutterExit': true,
|
|
'hardwareRendering': true,
|
|
'startPaused': true,
|
|
},
|
|
},
|
|
<String, Object?>{
|
|
'id': 'preview',
|
|
'name': 'Preview',
|
|
'platform': 'windows-x64',
|
|
'emulator': false,
|
|
'category': 'desktop',
|
|
'platformType': 'windowsPreview',
|
|
'ephemeral': false,
|
|
'emulatorId': null,
|
|
'sdk': 'preview',
|
|
'isConnected': true,
|
|
'connectionInterface': 'attached',
|
|
'capabilities': <String, Object?>{
|
|
'hotReload': true,
|
|
'hotRestart': true,
|
|
'screenshot': false,
|
|
'fastStart': false,
|
|
'flutterExit': true,
|
|
'hardwareRendering': true,
|
|
'startPaused': true,
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
AndroidWorkflow: () => FakeAndroidWorkflow(),
|
|
IOSWorkflow: () => FakeIOSWorkflow(),
|
|
FuchsiaWorkflow: () => FakeFuchsiaWorkflow(),
|
|
WindowsWorkflow: () => FakeWindowsWorkflow(),
|
|
});
|
|
|
|
testUsingContext('device.discoverDevices should respond with list', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.discoverDevices'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['result'], isList);
|
|
});
|
|
|
|
testUsingContext('device.discoverDevices reports available devices', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
discoverer.addDevice(FakeAndroidDevice());
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'device.discoverDevices'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
final Object? result = response.data['result'];
|
|
expect(result, isList);
|
|
expect(result, isNotEmpty);
|
|
expect(discoverer.discoverDevicesCalled, true);
|
|
});
|
|
|
|
testUsingContext('device.supportsRuntimeMode returns correct value', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
final FakeAndroidDevice device = FakeAndroidDevice();
|
|
discoverer.addDevice(device);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'device.supportsRuntimeMode',
|
|
'params': <String, Object?>{
|
|
'deviceId': 'device',
|
|
'buildMode': 'profile',
|
|
},
|
|
}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
final Object? result = response.data['result'];
|
|
expect(result, true);
|
|
expect(device.supportsRuntimeModeCalledBuildMode, BuildMode.profile);
|
|
});
|
|
|
|
testUsingContext('device.logReader.start and .stop starts and stops log reader', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
final FakeAndroidDevice device = FakeAndroidDevice();
|
|
discoverer.addDevice(device);
|
|
final FakeDeviceLogReader logReader = FakeDeviceLogReader();
|
|
device.logReader = logReader;
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'device.logReader.start',
|
|
'params': <String, Object?>{
|
|
'deviceId': 'device',
|
|
},
|
|
}));
|
|
final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
|
|
final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(firstResponse.data['id'], 0);
|
|
final String? logReaderId = firstResponse.data['result'] as String?;
|
|
expect(logReaderId, isNotNull);
|
|
|
|
// Try sending logs.
|
|
logReader.logLinesController.add('Sample log line');
|
|
final DaemonMessage logEvent = await broadcastOutput.firstWhere(
|
|
(DaemonMessage message) => message.data['event'] != null && message.data['event'] != 'device.added',
|
|
);
|
|
expect(logEvent.data['params'], 'Sample log line');
|
|
|
|
// Now try to stop the log reader.
|
|
expect(logReader.disposeCalled, false);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 1,
|
|
'method': 'device.logReader.stop',
|
|
'params': <String, Object?>{
|
|
'id': logReaderId,
|
|
},
|
|
}));
|
|
final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(stopResponse.data['id'], 1);
|
|
expect(logReader.disposeCalled, true);
|
|
});
|
|
|
|
group('device.startApp and .stopApp', () {
|
|
late FakeApplicationPackageFactory applicationPackageFactory;
|
|
setUp(() {
|
|
applicationPackageFactory = FakeApplicationPackageFactory();
|
|
});
|
|
|
|
testUsingContext('device.startApp and .stopApp starts and stops an app', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
final FakeAndroidDevice device = FakeAndroidDevice();
|
|
discoverer.addDevice(device);
|
|
final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
|
|
|
|
// First upload the application package.
|
|
final FakeApplicationPackage applicationPackage = FakeApplicationPackage();
|
|
applicationPackageFactory.applicationPackage = applicationPackage;
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'device.uploadApplicationPackage',
|
|
'params': <String, Object?>{
|
|
'targetPlatform': 'android',
|
|
'applicationBinary': 'test_file',
|
|
},
|
|
}));
|
|
final DaemonMessage applicationPackageIdResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(applicationPackageIdResponse.data['id'], 0);
|
|
expect(applicationPackageFactory.applicationBinaryRequested!.basename, 'test_file');
|
|
expect(applicationPackageFactory.platformRequested, TargetPlatform.android);
|
|
final String? applicationPackageId = applicationPackageIdResponse.data['result'] as String?;
|
|
|
|
// Try starting the app.
|
|
final Uri vmServiceUri = Uri.parse('http://127.0.0.1:12345/vmService');
|
|
device.launchResult = LaunchResult.succeeded(vmServiceUri: vmServiceUri);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 1,
|
|
'method': 'device.startApp',
|
|
'params': <String, Object?>{
|
|
'deviceId': 'device',
|
|
'applicationPackageId': applicationPackageId,
|
|
'debuggingOptions': DebuggingOptions.enabled(BuildInfo.debug).toJson(),
|
|
},
|
|
}));
|
|
final DaemonMessage startAppResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(startAppResponse.data['id'], 1);
|
|
expect(device.startAppPackage, applicationPackage);
|
|
final Map<String, Object?> startAppResult = startAppResponse.data['result']! as Map<String, Object?>;
|
|
expect(startAppResult['started'], true);
|
|
expect(startAppResult['vmServiceUri'], vmServiceUri.toString());
|
|
|
|
// Try stopping the app.
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 2,
|
|
'method': 'device.stopApp',
|
|
'params': <String, Object?>{
|
|
'deviceId': 'device',
|
|
'applicationPackageId': applicationPackageId,
|
|
},
|
|
}));
|
|
final DaemonMessage stopAppResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(stopAppResponse.data['id'], 2);
|
|
expect(device.stopAppPackage, applicationPackage);
|
|
final bool? stopAppResult = stopAppResponse.data['result'] as bool?;
|
|
expect(stopAppResult, true);
|
|
}, overrides: <Type, Generator>{
|
|
ApplicationPackageFactory: () => applicationPackageFactory,
|
|
});
|
|
});
|
|
|
|
testUsingContext('device.startDartDevelopmentService and .shutdownDartDevelopmentService starts and stops DDS', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer);
|
|
final FakeAndroidDevice device = FakeAndroidDevice();
|
|
discoverer.addDevice(device);
|
|
|
|
final Completer<void> ddsDoneCompleter = Completer<void>();
|
|
device.dds.done = ddsDoneCompleter.future;
|
|
final Uri fakeDdsUri = Uri.parse('http://fake_dds_uri');
|
|
device.dds.uri = fakeDdsUri;
|
|
|
|
// Try starting DDS.
|
|
expect(device.dds.startCalled, false);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'device.startDartDevelopmentService',
|
|
'params': <String, Object?>{
|
|
'deviceId': 'device',
|
|
'disableServiceAuthCodes': false,
|
|
'vmServiceUri': 'http://fake_uri/auth_code',
|
|
},
|
|
}));
|
|
final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
|
|
final DaemonMessage startResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(startResponse.data['id'], 0);
|
|
expect(startResponse.data['error'], isNull);
|
|
final Map<String, Object?>? result = startResponse.data['result'] as Map<String, Object?>?;
|
|
final String? ddsUri = result!['ddsUri'] as String?;
|
|
expect(ddsUri, fakeDdsUri.toString());
|
|
expect(device.dds.startCalled, true);
|
|
expect(device.dds.startDisableServiceAuthCodes, false);
|
|
expect(device.dds.startVMServiceUri, Uri.parse('http://fake_uri/auth_code'));
|
|
|
|
// dds.done event should be sent to the client.
|
|
ddsDoneCompleter.complete();
|
|
final DaemonMessage startEvent = await broadcastOutput.firstWhere(
|
|
(DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'device.dds.done.device',
|
|
);
|
|
expect(startEvent, isNotNull);
|
|
|
|
// Try stopping DDS.
|
|
expect(device.dds.shutdownCalled, false);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 1,
|
|
'method': 'device.shutdownDartDevelopmentService',
|
|
'params': <String, Object?>{
|
|
'deviceId': 'device',
|
|
},
|
|
}));
|
|
final DaemonMessage stopResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(stopResponse.data['id'], 1);
|
|
expect(stopResponse.data['error'], isNull);
|
|
expect(device.dds.shutdownCalled, true);
|
|
});
|
|
|
|
testUsingContext('device.getDiagnostics returns correct value', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final FakePollingDeviceDiscovery discoverer1 = FakePollingDeviceDiscovery();
|
|
discoverer1.diagnostics = <String>['fake diagnostic 1', 'fake diagnostic 2'];
|
|
final FakePollingDeviceDiscovery discoverer2 = FakePollingDeviceDiscovery();
|
|
discoverer2.diagnostics = <String>['fake diagnostic 3', 'fake diagnostic 4'];
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer1);
|
|
daemon.deviceDomain.addDeviceDiscoverer(discoverer2);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{
|
|
'id': 0,
|
|
'method': 'device.getDiagnostics',
|
|
}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['result'], <String>[
|
|
'fake diagnostic 1',
|
|
'fake diagnostic 2',
|
|
'fake diagnostic 3',
|
|
'fake diagnostic 4',
|
|
]);
|
|
});
|
|
|
|
testUsingContext('emulator.launch without an emulatorId should report an error', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.launch'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['error'], contains('emulatorId is required'));
|
|
});
|
|
|
|
testUsingContext('emulator.launch coldboot parameter must be boolean', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
final Map<String, Object?> params = <String, Object?>{'emulatorId': 'device', 'coldBoot': 1};
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.launch', 'params': params}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['error'], contains('coldBoot is not a bool'));
|
|
});
|
|
|
|
testUsingContext('emulator.getEmulators should respond with list', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'emulator.getEmulators'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere(_notEvent);
|
|
expect(response.data['id'], 0);
|
|
expect(response.data['result'], isList);
|
|
});
|
|
|
|
testUsingContext('daemon can send exposeUrl requests to the client', () async {
|
|
const String originalUrl = 'http://localhost:1234/';
|
|
const String mappedUrl = 'https://publichost:4321/';
|
|
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
// Respond to any requests from the daemon to expose a URL.
|
|
unawaited(daemonStreams.outputs.stream
|
|
.firstWhere((DaemonMessage request) => request.data['method'] == 'app.exposeUrl')
|
|
.then((DaemonMessage request) {
|
|
expect((request.data['params']! as Map<String, Object?>)['url'], equals(originalUrl));
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': request.data['id'], 'result': <String, Object?>{'url': mappedUrl}}));
|
|
})
|
|
);
|
|
|
|
final String exposedUrl = await daemon.daemonDomain.exposeUrl(originalUrl);
|
|
expect(exposedUrl, equals(mappedUrl));
|
|
});
|
|
|
|
testUsingContext('devtools.serve command should return host and port on success', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'devtools.serve'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage response) => response.data['id'] == 0);
|
|
final Map<String, Object?> result = response.data['result']! as Map<String, Object?>;
|
|
expect(result, isNotEmpty);
|
|
expect(result['host'], '127.0.0.1');
|
|
expect(result['port'], 1234);
|
|
}, overrides: <Type, Generator>{
|
|
DevtoolsLauncher: () => FakeDevtoolsLauncher(serverAddress: DevToolsServerAddress('127.0.0.1', 1234)),
|
|
});
|
|
|
|
testUsingContext('devtools.serve command should return null fields if null returned', () async {
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'devtools.serve'}));
|
|
final DaemonMessage response = await daemonStreams.outputs.stream.firstWhere((DaemonMessage response) => response.data['id'] == 0);
|
|
final Map<String, Object?> result = response.data['result']! as Map<String, Object?>;
|
|
expect(result, isNotEmpty);
|
|
expect(result['host'], null);
|
|
expect(result['port'], null);
|
|
}, overrides: <Type, Generator>{
|
|
DevtoolsLauncher: () => FakeDevtoolsLauncher(),
|
|
});
|
|
|
|
testUsingContext('proxy.connect tries to connect to an ipv4 address and proxies the connection correctly', () async {
|
|
final TestIOOverrides ioOverrides = TestIOOverrides();
|
|
await io.IOOverrides.runWithIOOverrides(() async {
|
|
final FakeSocket socket = FakeSocket();
|
|
bool connectCalled = false;
|
|
int? connectPort;
|
|
ioOverrides.connectCallback = (Object? host, int port) async {
|
|
connectCalled = true;
|
|
connectPort = port;
|
|
if (host == io.InternetAddress.loopbackIPv4) {
|
|
return socket;
|
|
}
|
|
throw const io.SocketException('fail');
|
|
};
|
|
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
|
|
|
|
final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
|
|
final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(firstResponse.data['id'], 0);
|
|
expect(firstResponse.data['result'], isNotNull);
|
|
expect(connectCalled, true);
|
|
expect(connectPort, 123);
|
|
|
|
final Object? id = firstResponse.data['result'];
|
|
|
|
// Can send received data as event.
|
|
socket.controller.add(Uint8List.fromList(<int>[10, 11, 12]));
|
|
final DaemonMessage dataEvent = await broadcastOutput.firstWhere(
|
|
(DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'proxy.data.$id',
|
|
);
|
|
expect(dataEvent.binary, isNotNull);
|
|
final List<List<int>> data = await dataEvent.binary!.toList();
|
|
expect(data[0], <int>[10, 11, 12]);
|
|
|
|
// Can proxy data to the socket.
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.write', 'params': <String, Object?>{'id': id}}, Stream<List<int>>.value(<int>[21, 22, 23])));
|
|
await pumpEventQueue();
|
|
expect(socket.addedData[0], <int>[21, 22, 23]);
|
|
|
|
// Closes the connection when disconnect request received.
|
|
expect(socket.closeCalled, false);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.disconnect', 'params': <String, Object?>{'id': id}}));
|
|
await pumpEventQueue();
|
|
expect(socket.closeCalled, true);
|
|
|
|
// Sends disconnected event when socket.done completer finishes.
|
|
socket.doneCompleter.complete(true);
|
|
final DaemonMessage disconnectEvent = await broadcastOutput.firstWhere(
|
|
(DaemonMessage message) => message.data['event'] != null && message.data['event'] == 'proxy.disconnected.$id',
|
|
);
|
|
expect(disconnectEvent.data, isNotNull);
|
|
}, ioOverrides);
|
|
});
|
|
|
|
testUsingContext('proxy.connect connects to ipv6 if ipv4 failed', () async {
|
|
final TestIOOverrides ioOverrides = TestIOOverrides();
|
|
await io.IOOverrides.runWithIOOverrides(() async {
|
|
final FakeSocket socket = FakeSocket();
|
|
bool connectIpv4Called = false;
|
|
int? connectPort;
|
|
ioOverrides.connectCallback = (Object? host, int port) async {
|
|
connectPort = port;
|
|
if (host == io.InternetAddress.loopbackIPv4) {
|
|
connectIpv4Called = true;
|
|
} else if (host == io.InternetAddress.loopbackIPv6) {
|
|
return socket;
|
|
}
|
|
throw const io.SocketException('fail');
|
|
};
|
|
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
|
|
|
|
final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
|
|
final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(firstResponse.data['id'], 0);
|
|
expect(firstResponse.data['result'], isNotNull);
|
|
expect(connectIpv4Called, true);
|
|
expect(connectPort, 123);
|
|
}, ioOverrides);
|
|
});
|
|
|
|
testUsingContext('proxy.connect fails if both ipv6 and ipv4 failed', () async {
|
|
final TestIOOverrides ioOverrides = TestIOOverrides();
|
|
await io.IOOverrides.runWithIOOverrides(() async {
|
|
ioOverrides.connectCallback = (Object? host, int port) => throw const io.SocketException('fail');
|
|
|
|
daemon = Daemon(
|
|
daemonConnection,
|
|
notifyingLogger: notifyingLogger,
|
|
);
|
|
daemonStreams.inputs.add(DaemonMessage(<String, Object?>{'id': 0, 'method': 'proxy.connect', 'params': <String, Object?>{'port': 123}}));
|
|
|
|
final Stream<DaemonMessage> broadcastOutput = daemonStreams.outputs.stream.asBroadcastStream();
|
|
final DaemonMessage firstResponse = await broadcastOutput.firstWhere(_notEvent);
|
|
expect(firstResponse.data['id'], 0);
|
|
expect(firstResponse.data['result'], isNull);
|
|
expect(firstResponse.data['error'], isNotNull);
|
|
}, ioOverrides);
|
|
});
|
|
});
|
|
|
|
group('notifyingLogger', () {
|
|
late BufferLogger bufferLogger;
|
|
setUp(() {
|
|
bufferLogger = BufferLogger.test();
|
|
});
|
|
|
|
tearDown(() {
|
|
bufferLogger.clear();
|
|
});
|
|
|
|
testUsingContext('outputs trace messages in verbose mode', () async {
|
|
final NotifyingLogger logger = NotifyingLogger(verbose: true, parent: bufferLogger);
|
|
logger.printTrace('test');
|
|
expect(bufferLogger.errorText, contains('test'));
|
|
});
|
|
|
|
testUsingContext('ignores trace messages in non-verbose mode', () async {
|
|
final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
|
|
|
|
final Future<LogMessage> messageResult = logger.onMessage.first;
|
|
logger.printTrace('test');
|
|
logger.printStatus('hello');
|
|
|
|
final LogMessage message = await messageResult;
|
|
|
|
expect(message.level, 'status');
|
|
expect(message.message, 'hello');
|
|
expect(bufferLogger.errorText, isEmpty);
|
|
});
|
|
|
|
testUsingContext('sends trace messages in notify verbose mode', () async {
|
|
final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger, notifyVerbose: true);
|
|
|
|
final Future<LogMessage> messageResult = logger.onMessage.first;
|
|
logger.printTrace('hello');
|
|
|
|
final LogMessage message = await messageResult;
|
|
|
|
expect(message.level, 'trace');
|
|
expect(message.message, 'hello');
|
|
expect(bufferLogger.errorText, isEmpty);
|
|
});
|
|
|
|
testUsingContext('buffers messages sent before a subscription', () async {
|
|
final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
|
|
|
|
logger.printStatus('hello');
|
|
|
|
final LogMessage message = await logger.onMessage.first;
|
|
|
|
expect(message.level, 'status');
|
|
expect(message.message, 'hello');
|
|
});
|
|
|
|
testWithoutContext('responds to .supportsColor', () async {
|
|
final NotifyingLogger logger = NotifyingLogger(verbose: false, parent: bufferLogger);
|
|
expect(logger.supportsColor, isFalse);
|
|
});
|
|
});
|
|
|
|
group('daemon queue', () {
|
|
late DebounceOperationQueue<int, String> queue;
|
|
const Duration debounceDuration = Duration(seconds: 1);
|
|
|
|
setUp(() {
|
|
queue = DebounceOperationQueue<int, String>();
|
|
});
|
|
|
|
testWithoutContext(
|
|
'debounces/merges same operation type and returns same result',
|
|
() async {
|
|
await _runFakeAsync((FakeAsync time) async {
|
|
final List<Future<int>> operations = <Future<int>>[
|
|
queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
|
|
queue.queueAndDebounce('OP1', debounceDuration, () async => 2),
|
|
];
|
|
|
|
time.elapse(debounceDuration * 5);
|
|
final List<int> results = await Future.wait(operations);
|
|
|
|
expect(results, orderedEquals(<int>[1, 1]));
|
|
});
|
|
});
|
|
|
|
testWithoutContext('does not merge results outside of the debounce duration',
|
|
() async {
|
|
await _runFakeAsync((FakeAsync time) async {
|
|
final List<Future<int>> operations = <Future<int>>[
|
|
queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
|
|
Future<void>.delayed(debounceDuration * 2).then((_) =>
|
|
queue.queueAndDebounce('OP1', debounceDuration, () async => 2)),
|
|
];
|
|
|
|
time.elapse(debounceDuration * 5);
|
|
final List<int> results = await Future.wait(operations);
|
|
|
|
expect(results, orderedEquals(<int>[1, 2]));
|
|
});
|
|
});
|
|
|
|
testWithoutContext('does not merge results of different operations',
|
|
() async {
|
|
await _runFakeAsync((FakeAsync time) async {
|
|
final List<Future<int>> operations = <Future<int>>[
|
|
queue.queueAndDebounce('OP1', debounceDuration, () async => 1),
|
|
queue.queueAndDebounce('OP2', debounceDuration, () async => 2),
|
|
];
|
|
|
|
time.elapse(debounceDuration * 5);
|
|
final List<int> results = await Future.wait(operations);
|
|
|
|
expect(results, orderedEquals(<int>[1, 2]));
|
|
});
|
|
});
|
|
|
|
testWithoutContext('does not run any operations concurrently', () async {
|
|
// Crete a function that's slow, but throws if another instance of the
|
|
// function is running.
|
|
bool isRunning = false;
|
|
Future<int> f(int ret) async {
|
|
if (isRunning) {
|
|
throw Exception('Functions ran concurrently!');
|
|
}
|
|
isRunning = true;
|
|
await Future<void>.delayed(debounceDuration * 2);
|
|
isRunning = false;
|
|
return ret;
|
|
}
|
|
|
|
await _runFakeAsync((FakeAsync time) async {
|
|
final List<Future<int>> operations = <Future<int>>[
|
|
queue.queueAndDebounce('OP1', debounceDuration, () => f(1)),
|
|
queue.queueAndDebounce('OP2', debounceDuration, () => f(2)),
|
|
];
|
|
|
|
time.elapse(debounceDuration * 5);
|
|
final List<int> results = await Future.wait(operations);
|
|
|
|
expect(results, orderedEquals(<int>[1, 2]));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
bool _notEvent(DaemonMessage message) => message.data['event'] == null;
|
|
|
|
bool _isConnectedEvent(DaemonMessage message) => message.data['event'] == 'daemon.connected';
|
|
|
|
class FakeWindowsWorkflow extends Fake implements WindowsWorkflow {
|
|
FakeWindowsWorkflow({ this.canListDevices = true });
|
|
|
|
@override
|
|
final bool canListDevices;
|
|
}
|
|
|
|
class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
|
|
FakeFuchsiaWorkflow({ this.canListDevices = true });
|
|
|
|
@override
|
|
final bool canListDevices;
|
|
}
|
|
|
|
class FakeAndroidWorkflow extends Fake implements AndroidWorkflow {
|
|
FakeAndroidWorkflow({ this.canListDevices = true });
|
|
|
|
@override
|
|
final bool canListDevices;
|
|
}
|
|
|
|
class FakeIOSWorkflow extends Fake implements IOSWorkflow {
|
|
FakeIOSWorkflow({ this.canListDevices = true });
|
|
|
|
@override
|
|
final bool canListDevices;
|
|
}
|
|
|
|
class FakeAndroidDevice extends Fake implements AndroidDevice {
|
|
@override
|
|
final String id = 'device';
|
|
|
|
@override
|
|
final String name = 'android device';
|
|
|
|
@override
|
|
Future<String> get emulatorId async => 'device';
|
|
|
|
@override
|
|
Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm;
|
|
|
|
@override
|
|
Future<bool> get isLocalEmulator async => false;
|
|
|
|
@override
|
|
final Category category = Category.mobile;
|
|
|
|
@override
|
|
final PlatformType platformType = PlatformType.android;
|
|
|
|
@override
|
|
final bool ephemeral = false;
|
|
|
|
@override
|
|
final bool isConnected = true;
|
|
|
|
@override
|
|
final DeviceConnectionInterface connectionInterface = DeviceConnectionInterface.attached;
|
|
|
|
@override
|
|
Future<String> get sdkNameAndVersion async => 'Android 12';
|
|
|
|
@override
|
|
bool get supportsHotReload => true;
|
|
|
|
@override
|
|
bool get supportsHotRestart => true;
|
|
|
|
@override
|
|
bool get supportsScreenshot => true;
|
|
|
|
@override
|
|
bool get supportsFastStart => true;
|
|
|
|
@override
|
|
bool get supportsFlutterExit => true;
|
|
|
|
@override
|
|
Future<bool> get supportsHardwareRendering async => true;
|
|
|
|
@override
|
|
bool get supportsStartPaused => true;
|
|
|
|
@override
|
|
final FakeDartDevelopmentService dds = FakeDartDevelopmentService();
|
|
|
|
BuildMode? supportsRuntimeModeCalledBuildMode;
|
|
@override
|
|
Future<bool> supportsRuntimeMode(BuildMode buildMode) async {
|
|
supportsRuntimeModeCalledBuildMode = buildMode;
|
|
return true;
|
|
}
|
|
|
|
late DeviceLogReader logReader;
|
|
@override
|
|
FutureOr<DeviceLogReader> getLogReader({
|
|
ApplicationPackage? app,
|
|
bool includePastLogs = false,
|
|
}) => logReader;
|
|
|
|
ApplicationPackage? startAppPackage;
|
|
late LaunchResult launchResult;
|
|
|
|
@override
|
|
Future<LaunchResult> startApp(
|
|
ApplicationPackage? package, {
|
|
String? mainPath,
|
|
String? route,
|
|
DebuggingOptions? debuggingOptions,
|
|
Map<String, Object?> platformArgs = const <String, Object>{},
|
|
bool prebuiltApplication = false,
|
|
bool ipv6 = false,
|
|
String? userIdentifier,
|
|
}) async {
|
|
startAppPackage = package;
|
|
return launchResult;
|
|
}
|
|
|
|
ApplicationPackage? stopAppPackage;
|
|
@override
|
|
Future<bool> stopApp(
|
|
ApplicationPackage? app, {
|
|
String? userIdentifier,
|
|
}) async {
|
|
stopAppPackage = app;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
|
|
bool startCalled = false;
|
|
late Uri startVMServiceUri;
|
|
bool? startDisableServiceAuthCodes;
|
|
|
|
bool shutdownCalled = false;
|
|
|
|
@override
|
|
late Future<void> done;
|
|
|
|
@override
|
|
Uri? uri;
|
|
|
|
@override
|
|
Future<void> startDartDevelopmentService(
|
|
Uri vmServiceUri, {
|
|
int? ddsPort,
|
|
FlutterDevice? device,
|
|
bool? ipv6,
|
|
bool? disableServiceAuthCodes,
|
|
bool enableDevTools = false,
|
|
bool cacheStartupProfile = false,
|
|
String? google3WorkspaceRoot,
|
|
Uri? devToolsServerAddress,
|
|
}) async {
|
|
startCalled = true;
|
|
startVMServiceUri = vmServiceUri;
|
|
startDisableServiceAuthCodes = disableServiceAuthCodes;
|
|
}
|
|
|
|
@override
|
|
Future<void> shutdown() async {
|
|
shutdownCalled = true;
|
|
}
|
|
}
|
|
|
|
class FakeDeviceLogReader implements DeviceLogReader {
|
|
final StreamController<String> logLinesController = StreamController<String>();
|
|
bool disposeCalled = false;
|
|
|
|
@override
|
|
int? appPid;
|
|
|
|
@override
|
|
FlutterVmService? connectedVMService;
|
|
|
|
@override
|
|
void dispose() {
|
|
disposeCalled = true;
|
|
}
|
|
|
|
@override
|
|
Stream<String> get logLines => logLinesController.stream;
|
|
|
|
@override
|
|
String get name => 'device';
|
|
|
|
}
|
|
|
|
class FakeApplicationPackageFactory implements ApplicationPackageFactory {
|
|
TargetPlatform? platformRequested;
|
|
File? applicationBinaryRequested;
|
|
ApplicationPackage? applicationPackage;
|
|
|
|
@override
|
|
Future<ApplicationPackage?> getPackageForPlatform(TargetPlatform platform, {BuildInfo? buildInfo, File? applicationBinary}) async {
|
|
platformRequested = platform;
|
|
applicationBinaryRequested = applicationBinary;
|
|
return applicationPackage;
|
|
}
|
|
}
|
|
|
|
class FakeApplicationPackage extends Fake implements ApplicationPackage {}
|
|
|
|
class TestIOOverrides extends io.IOOverrides {
|
|
late Future<io.Socket> Function(Object? host, int port) connectCallback;
|
|
|
|
@override
|
|
Future<io.Socket> socketConnect(Object? host, int port,
|
|
{Object? sourceAddress, int sourcePort = 0, Duration? timeout}) {
|
|
return connectCallback(host, port);
|
|
}
|
|
}
|
|
|
|
class FakeSocket extends Fake implements io.Socket {
|
|
bool closeCalled = false;
|
|
final StreamController<Uint8List> controller = StreamController<Uint8List>();
|
|
final List<List<int>> addedData = <List<int>>[];
|
|
final Completer<bool> doneCompleter = Completer<bool>();
|
|
|
|
@override
|
|
StreamSubscription<Uint8List> listen(
|
|
void Function(Uint8List event)? onData, {
|
|
Function? onError,
|
|
void Function()? onDone,
|
|
bool? cancelOnError,
|
|
}) {
|
|
return controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
|
|
}
|
|
|
|
@override
|
|
void add(List<int> data) {
|
|
addedData.add(data);
|
|
}
|
|
|
|
@override
|
|
Future<void> close() async {
|
|
closeCalled = true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> get done => doneCompleter.future;
|
|
|
|
@override
|
|
void destroy() {}
|
|
}
|