| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // 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'; | |
| import 'package:file/memory.dart'; | |
| import 'package:flutter_tools/src/android/android_studio.dart'; | |
| import 'package:flutter_tools/src/android/android_workflow.dart'; | |
| import 'package:flutter_tools/src/base/config.dart'; | |
| import 'package:flutter_tools/src/base/file_system.dart'; | |
| import 'package:flutter_tools/src/base/io.dart'; | |
| import 'package:flutter_tools/src/base/logger.dart'; | |
| import 'package:flutter_tools/src/base/platform.dart'; | |
| import 'package:flutter_tools/src/base/time.dart'; | |
| import 'package:flutter_tools/src/build_system/build_system.dart'; | |
| import 'package:flutter_tools/src/cache.dart'; | |
| import 'package:flutter_tools/src/commands/build.dart'; | |
| import 'package:flutter_tools/src/commands/config.dart'; | |
| import 'package:flutter_tools/src/commands/doctor.dart'; | |
| import 'package:flutter_tools/src/doctor.dart'; | |
| import 'package:flutter_tools/src/doctor_validator.dart'; | |
| import 'package:flutter_tools/src/features.dart'; | |
| import 'package:flutter_tools/src/globals.dart' as globals; | |
| import 'package:flutter_tools/src/reporting/reporting.dart'; | |
| import 'package:flutter_tools/src/runner/flutter_command.dart'; | |
| import 'package:flutter_tools/src/version.dart'; | |
| import 'package:test/fake.dart'; | |
| import 'package:usage/usage_io.dart'; | |
| import '../src/common.dart'; | |
| import '../src/context.dart'; | |
| import '../src/fakes.dart'; | |
| import '../src/test_build_system.dart'; | |
| import '../src/test_flutter_command_runner.dart'; | |
| void main() { | |
| setUpAll(() { | |
| Cache.disableLocking(); | |
| }); | |
| group('analytics', () { | |
| late Directory tempDir; | |
| late Config testConfig; | |
| late FileSystem fs; | |
| const String flutterRoot = '/path/to/flutter'; | |
| setUp(() { | |
| Cache.flutterRoot = flutterRoot; | |
| tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.'); | |
| testConfig = Config.test(); | |
| fs = MemoryFileSystem.test(); | |
| }); | |
| tearDown(() { | |
| tryToDelete(tempDir); | |
| }); | |
| // Ensure we don't send anything when analytics is disabled. | |
| testUsingContext("doesn't send when disabled", () async { | |
| int count = 0; | |
| globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++); | |
| final FlutterCommand command = FakeFlutterCommand(); | |
| final CommandRunner<void>runner = createTestCommandRunner(command); | |
| globals.flutterUsage.enabled = false; | |
| await runner.run(<String>['fake']); | |
| expect(count, 0); | |
| globals.flutterUsage.enabled = true; | |
| 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); | |
| count = 0; | |
| globals.flutterUsage.enabled = false; | |
| await runner.run(<String>['fake']); | |
| expect(count, 0); | |
| }, overrides: <Type, Generator>{ | |
| FlutterVersion: () => FakeFlutterVersion(), | |
| Usage: () => Usage( | |
| configDirOverride: tempDir.path, | |
| logFile: tempDir.childFile('analytics.log').path, | |
| runningOnBot: true, | |
| ), | |
| }); | |
| // Ensure we don't send for the 'flutter config' command. | |
| testUsingContext("config doesn't send", () async { | |
| int count = 0; | |
| globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++); | |
| globals.flutterUsage.enabled = false; | |
| final ConfigCommand command = ConfigCommand(); | |
| final CommandRunner<void> runner = createTestCommandRunner(command); | |
| await runner.run(<String>['config']); | |
| expect(count, 0); | |
| globals.flutterUsage.enabled = true; | |
| await runner.run(<String>['config']); | |
| expect(count, 0); | |
| }, overrides: <Type, Generator>{ | |
| FlutterVersion: () => FakeFlutterVersion(), | |
| Usage: () => Usage( | |
| configDirOverride: tempDir.path, | |
| logFile: tempDir.childFile('analytics.log').path, | |
| runningOnBot: true, | |
| ), | |
| }); | |
| testUsingContext('Usage records one feature in experiment setting', () async { | |
| testConfig.setValue(flutterWebFeature.configSetting!, true); | |
| final Usage usage = Usage(runningOnBot: true); | |
| usage.sendCommand('test'); | |
| final String featuresKey = CustomDimensionsEnum.enabledFlutterFeatures.cdKey; | |
| expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web')); | |
| }, overrides: <Type, Generator>{ | |
| FlutterVersion: () => FakeFlutterVersion(), | |
| Config: () => testConfig, | |
| Platform: () => FakePlatform(environment: <String, String>{ | |
| 'FLUTTER_ANALYTICS_LOG_FILE': 'test', | |
| }), | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| }); | |
| testUsingContext('Usage records multiple features in experiment setting', () async { | |
| testConfig.setValue(flutterWebFeature.configSetting!, true); | |
| testConfig.setValue(flutterLinuxDesktopFeature.configSetting!, true); | |
| testConfig.setValue(flutterMacOSDesktopFeature.configSetting!, true); | |
| final Usage usage = Usage(runningOnBot: true); | |
| usage.sendCommand('test'); | |
| final String featuresKey = CustomDimensionsEnum.enabledFlutterFeatures.cdKey; | |
| expect( | |
| globals.fs.file('test').readAsStringSync(), | |
| contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'), | |
| ); | |
| }, overrides: <Type, Generator>{ | |
| FlutterVersion: () => FakeFlutterVersion(), | |
| Config: () => testConfig, | |
| Platform: () => FakePlatform(environment: <String, String>{ | |
| 'FLUTTER_ANALYTICS_LOG_FILE': 'test', | |
| }), | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| }); | |
| }); | |
| group('analytics with fakes', () { | |
| late MemoryFileSystem memoryFileSystem; | |
| late FakeStdio fakeStdio; | |
| late TestUsage testUsage; | |
| late FakeClock fakeClock; | |
| late FakeDoctor doctor; | |
| late FakeAndroidStudio androidStudio; | |
| setUp(() { | |
| memoryFileSystem = MemoryFileSystem.test(); | |
| fakeStdio = FakeStdio(); | |
| testUsage = TestUsage(); | |
| fakeClock = FakeClock(); | |
| doctor = FakeDoctor(); | |
| androidStudio = FakeAndroidStudio(); | |
| }); | |
| testUsingContext('flutter commands send timing events', () async { | |
| fakeClock.times = <int>[1000, 2000]; | |
| doctor.diagnoseSucceeds = true; | |
| final DoctorCommand command = DoctorCommand(); | |
| final CommandRunner<void> runner = createTestCommandRunner(command); | |
| await runner.run(<String>['doctor']); | |
| expect(testUsage.timings, contains( | |
| const TestTimingEvent( | |
| 'flutter', 'doctor', Duration(milliseconds: 1000), label: 'success', | |
| ), | |
| )); | |
| }, overrides: <Type, Generator>{ | |
| AndroidStudio: () => androidStudio, | |
| SystemClock: () => fakeClock, | |
| Doctor: () => doctor, | |
| Usage: () => testUsage, | |
| }); | |
| testUsingContext('doctor fail sends warning', () async { | |
| fakeClock.times = <int>[1000, 2000]; | |
| doctor.diagnoseSucceeds = false; | |
| final DoctorCommand command = DoctorCommand(); | |
| final CommandRunner<void> runner = createTestCommandRunner(command); | |
| await runner.run(<String>['doctor']); | |
| expect(testUsage.timings, contains( | |
| const TestTimingEvent( | |
| 'flutter', 'doctor', Duration(milliseconds: 1000), label: 'warning', | |
| ), | |
| )); | |
| }, overrides: <Type, Generator>{ | |
| AndroidStudio: () => androidStudio, | |
| SystemClock: () => fakeClock, | |
| Doctor: () => doctor, | |
| Usage: () => testUsage, | |
| }); | |
| testUsingContext('single command usage path', () async { | |
| final FlutterCommand doctorCommand = DoctorCommand(); | |
| expect(await doctorCommand.usagePath, 'doctor'); | |
| }, overrides: <Type, Generator>{ | |
| Usage: () => testUsage, | |
| }); | |
| testUsingContext('compound command usage path', () async { | |
| final BuildCommand buildCommand = BuildCommand( | |
| androidSdk: FakeAndroidSdk(), | |
| buildSystem: TestBuildSystem.all(BuildResult(success: true)), | |
| fileSystem: MemoryFileSystem.test(), | |
| logger: BufferLogger.test(), | |
| osUtils: FakeOperatingSystemUtils(), | |
| ); | |
| final FlutterCommand buildApkCommand = buildCommand.subcommands['apk']! as FlutterCommand; | |
| expect(await buildApkCommand.usagePath, 'build/apk'); | |
| }, overrides: <Type, Generator>{ | |
| Usage: () => testUsage, | |
| }); | |
| testUsingContext('command sends localtime', () async { | |
| const int kMillis = 1000; | |
| fakeClock.times = <int>[kMillis]; | |
| // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics | |
| // will be written to a file. | |
| final Usage usage = Usage( | |
| versionOverride: 'test', | |
| runningOnBot: true, | |
| ); | |
| usage.suppressAnalytics = false; | |
| usage.enabled = true; | |
| usage.sendCommand('test'); | |
| final String log = globals.fs.file('analytics.log').readAsStringSync(); | |
| final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis); | |
| expect(log.contains(formatDateTime(dateTime)), isTrue); | |
| }, overrides: <Type, Generator>{ | |
| FileSystem: () => memoryFileSystem, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => fakeClock, | |
| Platform: () => FakePlatform( | |
| environment: <String, String>{ | |
| 'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log', | |
| }, | |
| ), | |
| Stdio: () => fakeStdio, | |
| }); | |
| testUsingContext('event sends localtime', () async { | |
| const int kMillis = 1000; | |
| fakeClock.times = <int>[kMillis]; | |
| // Since FLUTTER_ANALYTICS_LOG_FILE is set in the environment, analytics | |
| // will be written to a file. | |
| final Usage usage = Usage( | |
| versionOverride: 'test', | |
| runningOnBot: true, | |
| ); | |
| usage.suppressAnalytics = false; | |
| usage.enabled = true; | |
| usage.sendEvent('test', 'test'); | |
| final String log = globals.fs.file('analytics.log').readAsStringSync(); | |
| final DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(kMillis); | |
| expect(log.contains(formatDateTime(dateTime)), isTrue); | |
| }, overrides: <Type, Generator>{ | |
| FileSystem: () => memoryFileSystem, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => fakeClock, | |
| Platform: () => FakePlatform( | |
| environment: <String, String>{ | |
| 'FLUTTER_ANALYTICS_LOG_FILE': 'analytics.log', | |
| }, | |
| ), | |
| Stdio: () => fakeStdio, | |
| }); | |
| }); | |
| group('analytics bots', () { | |
| late Directory tempDir; | |
| setUp(() { | |
| tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_bots_test.'); | |
| }); | |
| tearDown(() { | |
| tryToDelete(tempDir); | |
| }); | |
| testUsingContext("don't send on bots with unknown version", () async { | |
| int count = 0; | |
| globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++); | |
| await createTestCommandRunner().run(<String>['--version']); | |
| expect(count, 0); | |
| }, overrides: <Type, Generator>{ | |
| Usage: () => Usage( | |
| settingsName: 'flutter_bot_test', | |
| versionOverride: 'dev/unknown', | |
| configDirOverride: tempDir.path, | |
| runningOnBot: false, | |
| ), | |
| }); | |
| testUsingContext("don't send on bots even when opted in", () async { | |
| int count = 0; | |
| globals.flutterUsage.onSend.listen((Map<String, dynamic> data) => count++); | |
| globals.flutterUsage.enabled = true; | |
| await createTestCommandRunner().run(<String>['--version']); | |
| expect(count, 0); | |
| }, overrides: <Type, Generator>{ | |
| Usage: () => Usage( | |
| settingsName: 'flutter_bot_test', | |
| versionOverride: 'dev/unknown', | |
| configDirOverride: tempDir.path, | |
| runningOnBot: false, | |
| ), | |
| }); | |
| 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); | |
| }); | |
| }); | |
| } | |
| Analytics throwingAnalyticsIOFactory( | |
| String trackingId, | |
| String applicationName, | |
| String applicationVersion, { | |
| String? analyticsUrl, | |
| Directory? documentDirectory, | |
| }) { | |
| throw const FileSystemException('Could not create file'); | |
| } | |
| class FakeFlutterCommand extends FlutterCommand { | |
| @override | |
| String get description => 'A fake command'; | |
| @override | |
| String get name => 'fake'; | |
| @override | |
| Future<FlutterCommandResult> runCommand() async { | |
| return FlutterCommandResult.success(); | |
| } | |
| } | |
| class FakeDoctor extends Fake implements Doctor { | |
| bool diagnoseSucceeds = false; | |
| @override | |
| Future<bool> diagnose({ | |
| bool androidLicenses = false, | |
| bool verbose = true, | |
| bool showColor = true, | |
| AndroidLicenseValidator? androidLicenseValidator, | |
| bool showPii = true, | |
| List<ValidatorTask>? startedValidatorTasks, | |
| bool sendEvent = true, | |
| FlutterVersion? version, | |
| }) async { | |
| return diagnoseSucceeds; | |
| } | |
| } | |
| class FakeClock extends Fake implements SystemClock { | |
| List<int> times = <int>[]; | |
| @override | |
| DateTime now() { | |
| return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0)); | |
| } | |
| } |