mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Remove record/replay/bug report functionality from the tool (#45999)
This commit is contained in:
parent
a484db665a
commit
b96d818c19
|
@ -5,18 +5,15 @@
|
|||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:file/record_replay.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'common.dart' show throwToolExit;
|
||||
import 'context.dart';
|
||||
import 'platform.dart';
|
||||
import 'process.dart';
|
||||
|
||||
export 'package:file/file.dart';
|
||||
export 'package:file/local.dart';
|
||||
|
||||
const String _kRecordingType = 'file';
|
||||
const FileSystem _kLocalFs = LocalFileSystem();
|
||||
|
||||
/// Currently active implementation of the file system.
|
||||
|
@ -25,33 +22,6 @@ const FileSystem _kLocalFs = LocalFileSystem();
|
|||
/// with [MemoryFileSystem].
|
||||
FileSystem get fs => context.get<FileSystem>() ?? _kLocalFs;
|
||||
|
||||
/// Gets a [FileSystem] that will record file system activity to the specified
|
||||
/// base recording [location].
|
||||
///
|
||||
/// Activity will be recorded in a subdirectory of [location] named `"file"`.
|
||||
/// It is permissible for [location] to represent an existing non-empty
|
||||
/// directory as long as there is no collision with the `"file"` subdirectory.
|
||||
RecordingFileSystem getRecordingFileSystem(String location) {
|
||||
final Directory dir = getRecordingSink(location, _kRecordingType);
|
||||
final RecordingFileSystem fileSystem = RecordingFileSystem(
|
||||
delegate: _kLocalFs, destination: dir);
|
||||
addShutdownHook(() async {
|
||||
await fileSystem.recording.flush();
|
||||
}, ShutdownStage.SERIALIZE_RECORDING);
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
/// Gets a [FileSystem] that replays invocation activity from a previously
|
||||
/// recorded set of invocations.
|
||||
///
|
||||
/// [location] must represent a directory to which file system activity has
|
||||
/// been recorded (i.e. the result of having been previously passed to
|
||||
/// [getRecordingFileSystem]), or a [ToolExit] will be thrown.
|
||||
ReplayFileSystem getReplayFileSystem(String location) {
|
||||
final Directory dir = getReplaySource(location, _kRecordingType);
|
||||
return ReplayFileSystem(recording: dir);
|
||||
}
|
||||
|
||||
/// Create the ancestor directories of a file path if they do not already exist.
|
||||
void ensureDirectoryExists(String filePath) {
|
||||
final String dirPath = fs.path.dirname(filePath);
|
||||
|
@ -105,49 +75,6 @@ void copyDirectorySync(
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets a directory to act as a recording destination, creating the directory
|
||||
/// as necessary.
|
||||
///
|
||||
/// The directory will exist in the local file system, be named [basename], and
|
||||
/// be a child of the directory identified by [dirname].
|
||||
///
|
||||
/// If the target directory already exists as a directory, the existing
|
||||
/// directory must be empty, or a [ToolExit] will be thrown. If the target
|
||||
/// directory exists as an entity other than a directory, a [ToolExit] will
|
||||
/// also be thrown.
|
||||
Directory getRecordingSink(String dirname, String basename) {
|
||||
final String location = _kLocalFs.path.join(dirname, basename);
|
||||
switch (_kLocalFs.typeSync(location, followLinks: false)) {
|
||||
case FileSystemEntityType.file:
|
||||
case FileSystemEntityType.link:
|
||||
throwToolExit('Invalid record-to location: $dirname ("$basename" exists as non-directory)');
|
||||
break;
|
||||
case FileSystemEntityType.directory:
|
||||
if (_kLocalFs.directory(location).listSync(followLinks: false).isNotEmpty) {
|
||||
throwToolExit('Invalid record-to location: $dirname ("$basename" is not empty)');
|
||||
}
|
||||
break;
|
||||
case FileSystemEntityType.notFound:
|
||||
_kLocalFs.directory(location).createSync(recursive: true);
|
||||
}
|
||||
return _kLocalFs.directory(location);
|
||||
}
|
||||
|
||||
/// Gets a directory that holds a saved recording to be used for the purpose of
|
||||
/// replay.
|
||||
///
|
||||
/// The directory will exist in the local file system, be named [basename], and
|
||||
/// be a child of the directory identified by [dirname].
|
||||
///
|
||||
/// If the target directory does not exist, a [ToolExit] will be thrown.
|
||||
Directory getReplaySource(String dirname, String basename) {
|
||||
final Directory dir = _kLocalFs.directory(_kLocalFs.path.join(dirname, basename));
|
||||
if (!dir.existsSync()) {
|
||||
throwToolExit('Invalid replay-from location: $dirname ("$basename" does not exist)');
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// Canonicalizes [path].
|
||||
///
|
||||
/// This function implements the behavior of `canonicalize` from
|
||||
|
|
|
@ -5,39 +5,9 @@
|
|||
import 'package:platform/platform.dart';
|
||||
|
||||
import 'context.dart';
|
||||
import 'file_system.dart';
|
||||
|
||||
export 'package:platform/platform.dart';
|
||||
|
||||
const Platform _kLocalPlatform = LocalPlatform();
|
||||
const String _kRecordingType = 'platform';
|
||||
|
||||
Platform get platform => context.get<Platform>() ?? _kLocalPlatform;
|
||||
|
||||
/// Serializes the current [platform] to the specified base recording
|
||||
/// [location].
|
||||
///
|
||||
/// Platform metadata will be recorded in a subdirectory of [location] named
|
||||
/// `"platform"`. It is permissible for [location] to represent an existing
|
||||
/// non-empty directory as long as there is no collision with the `"platform"`
|
||||
/// subdirectory.
|
||||
///
|
||||
/// Returns the existing platform.
|
||||
Platform getRecordingPlatform(String location) {
|
||||
final Directory dir = getRecordingSink(location, _kRecordingType);
|
||||
final File file = _getPlatformManifest(dir);
|
||||
file.writeAsStringSync(platform.toJson(), flush: true);
|
||||
return platform;
|
||||
}
|
||||
|
||||
FakePlatform getReplayPlatform(String location) {
|
||||
final Directory dir = getReplaySource(location, _kRecordingType);
|
||||
final File file = _getPlatformManifest(dir);
|
||||
final String json = file.readAsStringSync();
|
||||
return FakePlatform.fromJson(json);
|
||||
}
|
||||
|
||||
File _getPlatformManifest(Directory dir) {
|
||||
final String path = dir.fileSystem.path.join(dir.path, 'MANIFEST.txt');
|
||||
return dir.fileSystem.file(path);
|
||||
}
|
||||
|
|
|
@ -2,54 +2,11 @@
|
|||
// 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:process/process.dart';
|
||||
import 'package:process/record_replay.dart';
|
||||
|
||||
import 'common.dart';
|
||||
import 'context.dart';
|
||||
import 'file_system.dart';
|
||||
import 'process.dart';
|
||||
|
||||
const String _kRecordingType = 'process';
|
||||
const ProcessManager _kLocalProcessManager = LocalProcessManager();
|
||||
|
||||
/// The active process manager.
|
||||
ProcessManager get processManager => context.get<ProcessManager>() ?? _kLocalProcessManager;
|
||||
|
||||
/// Gets a [ProcessManager] that will record process invocation activity to the
|
||||
/// specified base recording [location].
|
||||
///
|
||||
/// Activity will be recorded in a subdirectory of [location] named `"process"`.
|
||||
/// It is permissible for [location] to represent an existing non-empty
|
||||
/// directory as long as there is no collision with the `"process"`
|
||||
/// subdirectory.
|
||||
RecordingProcessManager getRecordingProcessManager(String location) {
|
||||
final Directory dir = getRecordingSink(location, _kRecordingType);
|
||||
const ProcessManager delegate = LocalProcessManager();
|
||||
final RecordingProcessManager manager = RecordingProcessManager(delegate, dir);
|
||||
addShutdownHook(() async {
|
||||
await manager.flush(finishRunningProcesses: true);
|
||||
}, ShutdownStage.SERIALIZE_RECORDING);
|
||||
return manager;
|
||||
}
|
||||
|
||||
/// Gets a [ProcessManager] that replays process activity from a previously
|
||||
/// recorded set of invocations.
|
||||
///
|
||||
/// [location] must represent a directory to which process activity has been
|
||||
/// recorded (i.e. the result of having been previously passed to
|
||||
/// [getRecordingProcessManager]), or a [ToolExit] will be thrown.
|
||||
Future<ReplayProcessManager> getReplayProcessManager(String location) async {
|
||||
final Directory dir = getReplaySource(location, _kRecordingType);
|
||||
|
||||
ProcessManager manager;
|
||||
try {
|
||||
manager = await ReplayProcessManager.create(dir);
|
||||
} on ArgumentError catch (error) {
|
||||
throwToolExit('Invalid replay-from: $error');
|
||||
}
|
||||
|
||||
return manager as ReplayProcessManager;
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@ import 'package:args/args.dart';
|
|||
import 'package:args/command_runner.dart';
|
||||
import 'package:completion/completion.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
|
@ -17,10 +15,7 @@ import '../base/context.dart';
|
|||
import '../base/file_system.dart';
|
||||
import '../base/io.dart' as io;
|
||||
import '../base/logger.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/process_manager.dart';
|
||||
import '../base/terminal.dart';
|
||||
import '../base/user_messages.dart';
|
||||
import '../base/utils.dart';
|
||||
|
@ -32,7 +27,6 @@ import '../globals.dart';
|
|||
import '../reporting/reporting.dart';
|
||||
import '../tester/flutter_tester.dart';
|
||||
import '../version.dart';
|
||||
import '../vmservice.dart';
|
||||
|
||||
const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo)
|
||||
const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo)
|
||||
|
@ -96,10 +90,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
|
|||
argParser.addFlag('suppress-analytics',
|
||||
negatable: false,
|
||||
help: 'Suppress analytics reporting when this command runs.');
|
||||
argParser.addFlag('bug-report',
|
||||
negatable: false,
|
||||
help: 'Captures a bug report file to submit to the Flutter team.\n'
|
||||
'Contains local paths, device identifiers, and log snippets.');
|
||||
|
||||
String packagesHelp;
|
||||
bool showPackagesCommand;
|
||||
|
@ -141,19 +131,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
|
|||
if (verboseHelp) {
|
||||
argParser.addSeparator('Options for testing the "flutter" tool itself:');
|
||||
}
|
||||
|
||||
argParser.addOption('record-to',
|
||||
hide: !verboseHelp,
|
||||
help: 'Enables recording of process invocations (including stdout and stderr of all such invocations), '
|
||||
'and file system access (reads and writes).\n'
|
||||
'Serializes that recording to a directory with the path specified in this flag. If the '
|
||||
'directory does not already exist, it will be created.');
|
||||
argParser.addOption('replay-from',
|
||||
hide: !verboseHelp,
|
||||
help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from '
|
||||
'the specified recording (obtained via --record-to). The path specified in this flag must refer '
|
||||
'to a directory that holds serialized process invocations structured according to the output of '
|
||||
'--record-to.');
|
||||
argParser.addFlag('show-test-device',
|
||||
negatable: false,
|
||||
hide: !verboseHelp,
|
||||
|
@ -291,63 +268,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
|
|||
FlutterTesterDevices.showFlutterTesterDevice = true;
|
||||
}
|
||||
|
||||
String recordTo = topLevelResults['record-to'] as String;
|
||||
String replayFrom = topLevelResults['replay-from'] as String;
|
||||
|
||||
if (topLevelResults['bug-report'] as bool) {
|
||||
// --bug-report implies --record-to=<tmp_path>
|
||||
final Directory tempDir = const LocalFileSystem()
|
||||
.systemTempDirectory
|
||||
.createTempSync('flutter_tools_bug_report.');
|
||||
recordTo = tempDir.path;
|
||||
|
||||
// Record the arguments that were used to invoke this runner.
|
||||
final File manifest = tempDir.childFile('MANIFEST.txt');
|
||||
final StringBuffer buffer = StringBuffer()
|
||||
..writeln('# arguments')
|
||||
..writeln(topLevelResults.arguments)
|
||||
..writeln()
|
||||
..writeln('# rest')
|
||||
..writeln(topLevelResults.rest);
|
||||
manifest.writeAsStringSync(buffer.toString(), flush: true);
|
||||
|
||||
// ZIP the recording up once the recording has been serialized.
|
||||
addShutdownHook(() {
|
||||
final File zipFile = getUniqueFile(fs.currentDirectory, 'bugreport', 'zip');
|
||||
os.zip(tempDir, zipFile);
|
||||
printStatus(userMessages.runnerBugReportFinished(zipFile.basename));
|
||||
}, ShutdownStage.POST_PROCESS_RECORDING);
|
||||
addShutdownHook(() => tempDir.deleteSync(recursive: true), ShutdownStage.CLEANUP);
|
||||
}
|
||||
|
||||
assert(recordTo == null || replayFrom == null);
|
||||
|
||||
if (recordTo != null) {
|
||||
recordTo = recordTo.trim();
|
||||
if (recordTo.isEmpty) {
|
||||
throwToolExit(userMessages.runnerNoRecordTo);
|
||||
}
|
||||
contextOverrides.addAll(<Type, dynamic>{
|
||||
ProcessManager: getRecordingProcessManager(recordTo),
|
||||
FileSystem: getRecordingFileSystem(recordTo),
|
||||
Platform: getRecordingPlatform(recordTo),
|
||||
});
|
||||
VMService.enableRecordingConnection(recordTo);
|
||||
}
|
||||
|
||||
if (replayFrom != null) {
|
||||
replayFrom = replayFrom.trim();
|
||||
if (replayFrom.isEmpty) {
|
||||
throwToolExit(userMessages.runnerNoReplayFrom);
|
||||
}
|
||||
contextOverrides.addAll(<Type, dynamic>{
|
||||
ProcessManager: await getReplayProcessManager(replayFrom),
|
||||
FileSystem: getReplayFileSystem(replayFrom),
|
||||
Platform: getReplayPlatform(replayFrom),
|
||||
});
|
||||
VMService.enableReplayConnection(replayFrom);
|
||||
}
|
||||
|
||||
// We must set Cache.flutterRoot early because other features use it (e.g.
|
||||
// enginePath's initializer uses it).
|
||||
final String flutterRoot = topLevelResults['flutter-root'] as String ?? defaultFlutterRoot;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:json_rpc_2/error_code.dart' as rpc_error_code;
|
||||
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
|
||||
import 'package:meta/meta.dart' show required;
|
||||
|
@ -22,7 +21,6 @@ import 'convert.dart' show base64, utf8;
|
|||
import 'device.dart';
|
||||
import 'globals.dart';
|
||||
import 'version.dart';
|
||||
import 'vmservice_record_replay.dart';
|
||||
|
||||
/// Override `WebSocketConnector` in [context] to use a different constructor
|
||||
/// for [WebSocket]s (used by tests).
|
||||
|
@ -62,8 +60,6 @@ typedef CompileExpression = Future<String> Function(
|
|||
bool isStatic,
|
||||
);
|
||||
|
||||
const String _kRecordingType = 'vmservice';
|
||||
|
||||
Future<StreamChannel<String>> _defaultOpenChannel(Uri uri, {io.CompressionOptions compression = io.CompressionOptions.compressionDefault}) async {
|
||||
Duration delay = const Duration(milliseconds: 100);
|
||||
int attempts = 0;
|
||||
|
@ -284,31 +280,6 @@ class VMService {
|
|||
}
|
||||
}
|
||||
|
||||
/// Enables recording of VMService JSON-rpc activity to the specified base
|
||||
/// recording [location].
|
||||
///
|
||||
/// Activity will be recorded in a subdirectory of [location] named
|
||||
/// `"vmservice"`. It is permissible for [location] to represent an existing
|
||||
/// non-empty directory as long as there is no collision with the
|
||||
/// `"vmservice"` subdirectory.
|
||||
static void enableRecordingConnection(String location) {
|
||||
final Directory dir = getRecordingSink(location, _kRecordingType);
|
||||
_openChannel = (Uri uri, {io.CompressionOptions compression}) async {
|
||||
final StreamChannel<String> delegate = await _defaultOpenChannel(uri);
|
||||
return RecordingVMServiceChannel(delegate, dir);
|
||||
};
|
||||
}
|
||||
|
||||
/// Enables VMService JSON-rpc replay mode.
|
||||
///
|
||||
/// [location] must represent a directory to which VMService JSON-rpc
|
||||
/// activity has been recorded (i.e. the result of having been previously
|
||||
/// passed to [enableRecordingConnection]), or a [ToolExit] will be thrown.
|
||||
static void enableReplayConnection(String location) {
|
||||
final Directory dir = getReplaySource(location, _kRecordingType);
|
||||
_openChannel = (Uri uri, {io.CompressionOptions compression}) async => ReplayVMServiceChannel(dir);
|
||||
}
|
||||
|
||||
static void _unhandledError(dynamic error, dynamic stack) {
|
||||
logger.printTrace('Error in internal implementation of JSON RPC.\n$error\n$stack');
|
||||
assert(false);
|
||||
|
|
|
@ -1,294 +0,0 @@
|
|||
// 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:file/file.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
import 'base/io.dart';
|
||||
import 'base/process.dart';
|
||||
import 'base/utils.dart';
|
||||
import 'convert.dart';
|
||||
import 'globals.dart';
|
||||
|
||||
const String _kManifest = 'MANIFEST.txt';
|
||||
const String _kRequest = 'request';
|
||||
const String _kResponse = 'response';
|
||||
const String _kId = 'id';
|
||||
const String _kType = 'type';
|
||||
const String _kData = 'data';
|
||||
|
||||
/// A [StreamChannel] that expects VM service (JSON-rpc) protocol messages and
|
||||
/// serializes all such messages to the file system for later playback.
|
||||
class RecordingVMServiceChannel extends DelegatingStreamChannel<String> {
|
||||
RecordingVMServiceChannel(StreamChannel<String> delegate, Directory location)
|
||||
: super(delegate) {
|
||||
addShutdownHook(() {
|
||||
// Sort the messages such that they are ordered
|
||||
// `[request1, response1, request2, response2, ...]`. This serves no
|
||||
// purpose other than to make the serialized format more human-readable.
|
||||
_messages.sort();
|
||||
|
||||
final File file = _getManifest(location);
|
||||
final String json = const JsonEncoder.withIndent(' ').convert(_messages);
|
||||
file.writeAsStringSync(json, flush: true);
|
||||
}, ShutdownStage.SERIALIZE_RECORDING);
|
||||
}
|
||||
|
||||
final List<_Message> _messages = <_Message>[];
|
||||
|
||||
_RecordingStream _streamRecorder;
|
||||
_RecordingSink _sinkRecorder;
|
||||
|
||||
@override
|
||||
Stream<String> get stream {
|
||||
_streamRecorder ??= _RecordingStream(super.stream, _messages);
|
||||
return _streamRecorder.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
StreamSink<String> get sink => _sinkRecorder ??= _RecordingSink(super.sink, _messages);
|
||||
}
|
||||
|
||||
/// Base class for request and response JSON-rpc messages.
|
||||
abstract class _Message implements Comparable<_Message> {
|
||||
_Message(this.type, this.data);
|
||||
|
||||
factory _Message.fromRecording(Map<String, dynamic> recordingData) {
|
||||
return recordingData[_kType] == _kRequest
|
||||
? _Request(castStringKeyedMap(recordingData[_kData]))
|
||||
: _Response(castStringKeyedMap(recordingData[_kData]));
|
||||
}
|
||||
|
||||
final String type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
int get id => data[_kId] as int;
|
||||
|
||||
/// Allows [JsonEncoder] to properly encode objects of this type.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
_kType: type,
|
||||
_kData: data,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(_Message other) {
|
||||
if (id == null) {
|
||||
printError('Invalid VMService message data detected: $data');
|
||||
return -1;
|
||||
}
|
||||
final int result = id.compareTo(other.id);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
} else if (type == _kRequest) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A VM service JSON-rpc request (sent to the VM).
|
||||
class _Request extends _Message {
|
||||
_Request(Map<String, dynamic> data) : super(_kRequest, data);
|
||||
_Request.fromString(String data) : this(castStringKeyedMap(json.decode(data)));
|
||||
}
|
||||
|
||||
/// A VM service JSON-rpc response (from the VM).
|
||||
class _Response extends _Message {
|
||||
_Response(Map<String, dynamic> data) : super(_kResponse, data);
|
||||
_Response.fromString(String data) : this(castStringKeyedMap(json.decode(data)));
|
||||
}
|
||||
|
||||
/// A matching request/response pair.
|
||||
///
|
||||
/// A request and response match by virtue of having matching
|
||||
/// [IDs](_Message.id).
|
||||
class _Transaction {
|
||||
_Request request;
|
||||
_Response response;
|
||||
}
|
||||
|
||||
/// A helper class that monitors a [Stream] of VM service JSON-rpc responses
|
||||
/// and saves the responses to a recording.
|
||||
class _RecordingStream {
|
||||
_RecordingStream(Stream<String> stream, this._recording)
|
||||
: _delegate = stream,
|
||||
_controller = stream.isBroadcast
|
||||
? StreamController<String>.broadcast()
|
||||
: StreamController<String>() {
|
||||
_controller.onListen = () {
|
||||
assert(_subscription == null);
|
||||
_subscription = _listenToStream();
|
||||
};
|
||||
_controller.onCancel = () async {
|
||||
assert(_subscription != null);
|
||||
await _subscription.cancel();
|
||||
_subscription = null;
|
||||
};
|
||||
_controller.onPause = () {
|
||||
assert(_subscription != null && !_subscription.isPaused);
|
||||
_subscription.pause();
|
||||
};
|
||||
_controller.onResume = () {
|
||||
assert(_subscription != null && _subscription.isPaused);
|
||||
_subscription.resume();
|
||||
};
|
||||
}
|
||||
|
||||
final Stream<String> _delegate;
|
||||
final StreamController<String> _controller;
|
||||
final List<_Message> _recording;
|
||||
StreamSubscription<String> _subscription;
|
||||
|
||||
StreamSubscription<String> _listenToStream() {
|
||||
return _delegate.listen(
|
||||
(String element) {
|
||||
_recording.add(_Response.fromString(element));
|
||||
_controller.add(element);
|
||||
},
|
||||
onError: _controller.addError, // We currently don't support recording of errors.
|
||||
onDone: _controller.close,
|
||||
);
|
||||
}
|
||||
|
||||
/// The wrapped [Stream] to expose to callers.
|
||||
Stream<String> get stream => _controller.stream;
|
||||
}
|
||||
|
||||
/// A [StreamSink] that monitors VM service JSON-rpc requests and saves the
|
||||
/// requests to a recording.
|
||||
class _RecordingSink implements StreamSink<String> {
|
||||
_RecordingSink(this._delegate, this._recording);
|
||||
|
||||
final StreamSink<String> _delegate;
|
||||
final List<_Message> _recording;
|
||||
|
||||
@override
|
||||
Future<dynamic> close() => _delegate.close();
|
||||
|
||||
@override
|
||||
Future<dynamic> get done => _delegate.done;
|
||||
|
||||
@override
|
||||
void add(String data) {
|
||||
_delegate.add(data);
|
||||
_recording.add(_Request.fromString(data));
|
||||
}
|
||||
|
||||
@override
|
||||
void addError(dynamic errorEvent, [ StackTrace stackTrace ]) {
|
||||
throw UnimplementedError('Add support for this if the need ever arises');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> addStream(Stream<String> stream) {
|
||||
throw UnimplementedError('Add support for this if the need ever arises');
|
||||
}
|
||||
}
|
||||
|
||||
/// A [StreamChannel] that expects VM service (JSON-rpc) requests to be written
|
||||
/// to its [StreamChannel.sink], looks up those requests in a recording, and
|
||||
/// replays the corresponding responses back from the recording.
|
||||
class ReplayVMServiceChannel extends StreamChannelMixin<String> {
|
||||
ReplayVMServiceChannel(Directory location)
|
||||
: _transactions = _loadTransactions(location);
|
||||
|
||||
final Map<int, _Transaction> _transactions;
|
||||
final StreamController<String> _controller = StreamController<String>();
|
||||
_ReplaySink _replaySink;
|
||||
|
||||
static Map<int, _Transaction> _loadTransactions(Directory location) {
|
||||
final File file = _getManifest(location);
|
||||
final String jsonData = file.readAsStringSync();
|
||||
final Iterable<_Message> messages = (json.decode(jsonData) as List<dynamic>)
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map<_Message>(_toMessage);
|
||||
final Map<int, _Transaction> transactions = <int, _Transaction>{};
|
||||
for (_Message message in messages) {
|
||||
final _Transaction transaction =
|
||||
transactions.putIfAbsent(message.id, () => _Transaction());
|
||||
if (message.type == _kRequest) {
|
||||
assert(transaction.request == null);
|
||||
transaction.request = message as _Request;
|
||||
} else {
|
||||
assert(transaction.response == null);
|
||||
transaction.response = message as _Response;
|
||||
}
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
static _Message _toMessage(Map<String, dynamic> jsonData) {
|
||||
return _Message.fromRecording(jsonData);
|
||||
}
|
||||
|
||||
void send(_Request request) {
|
||||
if (!_transactions.containsKey(request.id)) {
|
||||
throw ArgumentError('No matching invocation found');
|
||||
}
|
||||
final _Transaction transaction = _transactions.remove(request.id);
|
||||
// TODO(tvolkert): validate that `transaction.request` matches `request`
|
||||
if (transaction.response == null) {
|
||||
// This signals that when we were recording, the VM shut down before
|
||||
// we received the response. This is typically due to the user quitting
|
||||
// the app runner. We follow suit here and exit.
|
||||
printStatus('Exiting due to dangling request');
|
||||
exit(0);
|
||||
} else {
|
||||
_controller.add(json.encoder.convert(transaction.response.data));
|
||||
if (_transactions.isEmpty) {
|
||||
_controller.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
StreamSink<String> get sink => _replaySink ??= _ReplaySink(this);
|
||||
|
||||
@override
|
||||
Stream<String> get stream => _controller.stream;
|
||||
}
|
||||
|
||||
class _ReplaySink implements StreamSink<String> {
|
||||
_ReplaySink(this.channel);
|
||||
|
||||
final ReplayVMServiceChannel channel;
|
||||
final Completer<void> _completer = Completer<void>();
|
||||
|
||||
@override
|
||||
Future<dynamic> close() {
|
||||
_completer.complete();
|
||||
return _completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> get done => _completer.future;
|
||||
|
||||
@override
|
||||
void add(String data) {
|
||||
if (_completer.isCompleted) {
|
||||
throw StateError('Sink already closed');
|
||||
}
|
||||
channel.send(_Request.fromString(data));
|
||||
}
|
||||
|
||||
@override
|
||||
void addError(dynamic errorEvent, [ StackTrace stackTrace ]) {
|
||||
throw UnimplementedError('Add support for this if the need ever arises');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<dynamic> addStream(Stream<String> stream) {
|
||||
throw UnimplementedError('Add support for this if the need ever arises');
|
||||
}
|
||||
}
|
||||
|
||||
File _getManifest(Directory location) {
|
||||
final String path = location.fileSystem.path.join(location.path, _kManifest);
|
||||
return location.fileSystem.file(path);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
// 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 'package:file_testing/file_testing.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'package:flutter_tools/executable.dart' as tools;
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/base/os.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import '../src/context.dart';
|
||||
|
||||
void main() {
|
||||
Cache.disableLocking();
|
||||
|
||||
int exitCode;
|
||||
setExitFunctionForTests((int code) {
|
||||
exitCode = code;
|
||||
});
|
||||
|
||||
group('--bug-report', () {
|
||||
testUsingContext('generates valid zip file', () async {
|
||||
await tools.main(<String>['devices', '--bug-report']);
|
||||
expect(exitCode, 0);
|
||||
verify(os.zip(any, argThat(hasPath(matches(r'bugreport_01\.zip')))));
|
||||
}, overrides: <Type, Generator>{
|
||||
OperatingSystemUtils: () => MockOperatingSystemUtils(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { }
|
Loading…
Reference in a new issue