analytics_test.dart 11 KB
Newer Older
1 2 3 4 5
// Copyright 2016 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 '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 11
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/time.dart';
12
import 'package:flutter_tools/src/cache.dart';
13
import 'package:flutter_tools/src/commands/build.dart';
14 15
import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
16
import 'package:flutter_tools/src/doctor.dart';
17 18
import 'package:flutter_tools/src/features.dart';
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

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

void main() {
  group('analytics', () {
30
    Directory tempDir;
31
    MockFlutterConfig mockFlutterConfig;
32

33 34 35 36
    setUpAll(() {
      Cache.disableLocking();
    });

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

    tearDown(() {
44
      tryToDelete(tempDir);
45 46 47 48 49 50 51 52
    });

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

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

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

      count = 0;
      flutterUsage.enabled = false;
62
      final DoctorCommand doctorCommand = DoctorCommand();
63
      final CommandRunner<void>runner = createTestCommandRunner(doctorCommand);
64
      await runner.run(<String>['doctor']);
65
      expect(count, 0);
66
    }, overrides: <Type, Generator>{
67
      FlutterVersion: () => FlutterVersion(const SystemClock()),
68 69
      Usage: () => Usage(
        configDirOverride: tempDir.path,
70
        logFile: tempDir.childFile('analytics.log').path,
71
      ),
72 73
    });

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

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

      flutterUsage.enabled = true;
      await runner.run(<String>['config']);
      expect(count, 0);
88
    }, overrides: <Type, Generator>{
89
      FlutterVersion: () => FlutterVersion(const SystemClock()),
90 91
      Usage: () => Usage(
        configDirOverride: tempDir.path,
92
        logFile: tempDir.childFile('analytics.log').path,
93
      ),
94
    });
95 96 97 98 99 100 101

    testUsingContext('Usage records one feature in experiment setting', () async {
      when<bool>(mockFlutterConfig.getValue(flutterWebFeature.configSetting))
          .thenReturn(true);
      final Usage usage = Usage();
      usage.sendCommand('test');

102 103
      final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
      expect(fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
104 105 106
    }, overrides: <Type, Generator>{
      FlutterVersion: () => FlutterVersion(const SystemClock()),
      Config: () => mockFlutterConfig,
107 108 109 110
      Platform: () => FakePlatform(environment: <String, String>{
        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
      }),
      FileSystem: () => MemoryFileSystem(),
111
      ProcessManager: () => FakeProcessManager.any(),
112 113 114 115 116 117 118 119 120 121 122 123
    });

    testUsingContext('Usage records multiple features in experiment setting', () async {
      when<bool>(mockFlutterConfig.getValue(flutterWebFeature.configSetting))
          .thenReturn(true);
      when<bool>(mockFlutterConfig.getValue(flutterLinuxDesktopFeature.configSetting))
          .thenReturn(true);
      when<bool>(mockFlutterConfig.getValue(flutterMacOSDesktopFeature.configSetting))
          .thenReturn(true);
      final Usage usage = Usage();
      usage.sendCommand('test');

124 125
      final String featuresKey = cdKey(CustomDimensions.enabledFlutterFeatures);
      expect(fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'));
126 127 128
    }, overrides: <Type, Generator>{
      FlutterVersion: () => FlutterVersion(const SystemClock()),
      Config: () => mockFlutterConfig,
129 130 131 132
      Platform: () => FakePlatform(environment: <String, String>{
        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
      }),
      FileSystem: () => MemoryFileSystem(),
133
      ProcessManager: () => FakeProcessManager.any(),
134
    });
135
  });
136

137
  group('analytics with mocks', () {
138 139
    MemoryFileSystem memoryFileSystem;
    MockStdio mockStdio;
140
    Usage mockUsage;
141
    SystemClock mockClock;
142
    Doctor mockDoctor;
143 144 145
    List<int> mockTimes;

    setUp(() {
146 147
      memoryFileSystem = MemoryFileSystem();
      mockStdio = MockStdio();
148
      mockUsage = MockUsage();
149
      when(mockUsage.isFirstRun).thenReturn(false);
150 151
      mockClock = MockClock();
      mockDoctor = MockDoctor();
152
      when(mockClock.now()).thenAnswer(
153
        (Invocation _) => DateTime.fromMillisecondsSinceEpoch(mockTimes.removeAt(0))
154 155 156 157 158
      );
    });

    testUsingContext('flutter commands send timing events', () async {
      mockTimes = <int>[1000, 2000];
159
      when(mockDoctor.diagnose(androidLicenses: false, verbose: false)).thenAnswer((_) async => true);
160
      final DoctorCommand command = DoctorCommand();
161
      final CommandRunner<void> runner = createTestCommandRunner(command);
162 163 164 165 166
      await runner.run(<String>['doctor']);

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

      expect(
167
        verify(mockUsage.sendTiming(captureAny, captureAny, captureAny, label: captureAnyNamed('label'))).captured,
168
        <dynamic>['flutter', 'doctor', const Duration(milliseconds: 1000), 'success'],
169 170
      );
    }, overrides: <Type, Generator>{
171
      SystemClock: () => mockClock,
172 173 174 175 176 177
      Doctor: () => mockDoctor,
      Usage: () => mockUsage,
    });

    testUsingContext('doctor fail sends warning', () async {
      mockTimes = <int>[1000, 2000];
178
      when(mockDoctor.diagnose(androidLicenses: false, verbose: false)).thenAnswer((_) async => false);
179
      final DoctorCommand command = DoctorCommand();
180
      final CommandRunner<void> runner = createTestCommandRunner(command);
181 182 183 184 185
      await runner.run(<String>['doctor']);

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

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

    testUsingContext('single command usage path', () async {
196
      final FlutterCommand doctorCommand = DoctorCommand();
197 198 199 200 201 202
      expect(await doctorCommand.usagePath, 'doctor');
    }, overrides: <Type, Generator>{
      Usage: () => mockUsage,
    });

    testUsingContext('compound command usage path', () async {
203
      final BuildCommand buildCommand = BuildCommand();
204 205 206 207 208
      final FlutterCommand buildApkCommand = buildCommand.subcommands['apk'];
      expect(await buildApkCommand.usagePath, 'build/apk');
    }, overrides: <Type, Generator>{
      Usage: () => mockUsage,
    });
209 210 211 212 213 214 215 216 217 218 219 220 221 222

    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.
      final Usage usage = Usage(versionOverride: 'test');
      usage.suppressAnalytics = false;
      usage.enabled = true;

      usage.sendCommand('test');

      final String log = fs.file('analytics.log').readAsStringSync();
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
223
      expect(log.contains(formatDateTime(dateTime)), isTrue);
224 225
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
226
      ProcessManager: () => FakeProcessManager.any(),
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
      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.
      final Usage usage = Usage(versionOverride: 'test');
      usage.suppressAnalytics = false;
      usage.enabled = true;

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

      final String log = fs.file('analytics.log').readAsStringSync();
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
249
      expect(log.contains(formatDateTime(dateTime)), isTrue);
250 251
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
252
      ProcessManager: () => FakeProcessManager.any(),
253 254 255 256 257 258 259 260
      SystemClock: () => mockClock,
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
      Stdio: () => mockStdio,
    });
261 262
  });

263
  group('analytics bots', () {
264 265
    Directory tempDir;

266
    setUp(() {
267 268 269 270 271
      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
    });

    tearDown(() {
      tryToDelete(tempDir);
272 273
    });

274 275 276 277 278 279
    testUsingContext('don\'t send on bots', () async {
      int count = 0;
      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);

      await createTestCommandRunner().run(<String>['--version']);
      expect(count, 0);
280
    }, overrides: <Type, Generator>{
281
      Usage: () => Usage(
282 283
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
284
        configDirOverride: tempDir.path,
285 286 287 288 289 290 291 292 293 294 295
      ),
    });

    testUsingContext('don\'t send on bots even when opted in', () async {
      int count = 0;
      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
      flutterUsage.enabled = true;

      await createTestCommandRunner().run(<String>['--version']);
      expect(count, 0);
    }, overrides: <Type, Generator>{
296
      Usage: () => Usage(
297 298
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
299
        configDirOverride: tempDir.path,
300
      ),
301 302
    });
  });
303
}
304 305

class MockUsage extends Mock implements Usage {}
306 307

class MockDoctor extends Mock implements Doctor {}
308 309

class MockFlutterConfig extends Mock implements Config {}