mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
CrashReportSender dependency injection (#54924)
This commit is contained in:
parent
9202e54704
commit
8109dcc2b4
|
@ -7,7 +7,7 @@ import 'dart:async';
|
|||
import 'package:args/command_runner.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:intl/intl_standalone.dart' as intl_standalone;
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'src/base/common.dart';
|
||||
import 'src/base/context.dart';
|
||||
|
@ -27,13 +27,13 @@ import 'src/runner/flutter_command_runner.dart';
|
|||
Future<int> run(
|
||||
List<String> args,
|
||||
List<FlutterCommand> commands, {
|
||||
bool muteCommandLogging = false,
|
||||
bool verbose = false,
|
||||
bool verboseHelp = false,
|
||||
bool reportCrashes,
|
||||
String flutterVersion,
|
||||
Map<Type, Generator> overrides,
|
||||
}) async {
|
||||
bool muteCommandLogging = false,
|
||||
bool verbose = false,
|
||||
bool verboseHelp = false,
|
||||
bool reportCrashes,
|
||||
String flutterVersion,
|
||||
Map<Type, Generator> overrides,
|
||||
}) async {
|
||||
if (muteCommandLogging) {
|
||||
// Remove the verbose option; for help and doctor, users don't need to see
|
||||
// verbose logs.
|
||||
|
@ -121,7 +121,14 @@ Future<int> _handleToolError(
|
|||
|
||||
// Report to both [Usage] and [CrashReportSender].
|
||||
globals.flutterUsage.sendException(error);
|
||||
await CrashReportSender.instance.sendReport(
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: http.Client(),
|
||||
usage: globals.flutterUsage,
|
||||
platform: globals.platform,
|
||||
logger: globals.logger,
|
||||
operatingSystemUtils: globals.os,
|
||||
);
|
||||
await crashReportSender.sendReport(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
getFlutterVersion: getFlutterVersion,
|
||||
|
@ -184,18 +191,10 @@ String _crashCommand(List<String> args) => 'flutter ${args.join(' ')}';
|
|||
|
||||
String _crashException(dynamic error) => '${error.runtimeType}: $error';
|
||||
|
||||
/// File system used by the crash reporting logic.
|
||||
///
|
||||
/// We do not want to use the file system stored in the context because it may
|
||||
/// be recording. Additionally, in the case of a crash we do not trust the
|
||||
/// integrity of the [AppContext].
|
||||
@visibleForTesting
|
||||
FileSystem crashFileSystem = const LocalFileSystem();
|
||||
|
||||
/// Saves the crash report to a local file.
|
||||
Future<File> _createLocalCrashReport(List<String> args, dynamic error, StackTrace stackTrace, String doctorText) async {
|
||||
File crashFile = globals.fsUtils.getUniqueFile(
|
||||
crashFileSystem.currentDirectory,
|
||||
globals.fs.currentDirectory,
|
||||
'flutter',
|
||||
'log',
|
||||
);
|
||||
|
@ -219,7 +218,7 @@ Future<File> _createLocalCrashReport(List<String> args, dynamic error, StackTrac
|
|||
} on FileSystemException catch (_) {
|
||||
// Fallback to the system temporary directory.
|
||||
crashFile = globals.fsUtils.getUniqueFile(
|
||||
crashFileSystem.systemTempDirectory,
|
||||
globals.fs.systemTempDirectory,
|
||||
'flutter',
|
||||
'log',
|
||||
);
|
||||
|
|
|
@ -35,28 +35,29 @@ const String _kStackTraceFilename = 'stacktrace_file';
|
|||
/// environment is behind a firewall and unable to send crash reports to
|
||||
/// Google, or when you wish to use your own server for collecting crash
|
||||
/// reports from Flutter Tools.
|
||||
/// * In tests call [initializeWith] and provide a mock implementation of
|
||||
/// [http.Client].
|
||||
class CrashReportSender {
|
||||
CrashReportSender._(this._client);
|
||||
CrashReportSender({
|
||||
@required http.Client client,
|
||||
@required Usage usage,
|
||||
@required Platform platform,
|
||||
@required Logger logger,
|
||||
@required OperatingSystemUtils operatingSystemUtils,
|
||||
}) : _client = client,
|
||||
_usage = usage,
|
||||
_platform = platform,
|
||||
_logger = logger,
|
||||
_operatingSystemUtils = operatingSystemUtils;
|
||||
|
||||
static CrashReportSender _instance;
|
||||
|
||||
static CrashReportSender get instance => _instance ?? CrashReportSender._(http.Client());
|
||||
final http.Client _client;
|
||||
final Usage _usage;
|
||||
final Platform _platform;
|
||||
final Logger _logger;
|
||||
final OperatingSystemUtils _operatingSystemUtils;
|
||||
|
||||
bool _crashReportSent = false;
|
||||
|
||||
/// Overrides the default [http.Client] with [client] for testing purposes.
|
||||
@visibleForTesting
|
||||
static void initializeWith(http.Client client) {
|
||||
_instance = CrashReportSender._(client);
|
||||
}
|
||||
|
||||
final http.Client _client;
|
||||
final Usage _usage = globals.flutterUsage;
|
||||
|
||||
Uri get _baseUrl {
|
||||
final String overrideUrl = globals.platform.environment['FLUTTER_CRASH_SERVER_BASE_URL'];
|
||||
final String overrideUrl = _platform.environment['FLUTTER_CRASH_SERVER_BASE_URL'];
|
||||
|
||||
if (overrideUrl != null) {
|
||||
return Uri.parse(overrideUrl);
|
||||
|
@ -90,7 +91,7 @@ class CrashReportSender {
|
|||
return;
|
||||
}
|
||||
|
||||
globals.printTrace('Sending crash report to Google.');
|
||||
_logger.printTrace('Sending crash report to Google.');
|
||||
|
||||
final Uri uri = _baseUrl.replace(
|
||||
queryParameters: <String, String>{
|
||||
|
@ -103,8 +104,8 @@ class CrashReportSender {
|
|||
req.fields['uuid'] = _usage.clientId;
|
||||
req.fields['product'] = _kProductId;
|
||||
req.fields['version'] = flutterVersion;
|
||||
req.fields['osName'] = globals.platform.operatingSystem;
|
||||
req.fields['osVersion'] = globals.os.name; // this actually includes version
|
||||
req.fields['osName'] = _platform.operatingSystem;
|
||||
req.fields['osVersion'] = _operatingSystemUtils.name; // this actually includes version
|
||||
req.fields['type'] = _kDartTypeId;
|
||||
req.fields['error_runtime_type'] = '${error.runtimeType}';
|
||||
req.fields['error_message'] = '$error';
|
||||
|
@ -120,20 +121,20 @@ class CrashReportSender {
|
|||
|
||||
if (resp.statusCode == 200) {
|
||||
final String reportId = await http.ByteStream(resp.stream)
|
||||
.bytesToString();
|
||||
globals.printTrace('Crash report sent (report ID: $reportId)');
|
||||
.bytesToString();
|
||||
_logger.printTrace('Crash report sent (report ID: $reportId)');
|
||||
_crashReportSent = true;
|
||||
} else {
|
||||
globals.printError('Failed to send crash report. Server responded with HTTP status code ${resp.statusCode}');
|
||||
_logger.printError('Failed to send crash report. Server responded with HTTP status code ${resp.statusCode}');
|
||||
}
|
||||
// Catch all exceptions to print the message that makes clear that the
|
||||
// crash logger crashed.
|
||||
} catch (sendError, sendStackTrace) { // ignore: avoid_catches_without_on_clauses
|
||||
if (sendError is SocketException || sendError is HttpException) {
|
||||
globals.printError('Failed to send crash report due to a network error: $sendError');
|
||||
_logger.printError('Failed to send crash report due to a network error: $sendError');
|
||||
} else {
|
||||
// If the sender itself crashes, just print. We did our best.
|
||||
globals.printError('Crash report sender itself crashed: $sendError\n$sendStackTrace');
|
||||
_logger.printError('Crash report sender itself crashed: $sendError\n$sendStackTrace');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,11 +10,13 @@ import 'package:file/file.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:usage/usage_io.dart';
|
||||
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/time.dart';
|
||||
import '../build_system/exceptions.dart';
|
||||
|
|
|
@ -5,160 +5,205 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/runner.dart' as tools;
|
||||
import 'package:flutter_tools/src/base/common.dart';
|
||||
import 'package:flutter_tools/src/base/context.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/os.dart';
|
||||
import 'package:flutter_tools/src/doctor.dart';
|
||||
import 'package:flutter_tools/src/reporting/reporting.dart';
|
||||
import 'package:flutter_tools/src/runner/flutter_command.dart';
|
||||
import 'package:flutter_tools/src/globals.dart' as globals;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:quiver/testing/async.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import '../src/context.dart';
|
||||
|
||||
void main() {
|
||||
group('crash reporting', () {
|
||||
setUpAll(() {
|
||||
Cache.disableLocking();
|
||||
});
|
||||
BufferLogger logger;
|
||||
MockUsage mockUsage;
|
||||
Platform platform;
|
||||
OperatingSystemUtils operatingSystemUtils;
|
||||
|
||||
setUp(() async {
|
||||
logger = BufferLogger.test();
|
||||
|
||||
mockUsage = MockUsage();
|
||||
when(mockUsage.clientId).thenReturn('00000000-0000-4000-0000-000000000000');
|
||||
|
||||
platform = FakePlatform(environment: <String, String>{}, operatingSystem: 'linux');
|
||||
operatingSystemUtils = OperatingSystemUtils(
|
||||
fileSystem: MemoryFileSystem.test(),
|
||||
logger: logger,
|
||||
platform: platform,
|
||||
processManager: FakeProcessManager.any(),
|
||||
);
|
||||
|
||||
MockCrashReportSender.sendCalls = 0;
|
||||
});
|
||||
|
||||
Future<void> verifyCrashReportSent(RequestInfo crashInfo, {
|
||||
int crashes = 1,
|
||||
}) async {
|
||||
// Verify that we sent the crash report.
|
||||
expect(crashInfo.method, 'POST');
|
||||
expect(crashInfo.uri, Uri(
|
||||
scheme: 'https',
|
||||
host: 'clients2.google.com',
|
||||
port: 443,
|
||||
path: '/cr/report',
|
||||
queryParameters: <String, String>{
|
||||
'product': 'Flutter_Tools',
|
||||
'version': 'test-version',
|
||||
},
|
||||
));
|
||||
expect(crashInfo.fields['uuid'], '00000000-0000-4000-0000-000000000000');
|
||||
expect(crashInfo.fields['product'], 'Flutter_Tools');
|
||||
expect(crashInfo.fields['version'], 'test-version');
|
||||
expect(crashInfo.fields['osName'], 'linux');
|
||||
expect(crashInfo.fields['osVersion'], 'Linux');
|
||||
expect(crashInfo.fields['type'], 'DartError');
|
||||
expect(crashInfo.fields['error_runtime_type'], 'StateError');
|
||||
expect(crashInfo.fields['error_message'], 'Bad state: Test bad state error');
|
||||
expect(crashInfo.fields['comments'], 'crash');
|
||||
|
||||
expect(logger.traceText, contains('Sending crash report to Google.'));
|
||||
expect(logger.traceText, contains('Crash report sent (report ID: test-report-id)'));
|
||||
}
|
||||
|
||||
testWithoutContext('suppress analytics', () async {
|
||||
when(mockUsage.suppressAnalytics).thenReturn(true);
|
||||
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: CrashingCrashReportSender(const SocketException('no internets')),
|
||||
usage: mockUsage,
|
||||
platform: platform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
expect(logger.traceText, isEmpty);
|
||||
});
|
||||
|
||||
group('allow analytics', () {
|
||||
setUp(() async {
|
||||
tools.crashFileSystem = MemoryFileSystem();
|
||||
setExitFunctionForTests((_) { });
|
||||
MockCrashReportSender.sendCalls = 0;
|
||||
when(mockUsage.suppressAnalytics).thenReturn(false);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
tools.crashFileSystem = const LocalFileSystem();
|
||||
restoreExitFunction();
|
||||
});
|
||||
|
||||
testUsingContext('should send crash reports', () async {
|
||||
testWithoutContext('should send crash reports', () async {
|
||||
final RequestInfo requestInfo = RequestInfo();
|
||||
|
||||
CrashReportSender.initializeWith(MockCrashReportSender(requestInfo));
|
||||
final int exitCode = await tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_CrashCommand()],
|
||||
reportCrashes: true,
|
||||
flutterVersion: 'test-version',
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: MockCrashReportSender(requestInfo),
|
||||
usage: mockUsage,
|
||||
platform: platform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
expect(exitCode, 1);
|
||||
|
||||
await verifyCrashReportSent(requestInfo);
|
||||
}, overrides: <Type, Generator>{
|
||||
Stdio: () => _NoStderr(),
|
||||
});
|
||||
|
||||
testUsingContext('should print an explanatory message when there is a SocketException', () async {
|
||||
final Completer<int> exitCodeCompleter = Completer<int>();
|
||||
setExitFunctionForTests((int exitCode) {
|
||||
exitCodeCompleter.complete(exitCode);
|
||||
});
|
||||
testWithoutContext('should print an explanatory message when there is a SocketException', () async {
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: CrashingCrashReportSender(const SocketException('no internets')),
|
||||
usage: mockUsage,
|
||||
platform: platform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
CrashReportSender.initializeWith(
|
||||
CrashingCrashReportSender(const SocketException('no internets')));
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
unawaited(tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_CrashAsyncCommand()],
|
||||
reportCrashes: true,
|
||||
flutterVersion: 'test-version',
|
||||
));
|
||||
expect(await exitCodeCompleter.future, 1);
|
||||
expect(testLogger.errorText, contains('Failed to send crash report due to a network error'));
|
||||
}, overrides: <Type, Generator>{
|
||||
Stdio: () => _NoStderr(),
|
||||
expect(logger.errorText, contains('Failed to send crash report due to a network error'));
|
||||
});
|
||||
|
||||
testUsingContext('should print an explanatory message when there is an HttpException', () async {
|
||||
final Completer<int> exitCodeCompleter = Completer<int>();
|
||||
setExitFunctionForTests((int exitCode) {
|
||||
exitCodeCompleter.complete(exitCode);
|
||||
});
|
||||
testWithoutContext('should print an explanatory message when there is an HttpException', () async {
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: CrashingCrashReportSender(const HttpException('no internets')),
|
||||
usage: mockUsage,
|
||||
platform: platform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
CrashReportSender.initializeWith(
|
||||
CrashingCrashReportSender(const HttpException('no internets')));
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
unawaited(tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_CrashAsyncCommand()],
|
||||
reportCrashes: true,
|
||||
flutterVersion: 'test-version',
|
||||
));
|
||||
expect(await exitCodeCompleter.future, 1);
|
||||
expect(testLogger.errorText, contains('Failed to send crash report due to a network error'));
|
||||
}, overrides: <Type, Generator>{
|
||||
Stdio: () => _NoStderr(),
|
||||
expect(logger.errorText, contains('Failed to send crash report due to a network error'));
|
||||
});
|
||||
|
||||
testUsingContext('should send crash reports when async throws', () async {
|
||||
final Completer<int> exitCodeCompleter = Completer<int>();
|
||||
setExitFunctionForTests((int exitCode) {
|
||||
exitCodeCompleter.complete(exitCode);
|
||||
});
|
||||
|
||||
testWithoutContext('should send only one crash report when sent many times', () async {
|
||||
final RequestInfo requestInfo = RequestInfo();
|
||||
|
||||
CrashReportSender.initializeWith(MockCrashReportSender(requestInfo));
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: MockCrashReportSender(requestInfo),
|
||||
usage: mockUsage,
|
||||
platform: platform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
unawaited(tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_CrashAsyncCommand()],
|
||||
reportCrashes: true,
|
||||
flutterVersion: 'test-version',
|
||||
));
|
||||
expect(await exitCodeCompleter.future, 1);
|
||||
await verifyCrashReportSent(requestInfo);
|
||||
}, overrides: <Type, Generator>{
|
||||
Stdio: () => _NoStderr(),
|
||||
});
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
testUsingContext('should send only one crash report when async throws many', () async {
|
||||
final Completer<int> exitCodeCompleter = Completer<int>();
|
||||
setExitFunctionForTests((int exitCode) {
|
||||
if (!exitCodeCompleter.isCompleted) {
|
||||
exitCodeCompleter.complete(exitCode);
|
||||
}
|
||||
});
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
final RequestInfo requestInfo = RequestInfo();
|
||||
final MockCrashReportSender sender = MockCrashReportSender(requestInfo);
|
||||
CrashReportSender.initializeWith(sender);
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
FakeAsync().run((FakeAsync time) {
|
||||
time.elapse(const Duration(seconds: 1));
|
||||
unawaited(tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_MultiCrashAsyncCommand(crashes: 4)],
|
||||
reportCrashes: true,
|
||||
flutterVersion: 'test-version',
|
||||
));
|
||||
time.elapse(const Duration(seconds: 1));
|
||||
time.flushMicrotasks();
|
||||
});
|
||||
expect(await exitCodeCompleter.future, 1);
|
||||
expect(MockCrashReportSender.sendCalls, 1);
|
||||
await verifyCrashReportSent(requestInfo, crashes: 4);
|
||||
}, overrides: <Type, Generator>{
|
||||
DoctorValidatorsProvider: () => FakeDoctorValidatorsProvider(),
|
||||
Stdio: () => _NoStderr(),
|
||||
});
|
||||
|
||||
testUsingContext('should not send a crash report if on a user-branch', () async {
|
||||
testWithoutContext('should not send a crash report if on a user-branch', () async {
|
||||
String method;
|
||||
Uri uri;
|
||||
|
||||
CrashReportSender.initializeWith(MockClient((Request request) async {
|
||||
final MockClient mockClient = MockClient((Request request) async {
|
||||
method = request.method;
|
||||
uri = request.url;
|
||||
|
||||
|
@ -166,41 +211,60 @@ void main() {
|
|||
'test-report-id',
|
||||
200,
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
final int exitCode = await tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_CrashCommand()],
|
||||
reportCrashes: true,
|
||||
flutterVersion: '[user-branch]/v1.2.3',
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: mockClient,
|
||||
usage: mockUsage,
|
||||
platform: platform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
expect(exitCode, 1);
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => '[user-branch]/v1.2.3',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
// Verify that the report wasn't sent
|
||||
expect(method, null);
|
||||
expect(uri, null);
|
||||
|
||||
expect(testLogger.traceText, isNot(contains('Crash report sent')));
|
||||
}, overrides: <Type, Generator>{
|
||||
Stdio: () => _NoStderr(),
|
||||
expect(logger.traceText, isNot(contains('Crash report sent')));
|
||||
});
|
||||
|
||||
testUsingContext('can override base URL', () async {
|
||||
testWithoutContext('can override base URL', () async {
|
||||
Uri uri;
|
||||
CrashReportSender.initializeWith(MockClient((Request request) async {
|
||||
final MockClient mockClient = MockClient((Request request) async {
|
||||
uri = request.url;
|
||||
return Response('test-report-id', 200);
|
||||
}));
|
||||
});
|
||||
|
||||
final int exitCode = await tools.run(
|
||||
<String>['crash'],
|
||||
<FlutterCommand>[_CrashCommand()],
|
||||
reportCrashes: true,
|
||||
flutterVersion: 'test-version',
|
||||
final Platform environmentPlatform = FakePlatform(
|
||||
operatingSystem: 'linux',
|
||||
environment: <String, String>{
|
||||
'HOME': '/',
|
||||
'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
|
||||
},
|
||||
script: Uri(scheme: 'data'),
|
||||
);
|
||||
|
||||
expect(exitCode, 1);
|
||||
final CrashReportSender crashReportSender = CrashReportSender(
|
||||
client: mockClient,
|
||||
usage: mockUsage,
|
||||
platform: environmentPlatform,
|
||||
logger: logger,
|
||||
operatingSystemUtils: operatingSystemUtils,
|
||||
);
|
||||
|
||||
await crashReportSender.sendReport(
|
||||
error: StateError('Test bad state error'),
|
||||
stackTrace: null,
|
||||
getFlutterVersion: () => 'test-version',
|
||||
command: 'crash',
|
||||
);
|
||||
|
||||
// Verify that we sent the crash report.
|
||||
expect(uri, isNotNull);
|
||||
|
@ -214,16 +278,6 @@ void main() {
|
|||
'version': 'test-version',
|
||||
},
|
||||
));
|
||||
}, overrides: <Type, Generator>{
|
||||
Platform: () => FakePlatform(
|
||||
operatingSystem: 'linux',
|
||||
environment: <String, String>{
|
||||
'HOME': '/',
|
||||
'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
|
||||
},
|
||||
script: Uri(scheme: 'data'),
|
||||
),
|
||||
Stdio: () => _NoStderr(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -234,42 +288,6 @@ class RequestInfo {
|
|||
Map<String, String> fields;
|
||||
}
|
||||
|
||||
Future<void> verifyCrashReportSent(RequestInfo crashInfo, {
|
||||
int crashes = 1,
|
||||
}) async {
|
||||
// Verify that we sent the crash report.
|
||||
expect(crashInfo.method, 'POST');
|
||||
expect(crashInfo.uri, Uri(
|
||||
scheme: 'https',
|
||||
host: 'clients2.google.com',
|
||||
port: 443,
|
||||
path: '/cr/report',
|
||||
queryParameters: <String, String>{
|
||||
'product': 'Flutter_Tools',
|
||||
'version': 'test-version',
|
||||
},
|
||||
));
|
||||
expect(crashInfo.fields['uuid'], '00000000-0000-4000-0000-000000000000');
|
||||
expect(crashInfo.fields['product'], 'Flutter_Tools');
|
||||
expect(crashInfo.fields['version'], 'test-version');
|
||||
expect(crashInfo.fields['osName'], globals.platform.operatingSystem);
|
||||
expect(crashInfo.fields['osVersion'], 'fake OS name and version');
|
||||
expect(crashInfo.fields['type'], 'DartError');
|
||||
expect(crashInfo.fields['error_runtime_type'], 'StateError');
|
||||
expect(crashInfo.fields['error_message'], 'Bad state: Test bad state error');
|
||||
expect(crashInfo.fields['comments'], 'crash');
|
||||
|
||||
expect(testLogger.traceText, contains('Sending crash report to Google.'));
|
||||
expect(testLogger.traceText, contains('Crash report sent (report ID: test-report-id)'));
|
||||
|
||||
// Verify that we've written the crash report to disk.
|
||||
final List<String> writtenFiles =
|
||||
(await tools.crashFileSystem.directory('/').list(recursive: true).toList())
|
||||
.map((FileSystemEntity e) => e.path).toList();
|
||||
expect(writtenFiles, hasLength(crashes));
|
||||
expect(writtenFiles, contains('flutter_01.log'));
|
||||
}
|
||||
|
||||
class MockCrashReportSender extends MockClient {
|
||||
MockCrashReportSender(RequestInfo crashInfo) : super((Request request) async {
|
||||
MockCrashReportSender.sendCalls++;
|
||||
|
@ -283,14 +301,14 @@ class MockCrashReportSender extends MockClient {
|
|||
utf8.decode(request.bodyBytes)
|
||||
.split('--$boundary')
|
||||
.map<List<String>>((String part) {
|
||||
final Match nameMatch = RegExp(r'name="(.*)"').firstMatch(part);
|
||||
if (nameMatch == null) {
|
||||
return null;
|
||||
}
|
||||
final String name = nameMatch[1];
|
||||
final String value = part.split('\n').skip(2).join('\n').trim();
|
||||
return <String>[name, value];
|
||||
})
|
||||
final Match nameMatch = RegExp(r'name="(.*)"').firstMatch(part);
|
||||
if (nameMatch == null) {
|
||||
return null;
|
||||
}
|
||||
final String name = nameMatch[1];
|
||||
final String value = part.split('\n').skip(2).join('\n').trim();
|
||||
return <String>[name, value];
|
||||
})
|
||||
.where((List<String> pair) => pair != null),
|
||||
key: (dynamic key) {
|
||||
final List<String> pair = key as List<String>;
|
||||
|
@ -317,78 +335,6 @@ class CrashingCrashReportSender extends MockClient {
|
|||
});
|
||||
}
|
||||
|
||||
/// Throws a random error to simulate a CLI crash.
|
||||
class _CrashCommand extends FlutterCommand {
|
||||
|
||||
@override
|
||||
String get description => 'Simulates a crash';
|
||||
|
||||
@override
|
||||
String get name => 'crash';
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
void fn1() {
|
||||
throw StateError('Test bad state error');
|
||||
}
|
||||
|
||||
void fn2() {
|
||||
fn1();
|
||||
}
|
||||
|
||||
void fn3() {
|
||||
fn2();
|
||||
}
|
||||
|
||||
fn3();
|
||||
|
||||
return FlutterCommandResult.success();
|
||||
}
|
||||
}
|
||||
|
||||
/// Throws StateError from async callback.
|
||||
class _CrashAsyncCommand extends FlutterCommand {
|
||||
|
||||
@override
|
||||
String get description => 'Simulates a crash';
|
||||
|
||||
@override
|
||||
String get name => 'crash';
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
Timer.run(() {
|
||||
throw StateError('Test bad state error');
|
||||
});
|
||||
return Completer<FlutterCommandResult>().future; // expect StateError
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates multiple asynchronous unhandled exceptions.
|
||||
class _MultiCrashAsyncCommand extends FlutterCommand {
|
||||
_MultiCrashAsyncCommand({
|
||||
int crashes = 1,
|
||||
}) : _crashes = crashes;
|
||||
|
||||
final int _crashes;
|
||||
|
||||
@override
|
||||
String get description => 'Simulates a crash';
|
||||
|
||||
@override
|
||||
String get name => 'crash';
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
for (int i = 0; i < _crashes; i++) {
|
||||
Timer.run(() {
|
||||
throw StateError('Test bad state error');
|
||||
});
|
||||
}
|
||||
return Completer<FlutterCommandResult>().future; // expect StateError
|
||||
}
|
||||
}
|
||||
|
||||
/// A DoctorValidatorsProvider that overrides the default validators without
|
||||
/// overriding the doctor.
|
||||
class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
|
||||
|
@ -399,49 +345,4 @@ class FakeDoctorValidatorsProvider implements DoctorValidatorsProvider {
|
|||
List<Workflow> get workflows => <Workflow>[];
|
||||
}
|
||||
|
||||
class _NoStderr extends Stdio {
|
||||
_NoStderr();
|
||||
|
||||
@override
|
||||
IOSink get stderr => const _NoopIOSink();
|
||||
}
|
||||
|
||||
class _NoopIOSink implements IOSink {
|
||||
const _NoopIOSink();
|
||||
|
||||
@override
|
||||
Encoding get encoding => utf8;
|
||||
|
||||
@override
|
||||
set encoding(_) => throw UnsupportedError('');
|
||||
|
||||
@override
|
||||
void add(_) { }
|
||||
|
||||
@override
|
||||
void write(_) { }
|
||||
|
||||
@override
|
||||
void writeAll(_, [ __ = '' ]) { }
|
||||
|
||||
@override
|
||||
void writeln([ _ = '' ]) { }
|
||||
|
||||
@override
|
||||
void writeCharCode(_) { }
|
||||
|
||||
@override
|
||||
void addError(_, [ __ ]) { }
|
||||
|
||||
@override
|
||||
Future<dynamic> addStream(_) async { }
|
||||
|
||||
@override
|
||||
Future<dynamic> flush() async { }
|
||||
|
||||
@override
|
||||
Future<dynamic> close() async { }
|
||||
|
||||
@override
|
||||
Future<dynamic> get done async { }
|
||||
}
|
||||
class MockUsage extends Mock implements Usage {}
|
||||
|
|
|
@ -24,7 +24,6 @@ void main() {
|
|||
MockGitHubTemplateCreator mockGitHubTemplateCreator;
|
||||
setUp(() {
|
||||
mockGitHubTemplateCreator = MockGitHubTemplateCreator();
|
||||
runner.crashFileSystem = MemoryFileSystem();
|
||||
// Instead of exiting with dart:io exit(), this causes an exception to
|
||||
// be thrown, which we catch with the onError callback in the zone below.
|
||||
io.setExitFunctionForTests((int _) { throw 'test exit';});
|
||||
|
@ -32,7 +31,6 @@ void main() {
|
|||
});
|
||||
|
||||
tearDown(() {
|
||||
runner.crashFileSystem = const LocalFileSystem();
|
||||
io.restoreExitFunction();
|
||||
Cache.enableLocking();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue