crash_reporting_test.dart 8.68 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:convert';
7 8 9 10

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
11
import 'package:flutter_tools/src/base/platform.dart';
12 13 14
import 'package:http/http.dart';
import 'package:http/testing.dart';

15
import 'package:flutter_tools/runner.dart' as tools;
16 17 18
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
19
import 'package:flutter_tools/src/cache.dart';
20 21
import 'package:flutter_tools/src/crash_reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
22
import 'package:pedantic/pedantic.dart';
23 24

import 'src/common.dart';
25 26 27 28
import 'src/context.dart';

void main() {
  group('crash reporting', () {
29 30 31 32
    setUpAll(() {
      Cache.disableLocking();
    });

33
    setUp(() async {
34
      tools.crashFileSystem = MemoryFileSystem();
35 36 37 38
      setExitFunctionForTests((_) { });
    });

    tearDown(() {
39
      tools.crashFileSystem = const LocalFileSystem();
40 41 42 43
      restoreExitFunction();
    });

    testUsingContext('should send crash reports', () async {
44
      final RequestInfo requestInfo = RequestInfo();
45

46
      CrashReportSender.initializeWith(MockCrashReportSender(requestInfo));
47
      final int exitCode = await tools.run(
48
        <String>['crash'],
49
        <FlutterCommand>[_CrashCommand()],
50 51 52 53 54
        reportCrashes: true,
        flutterVersion: 'test-version',
      );
      expect(exitCode, 1);

55 56 57 58
      await verifyCrashReportSent(requestInfo);
    }, overrides: <Type, Generator>{
      Stdio: () => const _NoStderr(),
    });
59

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
    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, equals(1));
      await verifyCrashReportSent(requestInfo);
78 79
    }, overrides: <Type, Generator>{
      Stdio: () => const _NoStderr(),
80
    });
81

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
    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(),
    });

115 116
    testUsingContext('can override base URL', () async {
      Uri uri;
117
      CrashReportSender.initializeWith(MockClient((Request request) async {
118
        uri = request.url;
119
        return Response('test-report-id', 200);
120 121 122 123
      }));

      final int exitCode = await tools.run(
        <String>['crash'],
124
        <FlutterCommand>[_CrashCommand()],
125 126 127 128 129 130 131 132
        reportCrashes: true,
        flutterVersion: 'test-version',
      );

      expect(exitCode, 1);

      // Verify that we sent the crash report.
      expect(uri, isNotNull);
133
      expect(uri, Uri(
134 135 136 137 138 139
        scheme: 'https',
        host: 'localhost',
        port: 12345,
        path: '/fake_server',
        queryParameters: <String, String>{
          'product': 'Flutter_Tools',
140
          'version': 'test-version',
141 142
        },
      ));
143
    }, overrides: <Type, Generator>{
144
      Platform: () => FakePlatform(
145 146
        operatingSystem: 'linux',
        environment: <String, String>{
147
          'HOME': '/',
148 149
          'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
        },
150
        script: Uri(scheme: 'data'),
151 152 153
      ),
      Stdio: () => const _NoStderr(),
    });
154 155 156
  });
}

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
class RequestInfo {
  String method;
  Uri uri;
  Map<String, String> fields;
}

Future<void> verifyCrashReportSent(RequestInfo crashInfo) 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'], 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');

  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'));
}

class MockCrashReportSender extends MockClient {
  MockCrashReportSender(RequestInfo crashInfo) : super((Request request) async {
      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;
          return pair[0];
        },
        value: (dynamic value) {
          final List<String> pair = value;
          return pair[1];
        },
      );

      return Response(
        'test-report-id',
        200,
      );
    });
}

234 235 236 237 238 239 240 241 242 243
/// 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
244
  Future<FlutterCommandResult> runCommand() async {
245
    void fn1() {
246
      throw StateError('Test bad state error');
247 248 249 250 251 252 253 254 255 256 257
    }

    void fn2() {
      fn1();
    }

    void fn3() {
      fn2();
    }

    fn3();
258 259

    return null;
260 261
  }
}
262

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
/// 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
  }
}

281 282 283 284 285 286 287 288 289 290 291 292 293 294
class _NoStderr extends Stdio {
  const _NoStderr();

  @override
  IOSink get stderr => const _NoopIOSink();
}

class _NoopIOSink implements IOSink {
  const _NoopIOSink();

  @override
  Encoding get encoding => utf8;

  @override
295
  set encoding(_) => throw UnsupportedError('');
296 297

  @override
298
  void add(_) { }
299 300

  @override
301
  void write(_) { }
302 303

  @override
304
  void writeAll(_, [ __ = '' ]) { }
305 306

  @override
307
  void writeln([ _ = '' ]) { }
308 309

  @override
310
  void writeCharCode(_) { }
311 312

  @override
313
  void addError(_, [ __ ]) { }
314 315

  @override
316
  Future<dynamic> addStream(_) async { }
317 318

  @override
319
  Future<dynamic> flush() async { }
320 321

  @override
322
  Future<dynamic> close() async { }
323 324

  @override
325
  Future<dynamic> get done async { }
326
}