mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
448 lines
13 KiB
Dart
448 lines
13 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: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/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:platform/platform.dart';
|
|
|
|
import '../src/common.dart';
|
|
import '../src/context.dart';
|
|
|
|
void main() {
|
|
group('crash reporting', () {
|
|
setUpAll(() {
|
|
Cache.disableLocking();
|
|
});
|
|
|
|
setUp(() async {
|
|
tools.crashFileSystem = MemoryFileSystem();
|
|
setExitFunctionForTests((_) { });
|
|
MockCrashReportSender.sendCalls = 0;
|
|
});
|
|
|
|
tearDown(() {
|
|
tools.crashFileSystem = const LocalFileSystem();
|
|
restoreExitFunction();
|
|
});
|
|
|
|
testUsingContext('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',
|
|
);
|
|
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);
|
|
});
|
|
|
|
CrashReportSender.initializeWith(
|
|
CrashingCrashReportSender(const SocketException('no internets')));
|
|
|
|
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(),
|
|
});
|
|
|
|
testUsingContext('should print an explanatory message when there is an HttpException', () async {
|
|
final Completer<int> exitCodeCompleter = Completer<int>();
|
|
setExitFunctionForTests((int exitCode) {
|
|
exitCodeCompleter.complete(exitCode);
|
|
});
|
|
|
|
CrashReportSender.initializeWith(
|
|
CrashingCrashReportSender(const HttpException('no internets')));
|
|
|
|
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(),
|
|
});
|
|
|
|
testUsingContext('should send crash reports when async throws', () async {
|
|
final Completer<int> exitCodeCompleter = Completer<int>();
|
|
setExitFunctionForTests((int exitCode) {
|
|
exitCodeCompleter.complete(exitCode);
|
|
});
|
|
|
|
final RequestInfo requestInfo = RequestInfo();
|
|
|
|
CrashReportSender.initializeWith(MockCrashReportSender(requestInfo));
|
|
|
|
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(),
|
|
});
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
final RequestInfo requestInfo = RequestInfo();
|
|
final MockCrashReportSender sender = MockCrashReportSender(requestInfo);
|
|
CrashReportSender.initializeWith(sender);
|
|
|
|
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 {
|
|
String method;
|
|
Uri uri;
|
|
|
|
CrashReportSender.initializeWith(MockClient((Request request) async {
|
|
method = request.method;
|
|
uri = request.url;
|
|
|
|
return Response(
|
|
'test-report-id',
|
|
200,
|
|
);
|
|
}));
|
|
|
|
final int exitCode = await tools.run(
|
|
<String>['crash'],
|
|
<FlutterCommand>[_CrashCommand()],
|
|
reportCrashes: true,
|
|
flutterVersion: '[user-branch]/v1.2.3',
|
|
);
|
|
|
|
expect(exitCode, 1);
|
|
|
|
// 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(),
|
|
});
|
|
|
|
testUsingContext('can override base URL', () async {
|
|
Uri uri;
|
|
CrashReportSender.initializeWith(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',
|
|
);
|
|
|
|
expect(exitCode, 1);
|
|
|
|
// Verify that we sent the crash report.
|
|
expect(uri, isNotNull);
|
|
expect(uri, Uri(
|
|
scheme: 'https',
|
|
host: 'localhost',
|
|
port: 12345,
|
|
path: '/fake_server',
|
|
queryParameters: <String, String>{
|
|
'product': 'Flutter_Tools',
|
|
'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(),
|
|
});
|
|
});
|
|
}
|
|
|
|
class RequestInfo {
|
|
String method;
|
|
Uri uri;
|
|
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++;
|
|
crashInfo.method = request.method;
|
|
crashInfo.uri = request.url;
|
|
|
|
// A very ad-hoc multipart request parser. Good enough for this test.
|
|
String boundary = request.headers['Content-Type'];
|
|
boundary = boundary.substring(boundary.indexOf('boundary=') + 9);
|
|
crashInfo.fields = Map<String, String>.fromIterable(
|
|
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];
|
|
})
|
|
.where((List<String> pair) => pair != null),
|
|
key: (dynamic key) {
|
|
final List<String> pair = key as List<String>;
|
|
return pair[0];
|
|
},
|
|
value: (dynamic value) {
|
|
final List<String> pair = value as List<String>;
|
|
return pair[1];
|
|
},
|
|
);
|
|
|
|
return Response(
|
|
'test-report-id',
|
|
200,
|
|
);
|
|
});
|
|
|
|
static int sendCalls = 0;
|
|
}
|
|
|
|
class CrashingCrashReportSender extends MockClient {
|
|
CrashingCrashReportSender(Object exception) : super((Request request) async {
|
|
throw exception;
|
|
});
|
|
}
|
|
|
|
/// 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 {
|
|
@override
|
|
List<DoctorValidator> get validators => <DoctorValidator>[];
|
|
|
|
@override
|
|
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 { }
|
|
}
|