| // Copyright 2017 The Chromium 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/src/base/platform.dart'; |
| import 'package:http/http.dart'; |
| import 'package:http/testing.dart'; |
| |
| import 'package:flutter_tools/runner.dart' as tools; |
| import 'package:flutter_tools/src/base/context.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/cache.dart'; |
| import 'package:flutter_tools/src/crash_reporting.dart'; |
| import 'package:flutter_tools/src/runner/flutter_command.dart'; |
| |
| import 'src/common.dart'; |
| import 'src/context.dart'; |
| |
| void main() { |
| group('crash reporting', () { |
| setUpAll(() { |
| Cache.disableLocking(); |
| }); |
| |
| setUp(() async { |
| tools.crashFileSystem = MemoryFileSystem(); |
| setExitFunctionForTests((_) { }); |
| }); |
| |
| tearDown(() { |
| tools.crashFileSystem = const LocalFileSystem(); |
| restoreExitFunction(); |
| }); |
| |
| testUsingContext('should send crash reports', () async { |
| String method; |
| Uri uri; |
| Map<String, String> fields; |
| |
| CrashReportSender.initializeWith(MockClient((Request request) async { |
| method = request.method; |
| 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); |
| 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; |
| return pair[0]; |
| }, |
| value: (dynamic value) { |
| final List<String> pair = value; |
| return pair[1]; |
| }, |
| ); |
| |
| 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(method, 'POST'); |
| expect(uri, Uri( |
| scheme: 'https', |
| host: 'clients2.google.com', |
| port: 443, |
| path: '/cr/report', |
| queryParameters: <String, String>{ |
| 'product': 'Flutter_Tools', |
| 'version': 'test-version', |
| }, |
| )); |
| expect(fields['uuid'], '00000000-0000-4000-0000-000000000000'); |
| expect(fields['product'], 'Flutter_Tools'); |
| expect(fields['version'], 'test-version'); |
| expect(fields['osName'], platform.operatingSystem); |
| expect(fields['osVersion'], 'fake OS name and version'); |
| expect(fields['type'], 'DartError'); |
| expect(fields['error_runtime_type'], 'StateError'); |
| expect(fields['error_message'], 'Bad state: Test bad state error'); |
| |
| final BufferLogger logger = context.get<Logger>(); |
| expect(logger.statusText, 'Sending crash report to Google.\n' |
| 'Crash report sent (report ID: test-report-id)\n'); |
| |
| // 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(1)); |
| expect(writtenFiles, contains('flutter_01.log')); |
| }, overrides: <Type, Generator>{ |
| Stdio: () => const _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); |
| |
| final BufferLogger logger = context.get<Logger>(); |
| expect(logger.statusText, ''); |
| }, overrides: <Type, Generator>{ |
| Stdio: () => const _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: () => const _NoStderr(), |
| }); |
| }); |
| } |
| |
| /// 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 null; |
| } |
| } |
| |
| class _NoStderr extends Stdio { |
| const _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 { } |
| } |