| // 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/reporting.dart'; |
| import 'package:http/http.dart'; |
| import 'package:http/testing.dart'; |
| import 'package:mockito/mockito.dart'; |
| |
| import '../src/common.dart'; |
| import '../src/context.dart'; |
| import '../src/testbed.dart'; |
| |
| void main() { |
| BufferLogger logger; |
| FileSystem fs; |
| MockUsage mockUsage; |
| Platform platform; |
| OperatingSystemUtils operatingSystemUtils; |
| |
| setUp(() async { |
| logger = BufferLogger.test(); |
| fs = MemoryFileSystem.test(); |
| |
| mockUsage = MockUsage(); |
| when(mockUsage.clientId).thenReturn('00000000-0000-4000-0000-000000000000'); |
| |
| platform = FakePlatform(environment: <String, String>{}, operatingSystem: 'linux'); |
| operatingSystemUtils = OperatingSystemUtils( |
| fileSystem: fs, |
| 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('CrashReporter.informUser provides basic instructions', () async { |
| final CrashReporter crashReporter = CrashReporter( |
| fileSystem: fs, |
| logger: logger, |
| flutterProjectFactory: FlutterProjectFactory(fileSystem: fs, logger: logger), |
| client: FakeHttpClient(), |
| ); |
| |
| final File file = fs.file('flutter_00.log'); |
| |
| await crashReporter.informUser( |
| CrashDetails( |
| command: 'arg1 arg2 arg3', |
| error: Exception('Dummy exception'), |
| stackTrace: StackTrace.current, |
| doctorText: 'Fake doctor text'), |
| file, |
| ); |
| |
| expect(logger.errorText, contains('A crash report has been written to ${file.path}.')); |
| expect(logger.statusText, contains('https://github.com/flutter/flutter/issues/new')); |
| }); |
| |
| 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 { |
| when(mockUsage.suppressAnalytics).thenReturn(false); |
| }); |
| |
| testWithoutContext('should send crash reports', () async { |
| final RequestInfo requestInfo = RequestInfo(); |
| |
| 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', |
| ); |
| |
| 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: 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.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: 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.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: mockUsage, |
| platform: platform, |
| logger: logger, |
| operatingSystemUtils: operatingSystemUtils, |
| ); |
| |
| await crashReportSender.sendReport( |
| error: ClientException('Test bad state error'), |
| stackTrace: null, |
| 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: mockUsage, |
| platform: platform, |
| logger: logger, |
| operatingSystemUtils: operatingSystemUtils, |
| ); |
| |
| 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', |
| ); |
| |
| 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', |
| ); |
| |
| 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: mockUsage, |
| platform: platform, |
| logger: logger, |
| operatingSystemUtils: operatingSystemUtils, |
| ); |
| |
| 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(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( |
| operatingSystem: 'linux', |
| 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: 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); |
| 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]; |
| }) |
| .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; |
| }); |
| } |
| |
| /// 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 MockUsage extends Mock implements Usage {} |