Unverified Commit 372fe290 authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tool] Update analytics policy, send event on disable (#43217)

parent f6eb1295
...@@ -215,9 +215,8 @@ Future<String> _doctorText() async { ...@@ -215,9 +215,8 @@ Future<String> _doctorText() async {
} }
Future<int> _exit(int code) async { Future<int> _exit(int code) async {
if (flutterUsage.isFirstRun) { // Prints the welcome message if needed.
flutterUsage.printWelcome(); flutterUsage.printWelcome();
}
// Send any last analytics calls that are in progress without overly delaying // Send any last analytics calls that are in progress without overly delaying
// the tool's exit (we wait a maximum of 250ms). // the tool's exit (we wait a maximum of 250ms).
......
...@@ -5,11 +5,10 @@ ...@@ -5,11 +5,10 @@
import '../convert.dart'; import '../convert.dart';
import 'context.dart'; import 'context.dart';
import 'file_system.dart'; import 'file_system.dart';
import 'platform.dart';
class Config { class Config {
Config([File configFile]) { Config([File configFile]) {
_configFile = configFile ?? fs.file(fs.path.join(_userHomeDir(), '.flutter_settings')); _configFile = configFile ?? fs.file(fs.path.join(userHomePath(), '.flutter_settings'));
if (_configFile.existsSync()) { if (_configFile.existsSync()) {
_values = json.decode(_configFile.readAsStringSync()); _values = json.decode(_configFile.readAsStringSync());
} }
...@@ -44,8 +43,3 @@ class Config { ...@@ -44,8 +43,3 @@ class Config {
_configFile.writeAsStringSync(json); _configFile.writeAsStringSync(json);
} }
} }
String _userHomeDir() {
final String envKey = platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
return platform.environment[envKey] ?? '.';
}
...@@ -185,3 +185,11 @@ class FileNotFoundException implements IOException { ...@@ -185,3 +185,11 @@ class FileNotFoundException implements IOException {
@override @override
String toString() => 'File not found: $path'; String toString() => 'File not found: $path';
} }
/// Reads the process environment to find the current user's home directory.
///
/// If the searched environment variables are not set, '.' is returned instead.
String userHomePath() {
final String envKey = platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
return platform.environment[envKey] ?? '.';
}
...@@ -117,6 +117,7 @@ class ConfigCommand extends FlutterCommand { ...@@ -117,6 +117,7 @@ class ConfigCommand extends FlutterCommand {
if (argResults.wasParsed('analytics')) { if (argResults.wasParsed('analytics')) {
final bool value = argResults['analytics']; final bool value = argResults['analytics'];
flutterUsage.enabled = value; flutterUsage.enabled = value;
AnalyticsConfigEvent(enabled: value).send();
printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.'); printStatus('Analytics reporting ${value ? 'enabled' : 'disabled'}.');
} }
......
...@@ -15,12 +15,14 @@ import '../base/process.dart'; ...@@ -15,12 +15,14 @@ import '../base/process.dart';
import '../cache.dart'; import '../cache.dart';
import '../dart/pub.dart'; import '../dart/pub.dart';
import '../globals.dart'; import '../globals.dart';
import '../persistent_tool_state.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../version.dart'; import '../version.dart';
import 'channel.dart'; import 'channel.dart';
class UpgradeCommand extends FlutterCommand { class UpgradeCommand extends FlutterCommand {
UpgradeCommand() { UpgradeCommand([UpgradeCommandRunner commandRunner])
: _commandRunner = commandRunner ?? UpgradeCommandRunner() {
argParser argParser
..addFlag( ..addFlag(
'force', 'force',
...@@ -32,10 +34,14 @@ class UpgradeCommand extends FlutterCommand { ...@@ -32,10 +34,14 @@ class UpgradeCommand extends FlutterCommand {
'continue', 'continue',
hide: true, hide: true,
negatable: false, negatable: false,
help: 'For the second half of the upgrade flow requiring the new version of Flutter. Should not be invoked manually, but re-entrantly by the standard upgrade command.', help: 'For the second half of the upgrade flow requiring the new '
'version of Flutter. Should not be invoked manually, but '
're-entrantly by the standard upgrade command.',
); );
} }
final UpgradeCommandRunner _commandRunner;
@override @override
final String name = 'upgrade'; final String name = 'upgrade';
...@@ -52,8 +58,7 @@ class UpgradeCommand extends FlutterCommand { ...@@ -52,8 +58,7 @@ class UpgradeCommand extends FlutterCommand {
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
final UpgradeCommandRunner upgradeCommandRunner = UpgradeCommandRunner(); await _commandRunner.runCommand(
await upgradeCommandRunner.runCommand(
argResults['force'], argResults['force'],
argResults['continue'], argResults['continue'],
GitTagVersion.determine(), GitTagVersion.determine(),
...@@ -141,9 +146,13 @@ class UpgradeCommandRunner { ...@@ -141,9 +146,13 @@ class UpgradeCommandRunner {
// This method should only be called if the upgrade command is invoked // This method should only be called if the upgrade command is invoked
// re-entrantly with the `--continue` flag // re-entrantly with the `--continue` flag
Future<void> runCommandSecondHalf(FlutterVersion flutterVersion) async { Future<void> runCommandSecondHalf(FlutterVersion flutterVersion) async {
// Make sure the welcome message re-display is delayed until the end.
persistentToolState.redisplayWelcomeMessage = false;
await precacheArtifacts(); await precacheArtifacts();
await updatePackages(flutterVersion); await updatePackages(flutterVersion);
await runDoctor(); await runDoctor();
// Force the welcome message to re-display following the upgrade.
persistentToolState.redisplayWelcomeMessage = true;
} }
Future<bool> hasUncomittedChanges() async { Future<bool> hasUncomittedChanges() async {
......
...@@ -48,6 +48,7 @@ import 'macos/macos_workflow.dart'; ...@@ -48,6 +48,7 @@ import 'macos/macos_workflow.dart';
import 'macos/xcode.dart'; import 'macos/xcode.dart';
import 'macos/xcode_validator.dart'; import 'macos/xcode_validator.dart';
import 'mdns_discovery.dart'; import 'mdns_discovery.dart';
import 'persistent_tool_state.dart';
import 'reporting/reporting.dart'; import 'reporting/reporting.dart';
import 'run_hot.dart'; import 'run_hot.dart';
import 'version.dart'; import 'version.dart';
...@@ -106,9 +107,10 @@ Future<T> runInContext<T>( ...@@ -106,9 +107,10 @@ Future<T> runInContext<T>(
MacOSWorkflow: () => const MacOSWorkflow(), MacOSWorkflow: () => const MacOSWorkflow(),
MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(), MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery(),
OperatingSystemUtils: () => OperatingSystemUtils(), OperatingSystemUtils: () => OperatingSystemUtils(),
Pub: () => const Pub(), PersistentToolState: () => PersistentToolState(),
ProcessInfo: () => ProcessInfo(), ProcessInfo: () => ProcessInfo(),
ProcessUtils: () => ProcessUtils(), ProcessUtils: () => ProcessUtils(),
Pub: () => const Pub(),
Signals: () => Signals(), Signals: () => Signals(),
SimControl: () => SimControl(), SimControl: () => SimControl(),
Stdio: () => const Stdio(), Stdio: () => const Stdio(),
...@@ -121,8 +123,8 @@ Future<T> runInContext<T>( ...@@ -121,8 +123,8 @@ Future<T> runInContext<T>(
WebWorkflow: () => const WebWorkflow(), WebWorkflow: () => const WebWorkflow(),
WindowsWorkflow: () => const WindowsWorkflow(), WindowsWorkflow: () => const WindowsWorkflow(),
Xcode: () => Xcode(), Xcode: () => Xcode(),
XcodeValidator: () => const XcodeValidator(),
XcodeProjectInterpreter: () => XcodeProjectInterpreter(), XcodeProjectInterpreter: () => XcodeProjectInterpreter(),
XcodeValidator: () => const XcodeValidator(),
}, },
); );
} }
// Copyright 2019 The Chromium 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 'base/config.dart';
import 'base/context.dart';
import 'base/file_system.dart';
PersistentToolState get persistentToolState => PersistentToolState.instance;
/// A class that represents global (non-project-specific) internal state that
/// must persist across tool invocations.
abstract class PersistentToolState {
factory PersistentToolState([File configFile]) =>
_DefaultPersistentToolState(configFile);
static PersistentToolState get instance => context.get<PersistentToolState>();
/// Whether the welcome message should be redisplayed.
///
/// May give null if the value has not been set.
bool redisplayWelcomeMessage;
}
class _DefaultPersistentToolState implements PersistentToolState {
_DefaultPersistentToolState([File configFile]) :
_config = Config(configFile ?? fs.file(fs.path.join(userHomePath(), _kFileName)));
static const String _kFileName = '.flutter_tool_state';
static const String _kRedisplayWelcomeMessage = 'redisplay-welcome-message';
final Config _config;
@override
bool get redisplayWelcomeMessage => _config.getValue(_kRedisplayWelcomeMessage);
@override
set redisplayWelcomeMessage(bool value) {
_config.setValue(_kRedisplayWelcomeMessage, value);
}
}
...@@ -199,3 +199,15 @@ class CommandResultEvent extends UsageEvent { ...@@ -199,3 +199,15 @@ class CommandResultEvent extends UsageEvent {
} }
} }
} }
/// An event that reports on changes in the configuration of analytics.
class AnalyticsConfigEvent extends UsageEvent {
AnalyticsConfigEvent({
/// Whether analytics reporting is being enabled (true) or disabled (false).
@required bool enabled,
}) : super(
'analytics',
'enabled',
label: enabled ? 'true' : 'false',
);
}
...@@ -21,6 +21,7 @@ import '../base/utils.dart'; ...@@ -21,6 +21,7 @@ import '../base/utils.dart';
import '../doctor.dart'; import '../doctor.dart';
import '../features.dart'; import '../features.dart';
import '../globals.dart'; import '../globals.dart';
import '../persistent_tool_state.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../version.dart'; import '../version.dart';
......
...@@ -323,35 +323,55 @@ class _DefaultUsage implements Usage { ...@@ -323,35 +323,55 @@ class _DefaultUsage implements Usage {
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250)); await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
} }
@override void _printWelcome() {
void printWelcome() {
// This gets called if it's the first run by the selected command, if any,
// and on exit, in case there was no command.
if (_printedWelcome) {
return;
}
_printedWelcome = true;
printStatus(''); printStatus('');
printStatus(''' printStatus('''
╔════════════════════════════════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════════════════════════════════╗
║ Welcome to Flutter! - https://flutter.dev ║ ║ Welcome to Flutter! - https://flutter.dev ║
║ ║ ║ ║
║ The Flutter tool anonymously reports feature usage statistics and crash ║ ║ The Flutter tool uses Google Analytics to anonymously report feature usage ║
║ reports to Google in order to help Google contribute improvements to ║ ║ statistics and basic crash reports. This data is used to help improve ║
║ Flutter over time. ║ ║ Flutter tools over time. ║
║ ║
║ Flutter tool analytics are not sent on the very first run. To disable ║
║ reporting, type 'flutter config --no-analytics'. To display the current ║
║ setting, type 'flutter config'. If you opt out of analytics, an opt-out ║
║ event will be sent, and then no further information will be sent by the ║
║ Flutter tool. ║
║ ║
║ By downloading the Flutter SDK, you agree to the Google Terms of Service. ║
║ Note: The Google Privacy Policy describes how data is handled in this ║
║ service. ║
║ ║
║ Moreover, Flutter includes the Dart SDK, which may send usage metrics and ║
║ crash reports to Google. ║
║ ║ ║ ║
║ Read about data we send with crash reports: ║ ║ Read about data we send with crash reports: ║
║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting ║ ║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting ║
║ ║ ║ ║
║ See Google's privacy policy: ║ See Google's privacy policy:
https://www.google.com/intl/en/policies/privacy/ ║ https://www.google.com/intl/en/policies/privacy/ ║
Use "flutter config --no-analytics" to disable analytics and crash
reporting.
╚════════════════════════════════════════════════════════════════════════════╝ ╚════════════════════════════════════════════════════════════════════════════╝
''', emphasis: true); ''', emphasis: true);
} }
@override
void printWelcome() {
// Only print once per run.
if (_printedWelcome) {
return;
}
if (// Display the welcome message if this is the first run of the tool.
isFirstRun ||
// Display the welcome message if we are not on master, and if the
// persistent tool state instructs that we should.
(!FlutterVersion.instance.isMaster &&
(persistentToolState.redisplayWelcomeMessage ?? true))) {
_printWelcome();
_printedWelcome = true;
persistentToolState.redisplayWelcomeMessage = false;
}
}
} }
// An Analytics mock that logs to file. Unimplemented methods goes to stdout. // An Analytics mock that logs to file. Unimplemented methods goes to stdout.
......
...@@ -447,9 +447,8 @@ abstract class FlutterCommand extends Command<void> { ...@@ -447,9 +447,8 @@ abstract class FlutterCommand extends Command<void> {
name: 'command', name: 'command',
overrides: <Type, Generator>{FlutterCommand: () => this}, overrides: <Type, Generator>{FlutterCommand: () => this},
body: () async { body: () async {
if (flutterUsage.isFirstRun) { // Prints the welcome message if needed.
flutterUsage.printWelcome(); flutterUsage.printWelcome();
}
final String commandPath = await usagePath; final String commandPath = await usagePath;
_registerSignalHandlers(commandPath, startTime); _registerSignalHandlers(commandPath, startTime);
FlutterCommandResult commandResult; FlutterCommandResult commandResult;
......
...@@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/logger.dart'; ...@@ -14,6 +14,7 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/config.dart'; import 'package:flutter_tools/src/commands/config.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -24,6 +25,7 @@ void main() { ...@@ -24,6 +25,7 @@ void main() {
MockAndroidStudio mockAndroidStudio; MockAndroidStudio mockAndroidStudio;
MockAndroidSdk mockAndroidSdk; MockAndroidSdk mockAndroidSdk;
MockFlutterVersion mockFlutterVersion; MockFlutterVersion mockFlutterVersion;
MockUsage mockUsage;
setUpAll(() { setUpAll(() {
Cache.disableLocking(); Cache.disableLocking();
...@@ -33,8 +35,31 @@ void main() { ...@@ -33,8 +35,31 @@ void main() {
mockAndroidStudio = MockAndroidStudio(); mockAndroidStudio = MockAndroidStudio();
mockAndroidSdk = MockAndroidSdk(); mockAndroidSdk = MockAndroidSdk();
mockFlutterVersion = MockFlutterVersion(); mockFlutterVersion = MockFlutterVersion();
mockUsage = MockUsage();
when(mockUsage.isFirstRun).thenReturn(false);
}); });
void verifyNoAnalytics() {
verifyNever(mockUsage.sendCommand(
any,
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendEvent(
any,
any,
label: anyNamed('label'),
value: anyNamed('value'),
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendTiming(
any,
any,
any,
label: anyNamed('label'),
));
}
group('config', () { group('config', () {
testUsingContext('machine flag', () async { testUsingContext('machine flag', () async {
final BufferLogger logger = context.get<Logger>(); final BufferLogger logger = context.get<Logger>();
...@@ -50,9 +75,11 @@ void main() { ...@@ -50,9 +75,11 @@ void main() {
expect(jsonObject.containsKey('android-sdk'), true); expect(jsonObject.containsKey('android-sdk'), true);
expect(jsonObject['android-sdk'], isNotNull); expect(jsonObject['android-sdk'], isNotNull);
verifyNoAnalytics();
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
AndroidStudio: () => mockAndroidStudio, AndroidStudio: () => mockAndroidStudio,
AndroidSdk: () => mockAndroidSdk, AndroidSdk: () => mockAndroidSdk,
Usage: () => mockUsage,
}); });
testUsingContext('Can set build-dir', () async { testUsingContext('Can set build-dir', () async {
...@@ -65,6 +92,9 @@ void main() { ...@@ -65,6 +92,9 @@ void main() {
]); ]);
expect(getBuildDirectory(), 'foo'); expect(getBuildDirectory(), 'foo');
verifyNoAnalytics();
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
}); });
testUsingContext('throws error on absolute path to build-dir', () async { testUsingContext('throws error on absolute path to build-dir', () async {
...@@ -75,6 +105,9 @@ void main() { ...@@ -75,6 +105,9 @@ void main() {
'config', 'config',
'--build-dir=/foo', '--build-dir=/foo',
]), throwsA(isInstanceOf<ToolExit>())); ]), throwsA(isInstanceOf<ToolExit>()));
verifyNoAnalytics();
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
}); });
testUsingContext('allows setting and removing feature flags', () async { testUsingContext('allows setting and removing feature flags', () async {
...@@ -115,9 +148,11 @@ void main() { ...@@ -115,9 +148,11 @@ void main() {
expect(Config.instance.getValue('enable-linux-desktop'), false); expect(Config.instance.getValue('enable-linux-desktop'), false);
expect(Config.instance.getValue('enable-windows-desktop'), false); expect(Config.instance.getValue('enable-windows-desktop'), false);
expect(Config.instance.getValue('enable-macos-desktop'), false); expect(Config.instance.getValue('enable-macos-desktop'), false);
verifyNoAnalytics();
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
AndroidStudio: () => mockAndroidStudio, AndroidStudio: () => mockAndroidStudio,
AndroidSdk: () => mockAndroidSdk, AndroidSdk: () => mockAndroidSdk,
Usage: () => mockUsage,
}); });
testUsingContext('displays which config settings are available on stable', () async { testUsingContext('displays which config settings are available on stable', () async {
...@@ -142,10 +177,86 @@ void main() { ...@@ -142,10 +177,86 @@ void main() {
expect(logger.statusText, contains('enable-linux-desktop: true (Unavailable)')); expect(logger.statusText, contains('enable-linux-desktop: true (Unavailable)'));
expect(logger.statusText, contains('enable-windows-desktop: true (Unavailable)')); expect(logger.statusText, contains('enable-windows-desktop: true (Unavailable)'));
expect(logger.statusText, contains('enable-macos-desktop: true (Unavailable)')); expect(logger.statusText, contains('enable-macos-desktop: true (Unavailable)'));
verifyNoAnalytics();
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
AndroidStudio: () => mockAndroidStudio, AndroidStudio: () => mockAndroidStudio,
AndroidSdk: () => mockAndroidSdk, AndroidSdk: () => mockAndroidSdk,
FlutterVersion: () => mockFlutterVersion, FlutterVersion: () => mockFlutterVersion,
Usage: () => mockUsage,
});
testUsingContext('no-analytics flag flips usage flag and sends event', () async {
final ConfigCommand configCommand = ConfigCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand);
await commandRunner.run(<String>[
'config',
'--no-analytics',
]);
expect(mockUsage.enabled, false);
// Verify that we only send the analytics disable event, and no other
// info.
verifyNever(mockUsage.sendCommand(
any,
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendTiming(
any,
any,
any,
label: anyNamed('label'),
));
expect(verify(mockUsage.sendEvent(
captureAny,
captureAny,
label: captureAnyNamed('label'),
value: anyNamed('value'),
parameters: anyNamed('parameters'),
)).captured,
<dynamic>['analytics', 'enabled', 'false'],
);
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
});
testUsingContext('analytics flag flips usage flag and sends event', () async {
final ConfigCommand configCommand = ConfigCommand();
final CommandRunner<void> commandRunner = createTestCommandRunner(configCommand);
await commandRunner.run(<String>[
'config',
'--analytics',
]);
expect(mockUsage.enabled, true);
// Verify that we only send the analytics disable event, and no other
// info.
verifyNever(mockUsage.sendCommand(
any,
parameters: anyNamed('parameters'),
));
verifyNever(mockUsage.sendTiming(
any,
any,
any,
label: anyNamed('label'),
));
expect(verify(mockUsage.sendEvent(
captureAny,
captureAny,
label: captureAnyNamed('label'),
value: anyNamed('value'),
parameters: anyNamed('parameters'),
)).captured,
<dynamic>['analytics', 'enabled', 'true'],
);
}, overrides: <Type, Generator>{
Usage: () => mockUsage,
}); });
}); });
} }
...@@ -161,3 +272,8 @@ class MockAndroidSdk extends Mock implements AndroidSdk { ...@@ -161,3 +272,8 @@ class MockAndroidSdk extends Mock implements AndroidSdk {
} }
class MockFlutterVersion extends Mock implements FlutterVersion {} class MockFlutterVersion extends Mock implements FlutterVersion {}
class MockUsage extends Mock implements Usage {
@override
bool enabled = true;
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_tools/runner.dart' as runner;
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
...@@ -9,6 +10,7 @@ import 'package:flutter_tools/src/base/platform.dart'; ...@@ -9,6 +10,7 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/upgrade.dart'; import 'package:flutter_tools/src/commands/upgrade.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -17,6 +19,7 @@ import 'package:process/process.dart'; ...@@ -17,6 +19,7 @@ import 'package:process/process.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/mocks.dart'; import '../../src/mocks.dart';
void main() { void main() {
...@@ -184,6 +187,49 @@ void main() { ...@@ -184,6 +187,49 @@ void main() {
ProcessManager: () => processManager, ProcessManager: () => processManager,
Platform: () => fakePlatform, Platform: () => fakePlatform,
}); });
group('full command', () {
final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>[
'git', 'describe', '--match', 'v*.*.*', '--first-parent', '--long', '--tags',
]),
]);
Directory tempDir;
File flutterToolState;
FlutterVersion mockFlutterVersion;
setUp(() {
Cache.disableLocking();
tempDir = fs.systemTempDirectory.createTempSync('flutter_upgrade_test.');
flutterToolState = tempDir.childFile('.flutter_tool_state');
mockFlutterVersion = MockFlutterVersion(isStable: true);
});
tearDown(() {
Cache.enableLocking();
tryToDelete(tempDir);
});
testUsingContext('upgrade continue prints welcome message', () async {
final UpgradeCommand upgradeCommand = UpgradeCommand(fakeCommandRunner);
await runner.run(
<String>[
'upgrade',
'--continue',
],
<FlutterCommand>[
upgradeCommand,
],
);
expect(testLogger.statusText, contains('Welcome to Flutter!'));
}, overrides: <Type, Generator>{
FlutterVersion: () => mockFlutterVersion,
ProcessManager: () => fakeProcessManager,
PersistentToolState: () => PersistentToolState(flutterToolState),
});
});
}); });
group('matchesGitLine', () { group('matchesGitLine', () {
...@@ -267,7 +313,6 @@ class FakeUpgradeCommandRunner extends UpgradeCommandRunner { ...@@ -267,7 +313,6 @@ class FakeUpgradeCommandRunner extends UpgradeCommandRunner {
Future<void> runDoctor() async {} Future<void> runDoctor() async {}
} }
class MockFlutterVersion extends Mock implements FlutterVersion {}
class MockProcess extends Mock implements Process {} class MockProcess extends Mock implements Process {}
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
class FakeProcessResult implements ProcessResult { class FakeProcessResult implements ProcessResult {
......
// Copyright 2019 The Chromium 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:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import '../src/common.dart';
import '../src/testbed.dart';
void main() {
Testbed testbed;
setUp(() {
testbed = Testbed();
});
test('state can be set and persists', () => testbed.run(() {
final File stateFile = fs.file('.flutter_tool_state');
final PersistentToolState state1 = PersistentToolState(stateFile);
expect(state1.redisplayWelcomeMessage, null);
state1.redisplayWelcomeMessage = true;
expect(stateFile.existsSync(), true);
expect(state1.redisplayWelcomeMessage, true);
state1.redisplayWelcomeMessage = false;
expect(state1.redisplayWelcomeMessage, false);
final PersistentToolState state2 = PersistentToolState(stateFile);
expect(state2.redisplayWelcomeMessage, false);
}));
}
...@@ -23,6 +23,7 @@ import 'package:flutter_tools/src/doctor.dart'; ...@@ -23,6 +23,7 @@ import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/persistent_tool_state.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/version.dart'; import 'package:flutter_tools/src/version.dart';
...@@ -71,12 +72,18 @@ void testUsingContext( ...@@ -71,12 +72,18 @@ void testUsingContext(
} }
}); });
Config buildConfig(FileSystem fs) { Config buildConfig(FileSystem fs) {
configDir = fs.systemTempDirectory.createTempSync('flutter_config_dir_test.'); configDir ??= fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
final File settingsFile = fs.file( final File settingsFile = fs.file(
fs.path.join(configDir.path, '.flutter_settings') fs.path.join(configDir.path, '.flutter_settings')
); );
return Config(settingsFile); return Config(settingsFile);
} }
PersistentToolState buildPersistentToolState(FileSystem fs) {
configDir ??= fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
final File toolStateFile = fs.file(
fs.path.join(configDir.path, '.flutter_tool_state'));
return PersistentToolState(toolStateFile);
}
test(description, () async { test(description, () async {
await runInContext<dynamic>(() { await runInContext<dynamic>(() {
...@@ -96,6 +103,7 @@ void testUsingContext( ...@@ -96,6 +103,7 @@ void testUsingContext(
OutputPreferences: () => OutputPreferences.test(), OutputPreferences: () => OutputPreferences.test(),
Logger: () => BufferLogger(), Logger: () => BufferLogger(),
OperatingSystemUtils: () => FakeOperatingSystemUtils(), OperatingSystemUtils: () => FakeOperatingSystemUtils(),
PersistentToolState: () => buildPersistentToolState(fs),
SimControl: () => MockSimControl(), SimControl: () => MockSimControl(),
Usage: () => FakeUsage(), Usage: () => FakeUsage(),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(), XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
......
...@@ -64,7 +64,7 @@ final Map<Type, Generator> _testbedDefaults = <Type, Generator>{ ...@@ -64,7 +64,7 @@ final Map<Type, Generator> _testbedDefaults = <Type, Generator>{
/// }); /// });
/// }) /// })
/// ///
/// test('Can delete a file', () => testBed.run(() { /// test('Can delete a file', () => testbed.run(() {
/// expect(fs.file('foo').existsSync(), true); /// expect(fs.file('foo').existsSync(), true);
/// fs.file('foo').deleteSync(); /// fs.file('foo').deleteSync();
/// expect(fs.file('foo').existsSync(), false); /// expect(fs.file('foo').existsSync(), false);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment