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/doctor_validator.dart';
21
import 'package:flutter_tools/src/features.dart';
22
import 'package:flutter_tools/src/globals.dart' as globals;
23
import 'package:flutter_tools/src/reporting/reporting.dart';
24
import 'package:flutter_tools/src/runner/flutter_command.dart';
25
import 'package:flutter_tools/src/version.dart';
26
import 'package:test/fake.dart';
27
import 'package:usage/usage_io.dart';
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

      usage.sendCommand('test');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  @override
  String get name => 'fake';

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

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

  @override
  Future<bool> diagnose({
    bool androidLicenses = false,
    bool verbose = true,
    bool showColor = true,
    AndroidLicenseValidator androidLicenseValidator,
372 373 374
    bool showPii = true,
    List<ValidatorTask> startedValidatorTasks,
    bool sendEvent = true,
375 376 377 378
  }) async {
    return diagnoseSucceeds;
  }
}
379

380 381 382 383 384 385 386 387
class FakeClock extends Fake implements SystemClock {
  List<int> times = <int>[];

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