analytics_test.dart 11.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

11
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:flutter_tools/src/globals.dart' as globals;
22
import 'package:mockito/mockito.dart';
23
import 'package:platform/platform.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 51 52 53
      int count = 0;
      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    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.
218 219 220 221
      final Usage usage = Usage(
        versionOverride: 'test',
        runningOnBot: true,
      );
222 223 224 225 226
      usage.suppressAnalytics = false;
      usage.enabled = true;

      usage.sendCommand('test');

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

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

256
      final String log = globals.fs.file('analytics.log').readAsStringSync();
257
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
258
      expect(log.contains(formatDateTime(dateTime)), isTrue);
259 260
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
261
      ProcessManager: () => FakeProcessManager.any(),
262 263 264 265 266 267 268 269
      SystemClock: () => mockClock,
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
      Stdio: () => mockStdio,
    });
270 271
  });

272
  group('analytics bots', () {
273 274
    Directory tempDir;

275
    setUp(() {
276
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
277 278 279 280
    });

    tearDown(() {
      tryToDelete(tempDir);
281 282
    });

283
    testUsingContext("don't send on bots with unknown version", () async {
284 285 286 287 288
      int count = 0;
      flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);

      await createTestCommandRunner().run(<String>['--version']);
      expect(count, 0);
289
    }, overrides: <Type, Generator>{
290
      Usage: () => Usage(
291 292
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
293
        configDirOverride: tempDir.path,
294
        runningOnBot: false,
295 296 297
      ),
    });

298
    testUsingContext("don't send on bots even when opted in", () async {
299 300 301 302 303 304 305
      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>{
306
      Usage: () => Usage(
307 308
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
309
        configDirOverride: tempDir.path,
310
        runningOnBot: false,
311
      ),
312 313
    });
  });
314
}
315 316

class MockUsage extends Mock implements Usage {}
317 318

class MockDoctor extends Mock implements Doctor {}
319 320

class MockFlutterConfig extends Mock implements Config {}