flutter/packages/flutter_tools/test/general.shard/analytics_test.dart
Christopher Fujino 3246808cd2
[flutter_tools] cache flutter sdk version to disk (#124558)
Fixes https://github.com/flutter/flutter/issues/112833

Most of the actual changes here are in [packages/flutter_tools/lib/src/version.dart](https://github.com/flutter/flutter/pull/124558/files#diff-092e00109d9e1589fbc7c6de750e29a6ae512b2dd44e85d60028953561201605), while the rest is largely just addressing changes to the constructor of `FlutterVersion` which now has different dependencies.

This change makes `FlutterVersion` an interface with two concrete implementations:

1. `_FlutterVersionGit` which is mostly the previous implementation, and
2. `_FlutterVersionFromFile` which will read a new `.version.json` file from the root of the repo

The [`FlutterVersion` constructor](https://github.com/flutter/flutter/pull/124558/files#diff-092e00109d9e1589fbc7c6de750e29a6ae512b2dd44e85d60028953561201605R70) is now a factory that first checks if `.version.json` exists, and if so returns an instance of `_FlutterVersionFromGit` else it returns the fallback `_FlutterVersionGit` which will end up writing `.version.json` so that we don't need to re-calculate the version on the next invocation.

`.version.json` will be deleted in the bash/batch entrypoints any time we need to rebuild he tool (this will usually be because the user did `flutter upgrade` or `flutter channel`, or manually changed the commit with git).
2023-06-15 00:20:30 +00:00

404 lines
13 KiB
Dart

// 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));
}
}