analytics_test.dart 10.8 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 : 3);
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 112 113 114 115 116 117 118 119 120 121 122
    });

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

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

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

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

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

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

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

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

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

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

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

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

    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);
221
      expect(log.contains(formatDateTime(dateTime)), isTrue);
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
      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);
246
      expect(log.contains(formatDateTime(dateTime)), isTrue);
247 248 249 250 251 252 253 254 255 256
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
      SystemClock: () => mockClock,
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
      Stdio: () => mockStdio,
    });
257 258
  });

259
  group('analytics bots', () {
260 261
    Directory tempDir;

262
    setUp(() {
263 264 265 266 267
      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
    });

    tearDown(() {
      tryToDelete(tempDir);
268 269
    });

270 271 272 273 274 275
    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);
276
    }, overrides: <Type, Generator>{
277
      Usage: () => Usage(
278 279
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
280
        configDirOverride: tempDir.path,
281 282 283 284 285 286 287 288 289 290 291
      ),
    });

    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>{
292
      Usage: () => Usage(
293 294
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
295
        configDirOverride: tempDir.path,
296
      ),
297 298
    });
  });
299
}
300 301

class MockUsage extends Mock implements Usage {}
302 303

class MockDoctor extends Mock implements Doctor {}
304 305

class MockFlutterConfig extends Mock implements Config {}