analytics_test.dart 13.1 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/android/android_studio.dart';
8
import 'package:flutter_tools/src/android/android_workflow.dart';
9
import 'package:flutter_tools/src/base/config.dart';
10
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/base/io.dart';
12
import 'package:flutter_tools/src/base/logger.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/build_system/build_system.dart';
16
import 'package:flutter_tools/src/cache.dart';
17
import 'package:flutter_tools/src/commands/build.dart';
18 19
import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/commands/doctor.dart';
20
import 'package:flutter_tools/src/doctor.dart';
21
import 'package:flutter_tools/src/doctor_validator.dart';
22
import 'package:flutter_tools/src/features.dart';
23
import 'package:flutter_tools/src/globals.dart' as globals;
24
import 'package:flutter_tools/src/reporting/reporting.dart';
25
import 'package:flutter_tools/src/runner/flutter_command.dart';
26
import 'package:flutter_tools/src/version.dart';
27
import 'package:test/fake.dart';
28
import 'package:usage/usage_io.dart';
29

30 31
import '../src/common.dart';
import '../src/context.dart';
32
import '../src/fakes.dart';
33
import '../src/test_build_system.dart';
34
import '../src/test_flutter_command_runner.dart';
35 36

void main() {
37 38 39 40
  setUpAll(() {
    Cache.disableLocking();
  });

41
  group('analytics', () {
42 43
    late Directory tempDir;
    late Config testConfig;
44 45

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

    tearDown(() {
52
      tryToDelete(tempDir);
53 54 55
    });

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

60 61 62
      final FlutterCommand command = FakeFlutterCommand();
      final CommandRunner<void>runner = createTestCommandRunner(command);

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

67
      globals.flutterUsage.enabled = true;
68 69 70 71 72
      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);
73 74

      count = 0;
75
      globals.flutterUsage.enabled = false;
76
      await runner.run(<String>['fake']);
77

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

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

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

99
      globals.flutterUsage.enabled = true;
100
      await runner.run(<String>['config']);
101

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

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

117
      final String featuresKey = CustomDimensionsEnum.enabledFlutterFeatures.cdKey;
118

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

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

137
      final String featuresKey = CustomDimensionsEnum.enabledFlutterFeatures.cdKey;
138 139 140 141 142

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

154
  group('analytics with fakes', () {
155 156 157 158 159
    late MemoryFileSystem memoryFileSystem;
    late FakeStdio fakeStdio;
    late TestUsage testUsage;
    late FakeClock fakeClock;
    late FakeDoctor doctor;
160
    late FakeAndroidStudio androidStudio;
161 162

    setUp(() {
163
      memoryFileSystem = MemoryFileSystem.test();
164
      fakeStdio = FakeStdio();
165
      testUsage = TestUsage();
166
      fakeClock = FakeClock();
167
      doctor = FakeDoctor();
168
      androidStudio = FakeAndroidStudio();
169 170 171
    });

    testUsingContext('flutter commands send timing events', () async {
172
      fakeClock.times = <int>[1000, 2000];
173
      doctor.diagnoseSucceeds = true;
174
      final DoctorCommand command = DoctorCommand();
175
      final CommandRunner<void> runner = createTestCommandRunner(command);
176 177
      await runner.run(<String>['doctor']);

178 179 180 181 182
      expect(testUsage.timings, contains(
        const TestTimingEvent(
            'flutter', 'doctor', Duration(milliseconds: 1000), label: 'success',
        ),
      ));
183
    }, overrides: <Type, Generator>{
184
      AndroidStudio: () => androidStudio,
185
      SystemClock: () => fakeClock,
186
      Doctor: () => doctor,
187
      Usage: () => testUsage,
188 189 190
    });

    testUsingContext('doctor fail sends warning', () async {
191
      fakeClock.times = <int>[1000, 2000];
192
      doctor.diagnoseSucceeds = false;
193
      final DoctorCommand command = DoctorCommand();
194
      final CommandRunner<void> runner = createTestCommandRunner(command);
195 196 197
      await runner.run(<String>['doctor']);


198 199 200 201 202
      expect(testUsage.timings, contains(
        const TestTimingEvent(
          'flutter', 'doctor', Duration(milliseconds: 1000), label: 'warning',
        ),
      ));
203
    }, overrides: <Type, Generator>{
204
      AndroidStudio: () => androidStudio,
205
      SystemClock: () => fakeClock,
206
      Doctor: () => doctor,
207
      Usage: () => testUsage,
208
    });
209 210

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

213 214
      expect(await doctorCommand.usagePath, 'doctor');
    }, overrides: <Type, Generator>{
215
      Usage: () => testUsage,
216 217 218
    });

    testUsingContext('compound command usage path', () async {
219 220 221 222
      final BuildCommand buildCommand = BuildCommand(
        androidSdk: FakeAndroidSdk(),
        buildSystem: TestBuildSystem.all(BuildResult(success: true)),
        fileSystem: MemoryFileSystem.test(),
223
        logger: BufferLogger.test(),
224 225
        osUtils: FakeOperatingSystemUtils(),
      );
226
      final FlutterCommand buildApkCommand = buildCommand.subcommands['apk']! as FlutterCommand;
227

228 229
      expect(await buildApkCommand.usagePath, 'build/apk');
    }, overrides: <Type, Generator>{
230
      Usage: () => testUsage,
231
    });
232 233 234

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

      usage.sendCommand('test');

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

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

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

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

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

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

294
  group('analytics bots', () {
295
    late Directory tempDir;
296

297
    setUp(() {
298
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
299 300 301 302
    });

    tearDown(() {
      tryToDelete(tempDir);
303 304
    });

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

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

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

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

    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);
    });
349
  });
350
}
351

352 353 354 355
Analytics throwingAnalyticsIOFactory(
  String trackingId,
  String applicationName,
  String applicationVersion, {
356 357
  String? analyticsUrl,
  Directory? documentDirectory,
358 359 360 361
}) {
  throw const FileSystemException('Could not create file');
}

362 363 364 365 366 367 368 369 370 371 372 373 374
class FakeFlutterCommand extends FlutterCommand {
  @override
  String get description => 'A fake command';

  @override
  String get name => 'fake';

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

375 376 377 378 379 380 381 382
class FakeDoctor extends Fake implements Doctor {
  bool diagnoseSucceeds = false;

  @override
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
383
    AndroidLicenseValidator? androidLicenseValidator,
384
    bool showPii = true,
385
    List<ValidatorTask>? startedValidatorTasks,
386
    bool sendEvent = true,
387 388 389 390
  }) async {
    return diagnoseSucceeds;
  }
}
391

392 393
class FakeAndroidStudio extends Fake implements AndroidStudio {}

394 395 396 397 398 399 400 401
class FakeClock extends Fake implements SystemClock {
  List<int> times = <int>[];

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