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

5 6
// @dart = 2.8

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

28 29
import '../src/common.dart';
import '../src/context.dart';
30
import '../src/fakes.dart';
31
import '../src/test_flutter_command_runner.dart';
32 33

void main() {
34 35 36 37
  setUpAll(() {
    Cache.disableLocking();
  });

38
  group('analytics', () {
39
    Directory tempDir;
40
    Config testConfig;
41 42

    setUp(() {
43
      Cache.flutterRoot = '../..';
44
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.');
45
      testConfig = Config.test();
46 47 48
    });

    tearDown(() {
49
      tryToDelete(tempDir);
50 51 52
    });

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

57 58 59
      final FlutterCommand command = FakeFlutterCommand();
      final CommandRunner<void>runner = createTestCommandRunner(command);

60
      globals.flutterUsage.enabled = false;
61
      await runner.run(<String>['fake']);
62 63
      expect(count, 0);

64
      globals.flutterUsage.enabled = true;
65 66 67 68 69
      await runner.run(<String>['fake']);
      // LogToFileAnalytics isFirstRun is hardcoded to false
      // so this usage will never act like the first run
      // (which would not send usage).
      expect(count, 4);
70 71

      count = 0;
72
      globals.flutterUsage.enabled = false;
73
      await runner.run(<String>['fake']);
74

75
      expect(count, 0);
76
    }, overrides: <Type, Generator>{
77
      FlutterVersion: () => FlutterVersion(clock: const SystemClock()),
78 79
      Usage: () => Usage(
        configDirOverride: tempDir.path,
80
        logFile: tempDir.childFile('analytics.log').path,
81
        runningOnBot: true,
82
      ),
83 84
    });

85
    // Ensure we don't send for the 'flutter config' command.
86
    testUsingContext("config doesn't send", () async {
87
      int count = 0;
88
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
89

90
      globals.flutterUsage.enabled = false;
91
      final ConfigCommand command = ConfigCommand();
92
      final CommandRunner<void> runner = createTestCommandRunner(command);
93 94 95
      await runner.run(<String>['config']);
      expect(count, 0);

96
      globals.flutterUsage.enabled = true;
97
      await runner.run(<String>['config']);
98

99
      expect(count, 0);
100
    }, overrides: <Type, Generator>{
101
      FlutterVersion: () => FlutterVersion(clock: const SystemClock()),
102 103
      Usage: () => Usage(
        configDirOverride: tempDir.path,
104
        logFile: tempDir.childFile('analytics.log').path,
105
        runningOnBot: true,
106
      ),
107
    });
108 109

    testUsingContext('Usage records one feature in experiment setting', () async {
110
      testConfig.setValue(flutterWebFeature.configSetting, true);
111
      final Usage usage = Usage(runningOnBot: true);
112 113
      usage.sendCommand('test');

114
      final String featuresKey = cdKey(CustomDimensionsEnum.enabledFlutterFeatures);
115

116
      expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
117
    }, overrides: <Type, Generator>{
118
      FlutterVersion: () => FlutterVersion(clock: const SystemClock()),
119
      Config: () => testConfig,
120 121 122
      Platform: () => FakePlatform(environment: <String, String>{
        'FLUTTER_ANALYTICS_LOG_FILE': 'test',
      }),
123
      FileSystem: () => MemoryFileSystem.test(),
124
      ProcessManager: () => FakeProcessManager.any(),
125 126 127
    });

    testUsingContext('Usage records multiple features in experiment setting', () async {
128 129 130
      testConfig.setValue(flutterWebFeature.configSetting, true);
      testConfig.setValue(flutterLinuxDesktopFeature.configSetting, true);
      testConfig.setValue(flutterMacOSDesktopFeature.configSetting, true);
131
      final Usage usage = Usage(runningOnBot: true);
132 133
      usage.sendCommand('test');

134
      final String featuresKey = cdKey(CustomDimensionsEnum.enabledFlutterFeatures);
135 136 137 138 139

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

151
  group('analytics with fakes', () {
152
    MemoryFileSystem memoryFileSystem;
153
    FakeStdio fakeStdio;
154
    TestUsage testUsage;
155
    FakeClock fakeClock;
156
    FakeDoctor doctor;
157 158

    setUp(() {
159
      memoryFileSystem = MemoryFileSystem.test();
160
      fakeStdio = FakeStdio();
161
      testUsage = TestUsage();
162
      fakeClock = FakeClock();
163
      doctor = FakeDoctor();
164 165 166
    });

    testUsingContext('flutter commands send timing events', () async {
167
      fakeClock.times = <int>[1000, 2000];
168
      doctor.diagnoseSucceeds = true;
169
      final DoctorCommand command = DoctorCommand();
170
      final CommandRunner<void> runner = createTestCommandRunner(command);
171 172
      await runner.run(<String>['doctor']);

173 174 175 176 177
      expect(testUsage.timings, contains(
        const TestTimingEvent(
            'flutter', 'doctor', Duration(milliseconds: 1000), label: 'success',
        ),
      ));
178
    }, overrides: <Type, Generator>{
179
      SystemClock: () => fakeClock,
180
      Doctor: () => doctor,
181
      Usage: () => testUsage,
182 183 184
    });

    testUsingContext('doctor fail sends warning', () async {
185
      fakeClock.times = <int>[1000, 2000];
186
      doctor.diagnoseSucceeds = false;
187
      final DoctorCommand command = DoctorCommand();
188
      final CommandRunner<void> runner = createTestCommandRunner(command);
189 190 191
      await runner.run(<String>['doctor']);


192 193 194 195 196
      expect(testUsage.timings, contains(
        const TestTimingEvent(
          'flutter', 'doctor', Duration(milliseconds: 1000), label: 'warning',
        ),
      ));
197
    }, overrides: <Type, Generator>{
198
      SystemClock: () => fakeClock,
199
      Doctor: () => doctor,
200
      Usage: () => testUsage,
201
    });
202 203

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

206 207
      expect(await doctorCommand.usagePath, 'doctor');
    }, overrides: <Type, Generator>{
208
      Usage: () => testUsage,
209 210 211
    });

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

215 216
      expect(await buildApkCommand.usagePath, 'build/apk');
    }, overrides: <Type, Generator>{
217
      Usage: () => testUsage,
218
    });
219 220 221

    testUsingContext('command sends localtime', () async {
      const int kMillis = 1000;
222
      fakeClock.times = <int>[kMillis];
223 224
      // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics
      // will be written to a file.
225 226 227 228
      final Usage usage = Usage(
        versionOverride: 'test',
        runningOnBot: true,
      );
229 230 231 232 233
      usage.suppressAnalytics = false;
      usage.enabled = true;

      usage.sendCommand('test');

234
      final String log = globals.fs.file('analytics.log').readAsStringSync();
235
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
236

237
      expect(log.contains(formatDateTime(dateTime)), isTrue);
238 239
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
240
      ProcessManager: () => FakeProcessManager.any(),
241
      SystemClock: () => fakeClock,
242 243 244 245 246
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
247
      Stdio: () => fakeStdio,
248 249 250 251
    });

    testUsingContext('event sends localtime', () async {
      const int kMillis = 1000;
252
      fakeClock.times = <int>[kMillis];
253 254
      // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics
      // will be written to a file.
255 256 257 258
      final Usage usage = Usage(
        versionOverride: 'test',
        runningOnBot: true,
      );
259 260 261 262 263
      usage.suppressAnalytics = false;
      usage.enabled = true;

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

264
      final String log = globals.fs.file('analytics.log').readAsStringSync();
265
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
266

267
      expect(log.contains(formatDateTime(dateTime)), isTrue);
268 269
    }, overrides: <Type, Generator>{
      FileSystem: () => memoryFileSystem,
270
      ProcessManager: () => FakeProcessManager.any(),
271
      SystemClock: () => fakeClock,
272 273 274 275 276
      Platform: () => FakePlatform(
        environment: <String, String>{
          'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log',
        },
      ),
277
      Stdio: () => fakeStdio,
278
    });
279 280
  });

281
  group('analytics bots', () {
282 283
    Directory tempDir;

284
    setUp(() {
285
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
286 287 288 289
    });

    tearDown(() {
      tryToDelete(tempDir);
290 291
    });

292
    testUsingContext("don't send on bots with unknown version", () async {
293
      int count = 0;
294
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
295
      await createTestCommandRunner().run(<String>['--version']);
296

297
      expect(count, 0);
298
    }, overrides: <Type, Generator>{
299
      Usage: () => Usage(
300 301
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
302
        configDirOverride: tempDir.path,
303
        runningOnBot: false,
304 305 306
      ),
    });

307
    testUsingContext("don't send on bots even when opted in", () async {
308
      int count = 0;
309 310
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
      globals.flutterUsage.enabled = true;
311
      await createTestCommandRunner().run(<String>['--version']);
312

313 314
      expect(count, 0);
    }, overrides: <Type, Generator>{
315
      Usage: () => Usage(
316 317
        settingsName: 'flutter_bot_test',
        versionOverride: 'dev/unknown',
318
        configDirOverride: tempDir.path,
319
        runningOnBot: false,
320
      ),
321
    });
322 323 324 325 326 327 328 329 330 331 332 333 334 335

    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);
    });
336
  });
337
}
338

339 340 341 342 343 344 345 346 347 348
Analytics throwingAnalyticsIOFactory(
  String trackingId,
  String applicationName,
  String applicationVersion, {
  String analyticsUrl,
  Directory documentDirectory,
}) {
  throw const FileSystemException('Could not create file');
}

349 350 351 352 353 354 355 356 357 358 359 360 361
class FakeFlutterCommand extends FlutterCommand {
  @override
  String get description => 'A fake command';

  @override
  String get name => 'fake';

  @override
  Future<FlutterCommandResult> runCommand() async {
    return FlutterCommandResult.success();
  }
}

362 363 364 365 366 367 368 369 370 371 372 373 374
class FakeDoctor extends Fake implements Doctor {
  bool diagnoseSucceeds = false;

  @override
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
    AndroidLicenseValidator androidLicenseValidator,
  }) async {
    return diagnoseSucceeds;
  }
}
375

376 377 378 379 380 381 382 383
class FakeClock extends Fake implements SystemClock {
  List<int> times = <int>[];

  @override
  DateTime now() {
    return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0));
  }
}