Unverified Commit 9bc533c9 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] do not error flutter doctor on missing AS/intellij plugins (#66782)

In cases where the Intellij/AS plugins are not located, display links to where they can be downloaded but do not surface an error. This should generally reduce confusion about whether the plugins are required for every installed IDE. For example, frequently users may only install AS so that they can install the Android SDK - or they may have multiple copies of Intellij installed.

For example: #66762
parent e8812c40
......@@ -2,26 +2,33 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import '../base/config.dart';
import '../base/file_system.dart';
import '../base/platform.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../doctor.dart';
import '../globals.dart' as globals;
import '../intellij/intellij.dart';
import 'android_studio.dart';
class AndroidStudioValidator extends DoctorValidator {
AndroidStudioValidator(this._studio) : super('Android Studio');
AndroidStudioValidator(this._studio, { @required FileSystem fileSystem })
: _fileSystem = fileSystem,
super('Android Studio');
final AndroidStudio _studio;
final FileSystem _fileSystem;
static List<DoctorValidator> get allValidators {
static List<DoctorValidator> allValidators(Config config, Platform platform, FileSystem fileSystem, UserMessages userMessages) {
final List<AndroidStudio> studios = AndroidStudio.allInstalled();
return <DoctorValidator>[
if (studios.isEmpty)
NoAndroidStudioValidator()
NoAndroidStudioValidator(config: config, platform: platform, userMessages: userMessages)
else
...studios.map<DoctorValidator>(
(AndroidStudio studio) => AndroidStudioValidator(studio)
(AndroidStudio studio) => AndroidStudioValidator(studio, fileSystem: fileSystem)
),
];
}
......@@ -38,14 +45,20 @@ class AndroidStudioValidator extends DoctorValidator {
userMessages.androidStudioLocation(_studio.directory),
));
final IntelliJPlugins plugins = IntelliJPlugins(_studio.pluginsPath);
final IntelliJPlugins plugins = IntelliJPlugins(_studio.pluginsPath, fileSystem: _fileSystem);
plugins.validatePackage(
messages,
<String>['flutter-intellij', 'flutter-intellij.jar'],
'Flutter',
IntelliJPlugins.kIntellijFlutterPluginUrl,
minVersion: IntelliJPlugins.kMinFlutterPluginVersion,
);
plugins.validatePackage(messages, <String>['Dart'], 'Dart');
plugins.validatePackage(
messages,
<String>['Dart'],
'Dart',
IntelliJPlugins.kIntellijDartPluginUrl,
);
if (_studio.isValid) {
type = _hasIssues(messages)
......@@ -74,21 +87,32 @@ class AndroidStudioValidator extends DoctorValidator {
}
class NoAndroidStudioValidator extends DoctorValidator {
NoAndroidStudioValidator() : super('Android Studio');
NoAndroidStudioValidator({
@required Config config,
@required Platform platform,
@required UserMessages userMessages,
}) : _config = config,
_platform = platform,
_userMessages = userMessages,
super('Android Studio');
final Config _config;
final Platform _platform;
final UserMessages _userMessages;
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
final String cfgAndroidStudio = globals.config.getValue(
final String cfgAndroidStudio = _config.getValue(
'android-studio-dir',
) as String;
if (cfgAndroidStudio != null) {
messages.add(ValidationMessage.error(
userMessages.androidStudioMissing(cfgAndroidStudio),
_userMessages.androidStudioMissing(cfgAndroidStudio),
));
}
messages.add(ValidationMessage(userMessages.androidStudioInstallation(globals.platform)));
messages.add(ValidationMessage(_userMessages.androidStudioInstallation(_platform)));
return ValidationResult(
ValidationType.notAvailable,
......
This diff is collapsed.
......@@ -3,23 +3,30 @@
// found in the LICENSE file.
import 'package:archive/archive.dart';
import 'package:meta/meta.dart';
import '../base/file_system.dart';
import '../base/version.dart';
import '../convert.dart';
import '../doctor.dart';
import '../globals.dart' as globals;
class IntelliJPlugins {
IntelliJPlugins(this.pluginsPath);
IntelliJPlugins(this.pluginsPath, {
@required FileSystem fileSystem
}) : _fileSystem = fileSystem;
final FileSystem _fileSystem;
final String pluginsPath;
static final Version kMinFlutterPluginVersion = Version(16, 0, 0);
static const String kIntellijDartPluginUrl = 'https://plugins.jetbrains.com/plugin/6351-dart';
static const String kIntellijFlutterPluginUrl = 'https://plugins.jetbrains.com/plugin/9212-flutter';
void validatePackage(
List<ValidationMessage> messages,
List<String> packageNames,
String title, {
String title,
String url, {
Version minVersion,
}) {
for (final String packageName in packageNames) {
......@@ -31,36 +38,36 @@ class IntelliJPlugins {
final Version version = Version.parse(versionText);
if (version != null && minVersion != null && version < minVersion) {
messages.add(ValidationMessage.error(
'$title plugin version $versionText - the recommended minimum version is $minVersion'));
'$title plugin version $versionText - the recommended minimum version is $minVersion'),
);
} else {
messages.add(ValidationMessage(
'$title plugin ${version != null ? "version $version" : "installed"}'));
'$title plugin ${version != null ? "version $version" : "installed"}'),
);
}
return;
}
messages.add(ValidationMessage.error(
'$title plugin not installed; this adds $title specific functionality.'));
messages.add(ValidationMessage(
'$title plugin can be installed from:',
contextUrl: url,
));
}
bool _hasPackage(String packageName) {
final String packagePath = globals.fs.path.join(pluginsPath, packageName);
final String packagePath = _fileSystem.path.join(pluginsPath, packageName);
if (packageName.endsWith('.jar')) {
return globals.fs.isFileSync(packagePath);
return _fileSystem.isFileSync(packagePath);
}
return globals.fs.isDirectorySync(packagePath);
return _fileSystem.isDirectorySync(packagePath);
}
String _readPackageVersion(String packageName) {
final String jarPath = packageName.endsWith('.jar')
? globals.fs.path.join(pluginsPath, packageName)
: globals.fs.path.join(pluginsPath, packageName, 'lib', '$packageName.jar');
// TODO(danrubel): look for a better way to extract a single 2K file from the zip
// rather than reading the entire file into memory.
? _fileSystem.path.join(pluginsPath, packageName)
: _fileSystem.path.join(pluginsPath, packageName, 'lib', '$packageName.jar');
try {
final Archive archive =
ZipDecoder().decodeBytes(globals.fs.file(jarPath).readAsBytesSync());
ZipDecoder().decodeBytes(_fileSystem.file(jarPath).readAsBytesSync());
final ArchiveFile file = archive.findFile('META-INF/plugin.xml');
final String content = utf8.decode(file.content as List<int>);
const String versionStartTag = '<version>';
......
......@@ -27,6 +27,7 @@ import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/vscode/vscode.dart';
import 'package:flutter_tools/src/vscode/vscode_validator.dart';
import 'package:flutter_tools/src/web/workflow.dart';
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:fake_async/fake_async.dart';
......@@ -61,6 +62,16 @@ void main() {
logger = BufferLogger.test();
});
testWithoutContext('ValidationMessage equality and hashCode includes contextUrl', () {
const ValidationMessage messageA = ValidationMessage('ab', contextUrl: 'a');
const ValidationMessage messageB = ValidationMessage('ab', contextUrl: 'b');
expect(messageB, isNot(messageA));
expect(messageB.hashCode, isNot(messageA.hashCode));
expect(messageA, isNot(messageB));
expect(messageA.hashCode, isNot(messageB.hashCode));
});
group('doctor', () {
MockPlistParser mockPlistParser;
MemoryFileSystem fileSystem;
......@@ -72,7 +83,8 @@ void main() {
testUsingContext('intellij validator', () async {
const String installPath = '/path/to/intelliJ';
final ValidationResult result = await IntelliJValidatorTestTarget('Test', installPath).validate();
// Uses real filesystem
final ValidationResult result = await IntelliJValidatorTestTarget('Test', installPath, fileSystem: globals.fs).validate();
expect(result.type, ValidationType.partial);
expect(result.statusInfo, 'version test.test.test');
expect(result.messages, hasLength(4));
......@@ -96,7 +108,7 @@ void main() {
final Directory pluginsDirectory = fileSystem.directory('/foo/bar/Library/Application Support/JetBrains/TestID2020.10/plugins')
..createSync(recursive: true);
final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/path/to/app');
final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/path/to/app', fileSystem: fileSystem);
expect(validator.plistFile, '/path/to/app/Contents/Info.plist');
expect(validator.pluginsPath, pluginsDirectory.path);
}, overrides: <Type, Generator>{
......@@ -113,7 +125,7 @@ void main() {
testUsingContext('legacy intellij plugins path checking on mac', () async {
when(mockPlistParser.getValueFromFile(any, PlistParser.kCFBundleShortVersionStringKey)).thenReturn('2020.10');
final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/foo');
final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/foo', fileSystem: fileSystem);
expect(validator.pluginsPath, '/foo/bar/Library/Application Support/TestID2020.10');
}, overrides: <Type, Generator>{
Platform: () => macPlatform,
......@@ -129,7 +141,7 @@ void main() {
testUsingContext('intellij plugins path checking on mac with override', () async {
when(mockPlistParser.getValueFromFile(any, 'JetBrainsToolboxApp')).thenReturn('/path/to/JetBrainsToolboxApp');
final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/foo');
final IntelliJValidatorOnMac validator = IntelliJValidatorOnMac('Test', 'TestID', '/foo', fileSystem: fileSystem);
expect(validator.pluginsPath, '/path/to/JetBrainsToolboxApp.plugins');
}, overrides: <Type, Generator>{
PlistParser: () => mockPlistParser,
......@@ -873,8 +885,10 @@ class NoOpDoctor implements Doctor {
class MockUsage extends Mock implements Usage {}
class IntelliJValidatorTestTarget extends IntelliJValidator {
IntelliJValidatorTestTarget(String title, String installPath) : super(title, installPath);
IntelliJValidatorTestTarget(String title, String installPath, {@required FileSystem fileSystem})
: super(title, installPath, fileSystem: fileSystem);
// Warning: requires real test data.
@override
String get pluginsPath => globals.fs.path.join('test', 'data', 'intellij', 'plugins');
......
......@@ -4,9 +4,12 @@
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_studio_validator.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/user_messages.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:mockito/mockito.dart';
......@@ -29,13 +32,19 @@ void main() {
fileSystem = MemoryFileSystem.test();
});
group('NoAndroidStudioValidator', () {
testUsingContext('shows Android Studio as "not available" when not available.', () async {
final NoAndroidStudioValidator validator = NoAndroidStudioValidator();
expect((await validator.validate()).type, equals(ValidationType.notAvailable));
}, overrides: <Type, Generator>{
Platform: () => linuxPlatform,
});
testWithoutContext('NoAndroidStudioValidator shows Android Studio as "not available" when not available.', () async {
final Config config = Config.test(
'test',
directory: fileSystem.currentDirectory,
logger: BufferLogger.test(),
);
final NoAndroidStudioValidator validator = NoAndroidStudioValidator(
config: config,
platform: linuxPlatform,
userMessages: UserMessages(),
);
expect((await validator.validate()).type, equals(ValidationType.notAvailable));
});
testUsingContext('AndroidStudioValidator gives doctor error on java crash', () async {
......@@ -53,7 +62,7 @@ void main() {
// This checks that running the validator doesn't throw an unhandled
// exception and that the ProcessException makes it into the error
// message list.
for (final DoctorValidator validator in AndroidStudioValidator.allValidators) {
for (final DoctorValidator validator in AndroidStudioValidator.allValidators(globals.config, globals.platform, globals.fs, globals.userMessages)) {
final ValidationResult result = await validator.validate();
expect(result.messages.where((ValidationMessage message) {
return message.isError && message.message.contains('ProcessException');
......
......@@ -10,91 +10,84 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/intellij/intellij.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
FileSystem fs;
FileSystem fileSystem;
void writeFileCreatingDirectories(String path, List<int> bytes) {
final File file = globals.fs.file(path);
final File file = fileSystem.file(path);
file.parent.createSync(recursive: true);
file.writeAsBytesSync(bytes);
}
setUp(() {
fs = MemoryFileSystem();
fileSystem = MemoryFileSystem.test();
});
group('IntelliJ', () {
group('plugins', () {
testUsingContext('found', () async {
final IntelliJPlugins plugins = IntelliJPlugins(_kPluginsPath);
testWithoutContext('IntelliJPlugins found', () async {
final IntelliJPlugins plugins = IntelliJPlugins(_kPluginsPath, fileSystem: fileSystem);
final Archive dartJarArchive =
buildSingleFileArchive('META-INF/plugin.xml', r'''
final Archive dartJarArchive =
buildSingleFileArchive('META-INF/plugin.xml', r'''
<idea-plugin version="2">
<name>Dart</name>
<version>162.2485</version>
<name>Dart</name>
<version>162.2485</version>
</idea-plugin>
''');
writeFileCreatingDirectories(
globals.fs.path.join(_kPluginsPath, 'Dart', 'lib', 'Dart.jar'),
ZipEncoder().encode(dartJarArchive));
writeFileCreatingDirectories(
fileSystem.path.join(_kPluginsPath, 'Dart', 'lib', 'Dart.jar'),
ZipEncoder().encode(dartJarArchive),
);
final Archive flutterJarArchive =
buildSingleFileArchive('META-INF/plugin.xml', r'''
final Archive flutterJarArchive = buildSingleFileArchive('META-INF/plugin.xml', r'''
<idea-plugin version="2">
<name>Flutter</name>
<version>0.1.3</version>
<name>Flutter</name>
<version>0.1.3</version>
</idea-plugin>
''');
writeFileCreatingDirectories(
globals.fs.path.join(_kPluginsPath, 'flutter-intellij.jar'),
ZipEncoder().encode(flutterJarArchive));
final List<ValidationMessage> messages = <ValidationMessage>[];
plugins.validatePackage(messages, <String>['Dart'], 'Dart');
plugins.validatePackage(messages,
<String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter',
minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
ValidationMessage message = messages
.firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
expect(message.message, 'Dart plugin version 162.2485');
message = messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '));
expect(message.message, contains('Flutter plugin version 0.1.3'));
expect(message.message, contains('recommended minimum version'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('not found', () async {
final IntelliJPlugins plugins = IntelliJPlugins(_kPluginsPath);
final List<ValidationMessage> messages = <ValidationMessage>[];
plugins.validatePackage(messages, <String>['Dart'], 'Dart');
plugins.validatePackage(messages,
<String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter',
minVersion: IntelliJPlugins.kMinFlutterPluginVersion);
ValidationMessage message = messages
.firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
expect(message.message, contains('Dart plugin not installed'));
message = messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '));
expect(message.message, contains('Flutter plugin not installed'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
});
writeFileCreatingDirectories(
fileSystem.path.join(_kPluginsPath, 'flutter-intellij.jar'),
ZipEncoder().encode(flutterJarArchive),
);
final List<ValidationMessage> messages = <ValidationMessage>[];
plugins.validatePackage(messages, <String>['Dart'], 'Dart', 'download-Dart');
plugins.validatePackage(messages,
<String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter', 'download-Flutter',
minVersion: IntelliJPlugins.kMinFlutterPluginVersion,
);
ValidationMessage message = messages
.firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
expect(message.message, 'Dart plugin version 162.2485');
message = messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '));
expect(message.message, contains('Flutter plugin version 0.1.3'));
expect(message.message, contains('recommended minimum version'));
});
testWithoutContext('IntelliJPlugins not found displays a link to their download site', () async {
final IntelliJPlugins plugins = IntelliJPlugins(_kPluginsPath, fileSystem: fileSystem);
final List<ValidationMessage> messages = <ValidationMessage>[];
plugins.validatePackage(messages, <String>['Dart'], 'Dart', 'download-Dart');
plugins.validatePackage(messages,
<String>['flutter-intellij', 'flutter-intellij.jar'], 'Flutter', 'download-Flutter',
minVersion: IntelliJPlugins.kMinFlutterPluginVersion,
);
ValidationMessage message = messages
.firstWhere((ValidationMessage m) => m.message.startsWith('Dart '));
expect(message.message, contains('Dart plugin can be installed from'));
expect(message.contextUrl, isNotNull);
message = messages.firstWhere(
(ValidationMessage m) => m.message.startsWith('Flutter '));
expect(message.message, contains('Flutter plugin can be installed from'));
expect(message.contextUrl, isNotNull);
});
}
......
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