analytics_test.dart 13.2 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
    late FileSystem fs;
    const String flutterRoot = '/path/to/flutter';
46 47

    setUp(() {
48
      Cache.flutterRoot = flutterRoot;
49
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.');
50
      testConfig = Config.test();
51
      fs = MemoryFileSystem.test();
52 53 54
    });

    tearDown(() {
55
      tryToDelete(tempDir);
56 57 58
    });

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

63 64 65
      final FlutterCommand command = FakeFlutterCommand();
      final CommandRunner<void>runner = createTestCommandRunner(command);

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

70
      globals.flutterUsage.enabled = true;
71 72 73 74 75
      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);
76 77

      count = 0;
78
      globals.flutterUsage.enabled = false;
79
      await runner.run(<String>['fake']);
80

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

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

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

102
      globals.flutterUsage.enabled = true;
103
      await runner.run(<String>['config']);
104

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

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

120
      final String featuresKey = CustomDimensionsEnum.enabledFlutterFeatures.cdKey;
121

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

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

140
      final String featuresKey = CustomDimensionsEnum.enabledFlutterFeatures.cdKey;
141 142 143 144 145

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

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

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

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

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

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


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

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

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

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

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

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

      usage.sendCommand('test');

250
      final String log = globals.fs.file('analytics.log').readAsStringSync();
251
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
252

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

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

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

280
      final String log = globals.fs.file('analytics.log').readAsStringSync();
281
      final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis);
282

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

297
  group('analytics bots', () {
298
    late Directory tempDir;
299

300
    setUp(() {
301
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.');
302 303 304 305
    });

    tearDown(() {
      tryToDelete(tempDir);
306 307
    });

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

313
      expect(count, 0);
314
    }, 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
    testUsingContext("don't send on bots even when opted in", () async {
324
      int count = 0;
325 326
      globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++);
      globals.flutterUsage.enabled = true;
327
      await createTestCommandRunner().run(<String>['--version']);
328

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

    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);
    });
352
  });
353
}
354

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

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

  @override
  String get name => 'fake';

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

378 379 380 381 382 383 384 385
class FakeDoctor extends Fake implements Doctor {
  bool diagnoseSucceeds = false;

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

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

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