mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
399 lines
13 KiB
Dart
399 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:convert';
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:file/memory.dart';
|
|
import 'package:flutter_tools/src/base/io.dart';
|
|
import 'package:flutter_tools/src/base/logger.dart';
|
|
import 'package:flutter_tools/src/base/os.dart';
|
|
import 'package:flutter_tools/src/base/platform.dart';
|
|
import 'package:flutter_tools/src/doctor.dart';
|
|
import 'package:flutter_tools/src/project.dart';
|
|
import 'package:flutter_tools/src/reporting/crash_reporting.dart';
|
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
|
import 'package:http/http.dart';
|
|
import 'package:http/testing.dart';
|
|
import 'package:test/fake.dart';
|
|
|
|
import '../src/common.dart';
|
|
import '../src/fake_process_manager.dart';
|
|
|
|
void main() {
|
|
late BufferLogger logger;
|
|
late FileSystem fs;
|
|
late TestUsage testUsage;
|
|
late Platform platform;
|
|
late OperatingSystemUtils operatingSystemUtils;
|
|
late StackTrace stackTrace;
|
|
|
|
setUp(() async {
|
|
logger = BufferLogger.test();
|
|
fs = MemoryFileSystem.test();
|
|
testUsage = TestUsage();
|
|
|
|
platform = FakePlatform(environment: <String, String>{});
|
|
operatingSystemUtils = OperatingSystemUtils(
|
|
fileSystem: fs,
|
|
logger: logger,
|
|
platform: platform,
|
|
processManager: FakeProcessManager.any(),
|
|
);
|
|
|
|
MockCrashReportSender.sendCalls = 0;
|
|
stackTrace = StackTrace.fromString('''
|
|
#0 _File.open.<anonymous closure> (dart:io/file_impl.dart:366:9)
|
|
#1 _rootRunUnary (dart:async/zone.dart:1141:38)''');
|
|
});
|
|
|
|
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'], testUsage.clientId);
|
|
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('CrashReporter.informUser provides basic instructions without PII', () async {
|
|
final CrashReporter crashReporter = CrashReporter(
|
|
fileSystem: fs,
|
|
logger: logger,
|
|
flutterProjectFactory: FlutterProjectFactory(fileSystem: fs, logger: logger),
|
|
);
|
|
|
|
final File file = fs.file('flutter_00.log');
|
|
|
|
await crashReporter.informUser(
|
|
CrashDetails(
|
|
command: 'arg1 arg2 arg3',
|
|
error: Exception('Dummy exception'),
|
|
stackTrace: StackTrace.current,
|
|
// Spaces are URL query encoded in the output, make it one word to make this test simpler.
|
|
doctorText: FakeDoctorText('Ignored', 'NoPIIFakeDoctorText'),
|
|
),
|
|
file,
|
|
);
|
|
|
|
expect(logger.statusText, contains('NoPIIFakeDoctorText'));
|
|
expect(logger.statusText, isNot(contains('Ignored')));
|
|
expect(logger.statusText, contains('https://github.com/flutter/flutter/issues/new'));
|
|
expect(logger.errorText, contains('A crash report has been written to ${file.path}.'));
|
|
});
|
|
|
|
testWithoutContext('suppress analytics', () async {
|
|
testUsage.suppressAnalytics = true;
|
|
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: CrashingCrashReportSender(const SocketException('no internets')),
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
expect(logger.traceText, isEmpty);
|
|
});
|
|
|
|
group('allow analytics', () {
|
|
setUp(() async {
|
|
testUsage.suppressAnalytics = false;
|
|
});
|
|
|
|
testWithoutContext('should send crash reports', () async {
|
|
final RequestInfo requestInfo = RequestInfo();
|
|
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: MockCrashReportSender(requestInfo),
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
await verifyCrashReportSent(requestInfo);
|
|
});
|
|
|
|
testWithoutContext('should print an explanatory message when there is a SocketException', () async {
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: CrashingCrashReportSender(const SocketException('no internets')),
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
expect(logger.errorText, contains('Failed to send crash report due to a network error'));
|
|
});
|
|
|
|
testWithoutContext('should print an explanatory message when there is an HttpException', () async {
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: CrashingCrashReportSender(const HttpException('no internets')),
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
expect(logger.errorText, contains('Failed to send crash report due to a network error'));
|
|
});
|
|
|
|
testWithoutContext('should print an explanatory message when there is a ClientException', () async {
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: CrashingCrashReportSender(const HttpException('no internets')),
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: ClientException('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
expect(logger.errorText, contains('Failed to send crash report due to a network error'));
|
|
});
|
|
|
|
testWithoutContext('should send only one crash report when sent many times', () async {
|
|
final RequestInfo requestInfo = RequestInfo();
|
|
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: MockCrashReportSender(requestInfo),
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
expect(MockCrashReportSender.sendCalls, 1);
|
|
await verifyCrashReportSent(requestInfo, crashes: 4);
|
|
});
|
|
|
|
testWithoutContext('should not send a crash report if on a user-branch', () async {
|
|
String? method;
|
|
Uri? uri;
|
|
|
|
final MockClient mockClient = MockClient((Request request) async {
|
|
method = request.method;
|
|
uri = request.url;
|
|
|
|
return Response(
|
|
'test-report-id',
|
|
200,
|
|
);
|
|
});
|
|
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: mockClient,
|
|
usage: testUsage,
|
|
platform: platform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => '[user-branch]/v1.2.3',
|
|
command: 'crash',
|
|
);
|
|
|
|
// Verify that the report wasn't sent
|
|
expect(method, null);
|
|
expect(uri, null);
|
|
|
|
expect(logger.traceText, isNot(contains('Crash report sent')));
|
|
});
|
|
|
|
testWithoutContext('can override base URL', () async {
|
|
Uri? uri;
|
|
final MockClient mockClient = MockClient((Request request) async {
|
|
uri = request.url;
|
|
return Response('test-report-id', 200);
|
|
});
|
|
|
|
final Platform environmentPlatform = FakePlatform(
|
|
environment: <String, String>{
|
|
'HOME': '/',
|
|
'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
|
|
},
|
|
script: Uri(scheme: 'data'),
|
|
);
|
|
|
|
final CrashReportSender crashReportSender = CrashReportSender(
|
|
client: mockClient,
|
|
usage: testUsage,
|
|
platform: environmentPlatform,
|
|
logger: logger,
|
|
operatingSystemUtils: operatingSystemUtils,
|
|
);
|
|
|
|
await crashReportSender.sendReport(
|
|
error: StateError('Test bad state error'),
|
|
stackTrace: stackTrace,
|
|
getFlutterVersion: () => 'test-version',
|
|
command: 'crash',
|
|
);
|
|
|
|
// 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',
|
|
},
|
|
));
|
|
});
|
|
});
|
|
}
|
|
|
|
class RequestInfo {
|
|
String? method;
|
|
Uri? uri;
|
|
Map<String, String>? fields;
|
|
}
|
|
|
|
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];
|
|
}).whereType<List<String>>(),
|
|
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(Exception exception) : super((Request request) async {
|
|
throw exception;
|
|
});
|
|
}
|
|
|
|
class FakeDoctorText extends Fake implements DoctorText {
|
|
FakeDoctorText(String text, String piiStrippedText)
|
|
: _text = text, _piiStrippedText = piiStrippedText;
|
|
|
|
@override
|
|
Future<String> get text async => _text;
|
|
final String _text;
|
|
|
|
@override
|
|
Future<String> get piiStrippedText async => _piiStrippedText;
|
|
final String _piiStrippedText;
|
|
}
|