crash_reporting_test.dart 6.41 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 23

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

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

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

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

    testUsingContext('should send crash reports', () async {
      String method;
      Uri uri;
45
      Map<String, String> fields;
46

47
      CrashReportSender.initializeWith(MockClient((Request request) async {
48 49
        method = request.method;
        uri = request.url;
50 51 52 53

        // 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);
54
        fields = Map<String, String>.fromIterable(
55
          utf8.decode(request.bodyBytes)
56 57
              .split('--$boundary')
              .map<List<String>>((String part) {
58
                final Match nameMatch = RegExp(r'name="(.*)"').firstMatch(part);
59 60 61 62 63 64 65
                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),
66 67 68 69 70 71 72
          key: (dynamic key) {
            final List<String> pair = key;
            return pair[0];
          },
          value: (dynamic value) {
            final List<String> pair = value;
            return pair[1];
73
          },
74 75
        );

76
        return Response(
77
            'test-report-id',
78
            200,
79 80 81
        );
      }));

82
      final int exitCode = await tools.run(
83
        <String>['crash'],
84
        <FlutterCommand>[_CrashCommand()],
85 86 87 88 89 90 91 92
        reportCrashes: true,
        flutterVersion: 'test-version',
      );

      expect(exitCode, 1);

      // Verify that we sent the crash report.
      expect(method, 'POST');
93
      expect(uri, Uri(
94 95 96
        scheme: 'https',
        host: 'clients2.google.com',
        port: 443,
97
        path: '/cr/report',
98 99
        queryParameters: <String, String>{
          'product': 'Flutter_Tools',
100
          'version': 'test-version',
101 102
        },
      ));
103 104 105 106 107 108 109
      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');
110
      expect(fields['error_message'], 'Bad state: Test bad state error');
111

112
      final BufferLogger logger = context[Logger];
113 114 115 116
      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.
117
      final List<String> writtenFiles =
118 119 120 121
        (await tools.crashFileSystem.directory('/').list(recursive: true).toList())
            .map((FileSystemEntity e) => e.path).toList();
      expect(writtenFiles, hasLength(1));
      expect(writtenFiles, contains('flutter_01.log'));
122 123
    }, overrides: <Type, Generator>{
      Stdio: () => const _NoStderr(),
124
    });
125 126 127

    testUsingContext('can override base URL', () async {
      Uri uri;
128
      CrashReportSender.initializeWith(MockClient((Request request) async {
129
        uri = request.url;
130
        return Response('test-report-id', 200);
131 132 133 134
      }));

      final int exitCode = await tools.run(
        <String>['crash'],
135
        <FlutterCommand>[_CrashCommand()],
136 137 138 139 140 141 142 143
        reportCrashes: true,
        flutterVersion: 'test-version',
      );

      expect(exitCode, 1);

      // Verify that we sent the crash report.
      expect(uri, isNotNull);
144
      expect(uri, Uri(
145 146 147 148 149 150
        scheme: 'https',
        host: 'localhost',
        port: 12345,
        path: '/fake_server',
        queryParameters: <String, String>{
          'product': 'Flutter_Tools',
151
          'version': 'test-version',
152 153
        },
      ));
154
    }, overrides: <Type, Generator>{
155
      Platform: () => FakePlatform(
156 157 158 159
        operatingSystem: 'linux',
        environment: <String, String>{
          'FLUTTER_CRASH_SERVER_BASE_URL': 'https://localhost:12345/fake_server',
        },
160
        script: Uri(scheme: 'data'),
161 162 163
      ),
      Stdio: () => const _NoStderr(),
    });
164 165 166 167 168 169 170 171 172 173 174 175 176
  });
}

/// 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
177
  Future<FlutterCommandResult> runCommand() async {
178
    void fn1() {
179
      throw StateError('Test bad state error');
180 181 182 183 184 185 186 187 188 189 190
    }

    void fn2() {
      fn1();
    }

    void fn3() {
      fn2();
    }

    fn3();
191 192

    return null;
193 194
  }
}
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209

class _NoStderr extends Stdio {
  const _NoStderr();

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

class _NoopIOSink implements IOSink {
  const _NoopIOSink();

  @override
  Encoding get encoding => utf8;

  @override
210
  set encoding(_) => throw UnsupportedError('');
211 212

  @override
213
  void add(_) { }
214 215

  @override
216
  void write(_) { }
217 218

  @override
219
  void writeAll(_, [ __ = '' ]) { }
220 221

  @override
222
  void writeln([ _ = '' ]) { }
223 224

  @override
225
  void writeCharCode(_) { }
226 227

  @override
228
  void addError(_, [ __ ]) { }
229 230

  @override
231
  Future<dynamic> addStream(_) async { }
232 233

  @override
234
  Future<dynamic> flush() async { }
235 236

  @override
237
  Future<dynamic> close() async { }
238 239

  @override
240
  Future<dynamic> get done async { }
241
}