crash_reporting_test.dart 12.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:convert';
6

7
import 'package:file/file.dart';
8 9
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
10 11
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
12
import 'package:flutter_tools/src/base/platform.dart';
13
import 'package:flutter_tools/src/doctor.dart';
14
import 'package:flutter_tools/src/project.dart';
15 16 17
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:http/http.dart';
import 'package:http/testing.dart';
18
import 'package:mockito/mockito.dart';
19

20 21
import '../src/common.dart';
import '../src/context.dart';
22
import '../src/testbed.dart';
23 24

void main() {
25
  BufferLogger logger;
26
  FileSystem fs;
27 28 29 30 31 32
  MockUsage mockUsage;
  Platform platform;
  OperatingSystemUtils operatingSystemUtils;

  setUp(() async {
    logger = BufferLogger.test();
33
    fs = MemoryFileSystem.test();
34 35 36 37 38 39

    mockUsage = MockUsage();
    when(mockUsage.clientId).thenReturn('00000000-0000-4000-0000-000000000000');

    platform = FakePlatform(environment: <String, String>{}, operatingSystem: 'linux');
    operatingSystemUtils = OperatingSystemUtils(
40
      fileSystem: fs,
41 42 43 44
      logger: logger,
      platform: platform,
      processManager: FakeProcessManager.any(),
    );
45

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
    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)'));
  }

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
  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'));
  });

101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  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);
  });
121

122 123 124
  group('allow analytics', () {
    setUp(() async {
      when(mockUsage.suppressAnalytics).thenReturn(false);
125 126
    });

127
    testWithoutContext('should send crash reports', () async {
128
      final RequestInfo requestInfo = RequestInfo();
129

130 131 132 133 134 135 136 137 138 139 140 141 142
      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',
143 144
      );

145 146
      await verifyCrashReportSent(requestInfo);
    });
147

148 149 150 151 152 153 154 155
    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,
      );
156

157 158 159 160 161 162
      await crashReportSender.sendReport(
        error: StateError('Test bad state error'),
        stackTrace: null,
        getFlutterVersion: () => 'test-version',
        command: 'crash',
      );
163

164
      expect(logger.errorText, contains('Failed to send crash report due to a network error'));
165 166
    });

167 168 169 170 171 172 173 174
    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,
      );
175

176 177 178 179 180 181
      await crashReportSender.sendReport(
        error: StateError('Test bad state error'),
        stackTrace: null,
        getFlutterVersion: () => 'test-version',
        command: 'crash',
      );
182

183
      expect(logger.errorText, contains('Failed to send crash report due to a network error'));
184 185
    });

186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    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'));
    });

205
    testWithoutContext('should send only one crash report when sent many times', () async {
206 207
      final RequestInfo requestInfo = RequestInfo();

208 209 210 211 212 213 214
      final CrashReportSender crashReportSender = CrashReportSender(
        client: MockCrashReportSender(requestInfo),
        usage: mockUsage,
        platform: platform,
        logger: logger,
        operatingSystemUtils: operatingSystemUtils,
      );
215

216 217 218 219 220 221
      await crashReportSender.sendReport(
        error: StateError('Test bad state error'),
        stackTrace: null,
        getFlutterVersion: () => 'test-version',
        command: 'crash',
      );
222

223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
      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',
      );
243 244 245 246 247

      expect(MockCrashReportSender.sendCalls, 1);
      await verifyCrashReportSent(requestInfo, crashes: 4);
    });

248
    testWithoutContext('should not send a crash report if on a user-branch', () async {
249 250 251
      String method;
      Uri uri;

252
      final MockClient mockClient = MockClient((Request request) async {
253 254 255 256 257 258 259
        method = request.method;
        uri = request.url;

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

262 263 264 265 266 267
      final CrashReportSender crashReportSender = CrashReportSender(
        client: mockClient,
        usage: mockUsage,
        platform: platform,
        logger: logger,
        operatingSystemUtils: operatingSystemUtils,
268 269
      );

270 271 272 273 274 275
      await crashReportSender.sendReport(
        error: StateError('Test bad state error'),
        stackTrace: null,
        getFlutterVersion: () => '[user-branch]/v1.2.3',
        command: 'crash',
      );
276 277 278 279 280

      // Verify that the report wasn't sent
      expect(method, null);
      expect(uri, null);

281
      expect(logger.traceText, isNot(contains('Crash report sent')));
282 283
    });

284
    testWithoutContext('can override base URL', () async {
285
      Uri uri;
286
      final MockClient mockClient = MockClient((Request request) async {
287
        uri = request.url;
288
        return Response('test-report-id', 200);
289 290 291 292 293 294 295 296 297 298
      });

      final Platform environmentPlatform = FakePlatform(
        operatingSystem: 'linux',
        environment: <String, String>{
          'HOME': '/',
          'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
        },
        script: Uri(scheme: 'data'),
      );
299

300 301 302 303 304 305
      final CrashReportSender crashReportSender = CrashReportSender(
        client: mockClient,
        usage: mockUsage,
        platform: environmentPlatform,
        logger: logger,
        operatingSystemUtils: operatingSystemUtils,
306 307
      );

308 309 310 311 312 313
      await crashReportSender.sendReport(
        error: StateError('Test bad state error'),
        stackTrace: null,
        getFlutterVersion: () => 'test-version',
        command: 'crash',
      );
314 315 316

      // Verify that we sent the crash report.
      expect(uri, isNotNull);
317
      expect(uri, Uri(
318 319 320 321 322 323
        scheme: 'https',
        host: 'localhost',
        port: 12345,
        path: '/fake_server',
        queryParameters: <String, String>{
          'product': 'Flutter_Tools',
324
          'version': 'test-version',
325 326 327
        },
      ));
    });
328 329 330
  });
}

331 332 333 334 335 336 337 338
class RequestInfo {
  String method;
  Uri uri;
  Map<String, String> fields;
}

class MockCrashReportSender extends MockClient {
  MockCrashReportSender(RequestInfo crashInfo) : super((Request request) async {
339 340 341 342 343 344 345 346 347 348 349
    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) {
350 351 352 353 354 355 356 357
        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];
      })
358 359
        .where((List<String> pair) => pair != null),
      key: (dynamic key) {
360
        final List<String> pair = key as List<String>;
361 362 363
        return pair[0];
      },
      value: (dynamic value) {
364
        final List<String> pair = value as List<String>;
365 366 367 368 369 370 371 372 373
        return pair[1];
      },
    );

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

  static int sendCalls = 0;
376 377
}

378
class CrashingCrashReportSender extends MockClient {
379
  CrashingCrashReportSender(Object exception) : super((Request request) async {
380 381 382 383
    throw exception;
  });
}

384 385 386 387 388 389 390 391 392 393
/// 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>[];
}

394
class MockUsage extends Mock implements Usage {}