Jonah Williams 8b6baae44c
[flutter_tools] move process manager into tool (#75350)
Our current top crasher is an unclear error when ProcessManager fails to resolve an executable path. To fix this, we'd like to being adjusting the process resolution logic and adding more instrumentation to track failures. In order to begin the process, the ProcessManager has been folded back into the flutter tool
2021-02-04 13:19:11 -08:00

378 lines
12 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.
// @dart = 2.8
import 'package:args/command_runner.dart';
import 'package:file/memory.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/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/time.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/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:mockito/mockito.dart';
import 'package:usage/usage_io.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
void main() {
setUpAll(() {
Cache.disableLocking();
});
group('analytics', () {
Directory tempDir;
Config testConfig;
setUp(() {
Cache.flutterRoot = '../..';
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_analytics_test.');
testConfig = Config.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: () => FlutterVersion(clock: const SystemClock()),
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: () => FlutterVersion(clock: const SystemClock()),
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 = cdKey(CustomDimensions.enabledFlutterFeatures);
expect(globals.fs.file('test').readAsStringSync(), contains('$featuresKey: enable-web'));
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(clock: const SystemClock()),
Config: () => testConfig,
Platform: () => FakePlatform(environment: <String, String>{
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
}),
FileSystem: () => MemoryFileSystem.test(),
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 = cdKey(CustomDimensions.enabledFlutterFeatures);
expect(
globals.fs.file('test').readAsStringSync(),
contains('$featuresKey: enable-web,enable-linux-desktop,enable-macos-desktop'),
);
}, overrides: <Type, Generator>{
FlutterVersion: () => FlutterVersion(clock: const SystemClock()),
Config: () => testConfig,
Platform: () => FakePlatform(environment: <String, String>{
'FLUTTER_ANALYTICS_LOG_FILE': 'test',
}),
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
});
});
group('analytics with mocks', () {
MemoryFileSystem memoryFileSystem;
FakeStdio fakeStdio;
TestUsage testUsage;
FakeClock fakeClock;
Doctor mockDoctor;
setUp(() {
memoryFileSystem = MemoryFileSystem.test();
fakeStdio = FakeStdio();
testUsage = TestUsage();
fakeClock = FakeClock();
mockDoctor = MockDoctor();
});
testUsingContext('flutter commands send timing events', () async {
fakeClock.times = <int>[1000, 2000];
when(mockDoctor.diagnose(
androidLicenses: false,
verbose: false,
androidLicenseValidator: anyNamed('androidLicenseValidator')
)).thenAnswer((_) async => 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>{
SystemClock: () => fakeClock,
Doctor: () => mockDoctor,
Usage: () => testUsage,
});
testUsingContext('doctor fail sends warning', () async {
fakeClock.times = <int>[1000, 2000];
when(mockDoctor.diagnose(androidLicenses: false, verbose: false, androidLicenseValidator: anyNamed('androidLicenseValidator')))
.thenAnswer((_) async => 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>{
SystemClock: () => fakeClock,
Doctor: () => mockDoctor,
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();
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', () {
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 MockDoctor extends Mock implements Doctor {}
class FakeClock extends Fake implements SystemClock {
List<int> times = <int>[];
@override
DateTime now() {
return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0));
}
}