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

import 'package:args/command_runner.dart';
6
import 'package:file/memory.dart';
7
import 'package:flutter_tools/src/base/config.dart';
8
import 'package:flutter_tools/src/base/file_system.dart';
9
import 'package:flutter_tools/src/base/io.dart';
10
import 'package:flutter_tools/src/base/time.dart';
11
import 'package:flutter_tools/src/cache.dart';
12
import 'package:flutter_tools/src/commands/build.dart';
13 14
import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
15
import 'package:flutter_tools/src/doctor.dart';
16
import 'package:flutter_tools/src/features.dart';
17
import 'package:flutter_tools/src/globals.dart' as globals;
18
import 'package:flutter_tools/src/reporting/reporting.dart';
19
import 'package:flutter_tools/src/runner/flutter_command.dart';
20
import 'package:flutter_tools/src/version.dart';
21
import 'package:mockito/mockito.dart';
22
import 'package:platform/platform.dart';
23
import 'package:usage/usage_io.dart';
24

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

void main() {
30 31 32 33
  setUpAll(() {
    Cache.disableLocking();
  });

34
  group('analytics', () {
35
    Directory tempDir;
36
    MockFlutterConfig mockFlutterConfig;
37 38

    setUp(() {
39
      Cache.flutterRoot = '../..';
40
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.');
41
      mockFlutterConfig = MockFlutterConfig();
42 43 44
    });

    tearDown(() {
45
      tryToDelete(tempDir);
46 47 48
    });

    // Ensure we don't send anything when analytics is disabled.
49
    testUsingContext("doesn't send when disabled", () async {
50
      int count = 0;
51
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
52

53
      globals.flutterUsage.enabled = false;
54
      await createProject(tempDir);
55 56
      expect(count, 0);

57
      globals.flutterUsage.enabled = true;
58
      await createProject(tempDir);
59
      expect(count, globals.flutterUsage.isFirstRun ? 0 : 4);
60 61

      count = 0;
62
      globals.flutterUsage.enabled = false;
63
      final DoctorCommand doctorCommand = DoctorCommand();
64
      final CommandRunner<void>runner = createTestCommandRunner(doctorCommand);
65
      await runner.run(<String>['doctor']);
66

67
      expect(count, 0);
68
    }, overrides: <Type, Generator>{
69
      FlutterVersion: () => FlutterVersion(const SystemClock()),
70 71
      Usage: () => Usage(
        configDirOverride: tempDir.path,
72
        logFile: tempDir.childFile('analytics.log').path,
73
        runningOnBot: true,
74
      ),
75 76
    });

77
    // Ensure we don't send for the 'flutter config' command.
78
    testUsingContext("config doesn't send", () async {
79
      int count = 0;
80
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
81

82
      globals.flutterUsage.enabled = false;
83
      final ConfigCommand command = ConfigCommand();
84
      final CommandRunner<void> runner = createTestCommandRunner(command);
85 86 87
      await runner.run(<String>['config']);
      expect(count, 0);

88
      globals.flutterUsage.enabled = true;
89
      await runner.run(<String>['config']);
90

91
      expect(count, 0);
92
    }, overrides: <Type, Generator>{
93
      FlutterVersion: () => FlutterVersion(const SystemClock()),
94 95
      Usage: () => Usage(
        configDirOverride: tempDir.path,
96
        logFile: tempDir.childFile('analytics.log').path,
97
        runningOnBot: true,
98
      ),
99
    });
100 101

    testUsingContext('Usage records one feature in experiment setting', () async {
102
      when<bool>(mockFlutterConfig.getValue(flutterWebFeature.configSetting) as bool)
103
          .thenReturn(true);
104
      final Usage usage = Usage(runningOnBot: true);
105 106
      usage.sendCommand('test');

107
      final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
108

109
      expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
110 111 112
    }, overrides: <Type, Generator>{
      FlutterVersion: () => FlutterVersion(const SystemClock()),
      Config: () => mockFlutterConfig,
113 114 115 116
      Platform: () => FakePlatform(environment: <String, String>{
        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
      }),
      FileSystem: () => MemoryFileSystem(),
117
      ProcessManager: () => FakeProcessManager.any(),
118 119 120
    });

    testUsingContext('Usage records multiple features in experiment setting', () async {
121
      when<bool>(mockFlutterConfig.getValue(flutterWebFeature.configSetting) as bool)
122
          .thenReturn(true);
123
      when<bool>(mockFlutterConfig.getValue(flutterLinuxDesktopFeature.configSetting) as bool)
124
          .thenReturn(true);
125
      when<bool>(mockFlutterConfig.getValue(flutterMacOSDesktopFeature.configSetting) as bool)
126
          .thenReturn(true);
127
      final Usage usage = Usage(runningOnBot: true);
128 129
      usage.sendCommand('test');

130
      final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
131 132 133 134 135

      expect(
        globals.fs.file('test').readAsStringSync(),
        contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'),
      );
136 137 138
    }, overrides: <Type, Generator>{
      FlutterVersion: () => FlutterVersion(const SystemClock()),
      Config: () => mockFlutterConfig,
139 140 141 142
      Platform: () => FakePlatform(environment: <String, String>{
        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
      }),
      FileSystem: () => MemoryFileSystem(),
143
      ProcessManager: () => FakeProcessManager.any(),
144
    });
145
  });
146

147
  group('analytics with mocks', () {
148 149
    MemoryFileSystem memoryFileSystem;
    MockStdio mockStdio;
150
    Usage mockUsage;
151
    SystemClock mockClock;
152
    Doctor mockDoctor;
153 154 155
    List<int> mockTimes;

    setUp(() {
156 157
      memoryFileSystem = MemoryFileSystem();
      mockStdio = MockStdio();
158
      mockUsage = MockUsage();
159
      when(mockUsage.isFirstRun).thenReturn(false);
160 161
      mockClock = MockClock();
      mockDoctor = MockDoctor();
162
      when(mockClock.now()).thenAnswer(
163
        (Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
164 165 166 167 168
      );
    });

    testUsingContext('flutter commands send timing events', () async {
      mockTimes = <int>[1000, 2000];
169 170 171 172
      when(mockDoctor.diagnose(
        androidLicenses: false,
        verbose: false,
      )).thenAnswer((_) async => true);
173
      final DoctorCommand command = DoctorCommand();
174
      final CommandRunner<void> runner = createTestCommandRunner(command);
175 176 177 178 179
      await runner.run(<String>['doctor']);

      verify(mockClock.now()).called(2);

      expect(
180 181 182 183 184 185
        verify(mockUsage.sendTiming(
          captureAny,
          captureAny,
          captureAny,
          label: captureAnyNamed('label'),
        )).captured,
186
        <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'success'],
187 188
      );
    }, overrides: <Type, Generator>{
189
      SystemClock: () => mockClock,
190 191 192 193 194 195
      Doctor: () => mockDoctor,
      Usage: () => mockUsage,
    });

    testUsingContext('doctor fail sends warning', () async {
      mockTimes = <int>[1000, 2000];
196
      when(mockDoctor.diagnose(androidLicenses: false, verbose: false)).thenAnswer((_) async => false);
197
      final DoctorCommand command = DoctorCommand();
198
      final CommandRunner<void> runner = createTestCommandRunner(command);
199 200 201 202 203
      await runner.run(<String>['doctor']);

      verify(mockClock.now()).called(2);

      expect(
204 205 206 207 208 209
        verify(mockUsage.sendTiming(
          captureAny,
          captureAny,
          captureAny,
          label: captureAnyNamed('label'),
        )).captured,
210
        <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'warning'],
211 212
      );
    }, overrides: <Type, Generator>{
213
      SystemClock: () => mockClock,
214
      Doctor: () => mockDoctor,
215 216
      Usage: () => mockUsage,
    });
217 218

    testUsingContext('single command usage path', () async {
219
      final FlutterCommand doctorCommand = DoctorCommand();
220

221 222 223 224 225 226
      expect(await doctorCommand.usagePath, 'doctor');
    }, overrides: <Type, Generator>{
      Usage: () => mockUsage,
    });

    testUsingContext('compound command usage path', () async {
227
      final BuildCommand buildCommand = BuildCommand();
228
      final FlutterCommand buildApkCommand = buildCommand.subcommands['apk'] as FlutterCommand;
229

230 231 232 233
      expect(await buildApkCommand.usagePath, 'build/apk');
    }, overrides: <Type, Generator>{
      Usage: () => mockUsage,
    });
234 235 236 237 238 239

    testUsingContext('command sends localtime', () async {
      const int kMillis = 1000;
      mockTimes = <int>[kMillis];
      // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics
      // will be written to a file.
240 241 242 243
      final Usage usage = Usage(
        versionOverride: 'test',
        runningOnBot: true,
      );
244 245 246 247 248
      usage.suppressAnalytics = false;
      usage.enabled = true;

      usage.sendCommand('test');

249
      final String log = globals.fs.file('analytics.log').readAsStringSync();
250
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
251

252
      expect(log.contains(formatDateTime(dateTime)), isTrue);
253 254
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
255
      ProcessManager: () => FakeProcessManager.any(),
256 257 258 259 260 261 262 263 264 265 266 267 268 269
      SystemClock: () => mockClock,
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
      Stdio: () => mockStdio,
    });

    testUsingContext('event sends localtime', () async {
      const int kMillis = 1000;
      mockTimes = <int>[kMillis];
      // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics
      // will be written to a file.
270 271 272 273
      final Usage usage = Usage(
        versionOverride: 'test',
        runningOnBot: true,
      );
274 275 276 277 278
      usage.suppressAnalytics = false;
      usage.enabled = true;

      usage.sendEvent('test', 'test');

279
      final String log = globals.fs.file('analytics.log').readAsStringSync();
280
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
281

282
      expect(log.contains(formatDateTime(dateTime)), isTrue);
283 284
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
285
      ProcessManager: () => FakeProcessManager.any(),
286 287 288 289 290 291 292 293
      SystemClock: () => mockClock,
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
      Stdio: () => mockStdio,
    });
294 295
  });

296
  group('analytics bots', () {
297 298
    Directory tempDir;

299
    setUp(() {
300 301 302
      tempDir = globals.fs.systemTempDirectory.createTempSync(
        'flutter_tools_analytics_bots_test.',
      );
303 304 305 306
    });

    tearDown(() {
      tryToDelete(tempDir);
307 308
    });

309
    testUsingContext("don't send on bots with unknown version", () async {
310
      int count = 0;
311
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
312
      await createTestCommandRunner().run(<String>['--version']);
313

314
      expect(count, 0);
315
    }, overrides: <Type, Generator>{
316
      Usage: () => Usage(
317 318
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
319
        configDirOverride: tempDir.path,
320
        runningOnBot: false,
321 322 323
      ),
    });

324
    testUsingContext("don't send on bots even when opted in", () async {
325
      int count = 0;
326 327
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
      globals.flutterUsage.enabled = true;
328
      await createTestCommandRunner().run(<String>['--version']);
329

330 331
      expect(count, 0);
    }, overrides: <Type, Generator>{
332
      Usage: () => Usage(
333 334
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
335
        configDirOverride: tempDir.path,
336
        runningOnBot: false,
337
      ),
338
    });
339 340 341 342 343 344 345 346 347 348 349 350 351 352

    testUsingContext('Uses AnalyticsMock when .flutter cannot be created', () async {
      final Usage usage = Usage(
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/known',
        configDirOverride: tempDir.path,
        analyticsIOFactory: throwingAnalyticsIOFactory,
        runningOnBot: false,
      );
      final AnalyticsMock analyticsMock = AnalyticsMock();

      expect(usage.clientId, analyticsMock.clientId);
      expect(usage.suppressAnalytics, isTrue);
    });
353
  });
354
}
355

356 357 358 359 360 361 362 363 364 365
Analytics throwingAnalyticsIOFactory(
  String trackingId,
  String applicationName,
  String applicationVersion, {
  String analyticsUrl,
  Directory documentDirectory,
}) {
  throw const FileSystemException('Could not create file');
}

366
class MockUsage extends Mock implements Usage {}
367 368

class MockDoctor extends Mock implements Doctor {}
369 370

class MockFlutterConfig extends Mock implements Config {}