| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io' as io; |
| |
| import 'package:analysis_server/src/plugin/notification_manager.dart'; |
| import 'package:analysis_server/src/plugin/plugin_manager.dart'; |
| import 'package:analyzer/context/context_root.dart'; |
| import 'package:analyzer/file_system/memory_file_system.dart'; |
| import 'package:analyzer/file_system/physical_file_system.dart'; |
| import 'package:analyzer/instrumentation/instrumentation.dart'; |
| import 'package:analyzer_plugin/channel/channel.dart'; |
| import 'package:analyzer_plugin/protocol/protocol.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart' |
| hide ContextRoot; |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| import 'package:watcher/watcher.dart' as watcher; |
| |
| main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(PluginInfoTest); |
| defineReflectiveTests(PluginManagerTest); |
| defineReflectiveTests(PluginManagerFromDiskTest); |
| defineReflectiveTests(PluginSessionTest); |
| defineReflectiveTests(PluginSessionFromDiskTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class PluginInfoTest { |
| MemoryResourceProvider resourceProvider; |
| TestNotificationManager notificationManager; |
| String pluginPath = '/pluginDir'; |
| String executionPath = '/pluginDir/bin/plugin.dart'; |
| String packagesPath = '/pluginDir/.packages'; |
| PluginInfo plugin; |
| |
| void setUp() { |
| resourceProvider = new MemoryResourceProvider(); |
| notificationManager = new TestNotificationManager(); |
| plugin = new PluginInfo(pluginPath, executionPath, packagesPath, |
| notificationManager, InstrumentationService.NULL_SERVICE); |
| } |
| |
| test_addContextRoot() { |
| ContextRoot contextRoot1 = new ContextRoot('/pkg1', []); |
| plugin.addContextRoot(contextRoot1); |
| expect(plugin.contextRoots, [contextRoot1]); |
| plugin.addContextRoot(contextRoot1); |
| expect(plugin.contextRoots, [contextRoot1]); |
| } |
| |
| test_creation() { |
| expect(plugin.path, pluginPath); |
| expect(plugin.executionPath, executionPath); |
| expect(plugin.notificationManager, notificationManager); |
| expect(plugin.contextRoots, isEmpty); |
| expect(plugin.currentSession, isNull); |
| } |
| |
| test_removeContextRoot() { |
| ContextRoot contextRoot1 = new ContextRoot('/pkg1', []); |
| ContextRoot contextRoot2 = new ContextRoot('/pkg2', []); |
| plugin.addContextRoot(contextRoot1); |
| expect(plugin.contextRoots, unorderedEquals([contextRoot1])); |
| plugin.addContextRoot(contextRoot2); |
| expect(plugin.contextRoots, unorderedEquals([contextRoot1, contextRoot2])); |
| plugin.removeContextRoot(contextRoot1); |
| expect(plugin.contextRoots, unorderedEquals([contextRoot2])); |
| plugin.removeContextRoot(contextRoot2); |
| expect(plugin.contextRoots, isEmpty); |
| } |
| |
| @failingTest |
| test_start_notRunning() { |
| fail('Not tested'); |
| } |
| |
| test_start_running() async { |
| plugin.currentSession = new PluginSession(plugin); |
| try { |
| await plugin.start(''); |
| fail('Expected a StateError'); |
| } on StateError { |
| // Expected. |
| } |
| } |
| |
| test_stop_notRunning() { |
| expect(() => plugin.stop(), throwsA(new isInstanceOf<StateError>())); |
| } |
| |
| test_stop_running() { |
| PluginSession session = new PluginSession(plugin); |
| TestServerCommunicationChannel channel = |
| new TestServerCommunicationChannel(session); |
| plugin.currentSession = session; |
| plugin.stop(); |
| expect(plugin.currentSession, isNull); |
| expect(channel.sentRequests, hasLength(1)); |
| expect(channel.sentRequests[0].method, 'plugin.shutdown'); |
| } |
| } |
| |
| @reflectiveTest |
| class PluginManagerFromDiskTest extends PluginTestSupport { |
| String byteStorePath = '/byteStore'; |
| PluginManager manager; |
| |
| void setUp() { |
| super.setUp(); |
| manager = new PluginManager(resourceProvider, byteStorePath, |
| notificationManager, InstrumentationService.NULL_SERVICE); |
| } |
| |
| test_addPluginToContextRoot() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin(test: (String pluginPath) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, pluginPath); |
| await manager.stopAll(); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| |
| test_broadcastRequest_many() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin( |
| pluginName: 'plugin1', |
| test: (String plugin1Path) async { |
| await withPlugin( |
| pluginName: 'plugin2', |
| test: (String plugin2Path) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, plugin1Path); |
| await manager.addPluginToContextRoot(contextRoot, plugin2Path); |
| |
| Map<PluginInfo, Future<Response>> responses = |
| manager.broadcastRequest( |
| new CompletionGetSuggestionsParams( |
| '/pkg1/lib/pkg1.dart', 100), |
| contextRoot: contextRoot); |
| expect(responses, hasLength(2)); |
| |
| await manager.stopAll(); |
| }); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| |
| test_broadcastRequest_many_noContextRoot() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin( |
| pluginName: 'plugin1', |
| test: (String plugin1Path) async { |
| await withPlugin( |
| pluginName: 'plugin2', |
| test: (String plugin2Path) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, plugin1Path); |
| await manager.addPluginToContextRoot(contextRoot, plugin2Path); |
| |
| Map<PluginInfo, Future<Response>> responses = |
| manager.broadcastRequest(new CompletionGetSuggestionsParams( |
| '/pkg1/lib/pkg1.dart', 100)); |
| expect(responses, hasLength(2)); |
| |
| await manager.stopAll(); |
| }); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| |
| test_broadcastWatchEvent() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin( |
| pluginName: 'plugin1', |
| test: (String plugin1Path) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, plugin1Path); |
| List<PluginInfo> plugins = manager.pluginsForContextRoot(contextRoot); |
| expect(plugins, hasLength(1)); |
| watcher.WatchEvent watchEvent = new watcher.WatchEvent( |
| watcher.ChangeType.MODIFY, |
| path.join(plugin1Path, 'lib', 'lib.dart')); |
| List<Future<Response>> responses = |
| await manager.broadcastWatchEvent(watchEvent); |
| expect(responses, hasLength(1)); |
| Response response = await responses[0]; |
| expect(response, isNotNull); |
| expect(response.error, isNull); |
| await manager.stopAll(); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| |
| test_pluginsForContextRoot_multiple() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin( |
| pluginName: 'plugin1', |
| test: (String plugin1Path) async { |
| await withPlugin( |
| pluginName: 'plugin2', |
| test: (String plugin2Path) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, plugin1Path); |
| await manager.addPluginToContextRoot(contextRoot, plugin2Path); |
| |
| List<PluginInfo> plugins = |
| manager.pluginsForContextRoot(contextRoot); |
| expect(plugins, hasLength(2)); |
| List<String> paths = |
| plugins.map((PluginInfo plugin) => plugin.path).toList(); |
| expect(paths, unorderedEquals([plugin1Path, plugin2Path])); |
| |
| await manager.stopAll(); |
| }); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| |
| test_pluginsForContextRoot_one() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin(test: (String pluginPath) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, pluginPath); |
| |
| List<PluginInfo> plugins = manager.pluginsForContextRoot(contextRoot); |
| expect(plugins, hasLength(1)); |
| expect(plugins[0].path, pluginPath); |
| |
| await manager.stopAll(); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| |
| test_removedContextRoot() async { |
| io.Directory pkg1Dir = io.Directory.systemTemp.createTempSync('pkg1'); |
| String pkgPath = pkg1Dir.resolveSymbolicLinksSync(); |
| await withPlugin(test: (String pluginPath) async { |
| ContextRoot contextRoot = new ContextRoot(pkgPath, []); |
| await manager.addPluginToContextRoot(contextRoot, pluginPath); |
| |
| manager.removedContextRoot(contextRoot); |
| |
| await manager.stopAll(); |
| }); |
| pkg1Dir.deleteSync(recursive: true); |
| } |
| } |
| |
| @reflectiveTest |
| class PluginManagerTest { |
| MemoryResourceProvider resourceProvider; |
| String byteStorePath; |
| TestNotificationManager notificationManager; |
| PluginManager manager; |
| |
| void setUp() { |
| resourceProvider = new MemoryResourceProvider(); |
| byteStorePath = '/byteStore'; |
| notificationManager = new TestNotificationManager(); |
| manager = new PluginManager(resourceProvider, byteStorePath, |
| notificationManager, InstrumentationService.NULL_SERVICE); |
| } |
| |
| void test_broadcastRequest_none() { |
| ContextRoot contextRoot = new ContextRoot('/pkg1', []); |
| Map<PluginInfo, Future<Response>> responses = manager.broadcastRequest( |
| new CompletionGetSuggestionsParams('/pkg1/lib/pkg1.dart', 100), |
| contextRoot: contextRoot); |
| expect(responses, hasLength(0)); |
| } |
| |
| void test_creation() { |
| expect(manager.resourceProvider, resourceProvider); |
| expect(manager.byteStorePath, byteStorePath); |
| expect(manager.notificationManager, notificationManager); |
| } |
| |
| void test_pluginsForContextRoot_none() { |
| ContextRoot contextRoot = new ContextRoot('/pkg1', []); |
| expect(manager.pluginsForContextRoot(contextRoot), isEmpty); |
| } |
| |
| void test_stopAll_none() { |
| manager.stopAll(); |
| } |
| } |
| |
| @reflectiveTest |
| class PluginSessionFromDiskTest extends PluginTestSupport { |
| test_start_notRunning() async { |
| await withPlugin(test: (String pluginPath) async { |
| String packagesPath = path.join(pluginPath, '.packages'); |
| String mainPath = path.join(pluginPath, 'bin', 'plugin.dart'); |
| String byteStorePath = path.join(pluginPath, 'byteStore'); |
| new io.Directory(byteStorePath).createSync(); |
| PluginInfo plugin = new PluginInfo(pluginPath, mainPath, packagesPath, |
| notificationManager, InstrumentationService.NULL_SERVICE); |
| PluginSession session = new PluginSession(plugin); |
| plugin.currentSession = session; |
| expect(await session.start(byteStorePath), isTrue); |
| await session.stop(); |
| }); |
| } |
| } |
| |
| @reflectiveTest |
| class PluginSessionTest { |
| MemoryResourceProvider resourceProvider; |
| TestNotificationManager notificationManager; |
| String pluginPath = '/pluginDir'; |
| String executionPath = '/pluginDir/bin/plugin.dart'; |
| String packagesPath = '/pluginDir/.packages'; |
| PluginInfo plugin; |
| PluginSession session; |
| |
| void setUp() { |
| resourceProvider = new MemoryResourceProvider(); |
| notificationManager = new TestNotificationManager(); |
| plugin = new PluginInfo(pluginPath, executionPath, packagesPath, |
| notificationManager, InstrumentationService.NULL_SERVICE); |
| session = new PluginSession(plugin); |
| } |
| |
| void test_handleNotification() { |
| Notification notification = |
| new AnalysisErrorsParams('/test.dart', <AnalysisError>[]) |
| .toNotification(); |
| expect(notificationManager.notifications, hasLength(0)); |
| session.handleNotification(notification); |
| expect(notificationManager.notifications, hasLength(1)); |
| expect(notificationManager.notifications[0], notification); |
| } |
| |
| void test_handleOnDone() { |
| TestServerCommunicationChannel channel = |
| new TestServerCommunicationChannel(session); |
| session.handleOnDone(); |
| expect(channel.closeCount, 1); |
| expect(session.pluginStoppedCompleter.isCompleted, isTrue); |
| } |
| |
| @failingTest |
| void test_handleOnError() { |
| session.handleOnError(<String>['message', 'trace']); |
| fail('The method handleOnError is not implemented'); |
| } |
| |
| test_handleResponse() async { |
| new TestServerCommunicationChannel(session); |
| Response response = new PluginVersionCheckResult( |
| true, 'name', 'version', <String>[], |
| contactInfo: 'contactInfo') |
| .toResponse('0'); |
| Future<Response> future = |
| session.sendRequest(new PluginVersionCheckParams('', '')); |
| expect(session.pendingRequests, hasLength(1)); |
| session.handleResponse(response); |
| expect(session.pendingRequests, hasLength(0)); |
| Response result = await future; |
| expect(result, same(response)); |
| } |
| |
| void test_nextRequestId() { |
| expect(session.requestId, 0); |
| expect(session.nextRequestId, '0'); |
| expect(session.requestId, 1); |
| } |
| |
| void test_sendRequest() { |
| TestServerCommunicationChannel channel = |
| new TestServerCommunicationChannel(session); |
| session.sendRequest(new PluginVersionCheckParams('', '')); |
| expect(channel.sentRequests, hasLength(1)); |
| expect(channel.sentRequests[0].method, 'plugin.versionCheck'); |
| } |
| |
| test_start_notCompatible() async { |
| session.isCompatible = false; |
| expect(await session.start(path.join(pluginPath, 'byteStore')), isFalse); |
| } |
| |
| test_start_running() async { |
| new TestServerCommunicationChannel(session); |
| try { |
| await session.start(null); |
| fail('Expected a StateError to be thrown'); |
| } on StateError { |
| // Expected behavior |
| } |
| } |
| |
| test_stop_notRunning() { |
| expect(() => session.stop(), throwsA(new isInstanceOf<StateError>())); |
| } |
| |
| void test_stop_running() { |
| TestServerCommunicationChannel channel = |
| new TestServerCommunicationChannel(session); |
| session.stop(); |
| expect(channel.sentRequests, hasLength(1)); |
| expect(channel.sentRequests[0].method, 'plugin.shutdown'); |
| } |
| } |
| |
| /** |
| * A class designed to be used as a superclass for test classes that define |
| * tests that require plugins to be created on disk. |
| */ |
| abstract class PluginTestSupport { |
| PhysicalResourceProvider resourceProvider; |
| TestNotificationManager notificationManager; |
| |
| /** |
| * The content to be used for the '.packages' file, or `null` if the content |
| * has not yet been computed. |
| */ |
| String _packagesFileContent; |
| |
| void setUp() { |
| resourceProvider = PhysicalResourceProvider.INSTANCE; |
| notificationManager = new TestNotificationManager(); |
| } |
| |
| /** |
| * Create a directory structure representing a plugin on disk, run the given |
| * [test] function, and then remove the directory. The directory will have the |
| * following structure: |
| * ``` |
| * pluginDirectory |
| * .packages |
| * bin |
| * plugin.dart |
| * ``` |
| * The name of the plugin directory will be the [pluginName], if one is |
| * provided (in order to allow more than one plugin to be created by a single |
| * test). The 'plugin.dart' file will contain the given [content], or default |
| * content that implements a minimal plugin if the contents are not given. The |
| * [test] function will be passed the path of the directory that was created. |
| */ |
| Future<Null> withPlugin( |
| {String content, |
| String pluginName, |
| Future<Null> test(String pluginPath)}) async { |
| io.Directory tempDirectory = |
| io.Directory.systemTemp.createTempSync(pluginName ?? 'test_plugin'); |
| try { |
| String pluginPath = tempDirectory.resolveSymbolicLinksSync(); |
| // |
| // Create a .packages file. |
| // |
| io.File packagesFile = new io.File(path.join(pluginPath, '.packages')); |
| packagesFile.writeAsStringSync(_getPackagesFileContent()); |
| // |
| // Create the 'bin' directory. |
| // |
| String binPath = path.join(pluginPath, 'bin'); |
| new io.Directory(binPath).createSync(); |
| // |
| // Create the 'plugin.dart' file. |
| // |
| io.File pluginFile = new io.File(path.join(binPath, 'plugin.dart')); |
| pluginFile.writeAsStringSync(content ?? _defaultPluginContent()); |
| // |
| // Run the actual test code. |
| // |
| await test(pluginPath); |
| } finally { |
| tempDirectory.deleteSync(recursive: true); |
| } |
| } |
| |
| /** |
| * Convert the [sdkPackageMap] into a plugin-specific map by applying the |
| * given relative path [delta] to each line. |
| */ |
| String _convertPackageMap(String sdkDirPath, List<String> sdkPackageMap) { |
| StringBuffer buffer = new StringBuffer(); |
| for (String line in sdkPackageMap) { |
| if (!line.startsWith('#')) { |
| int index = line.indexOf(':'); |
| String packageName = line.substring(0, index + 1); |
| String relativePath = line.substring(index + 1); |
| String absolutePath = path.join(sdkDirPath, relativePath); |
| buffer.write(packageName); |
| buffer.writeln(absolutePath); |
| } |
| } |
| return buffer.toString(); |
| } |
| |
| /** |
| * The default content of the plugin. This is a minimal plugin that will only |
| * respond correctly to version checks and to shutdown requests. |
| */ |
| String _defaultPluginContent() { |
| return r''' |
| import 'dart:isolate'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/file_system/physical_file_system.dart'; |
| import 'package:analyzer_plugin/plugin/plugin.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart'; |
| import 'package:analyzer_plugin/starter.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| void main(List<String> args, SendPort sendPort) { |
| MinimalPlugin plugin = new MinimalPlugin(PhysicalResourceProvider.INSTANCE); |
| new ServerPluginStarter(plugin).start(sendPort); |
| } |
| |
| class MinimalPlugin extends ServerPlugin { |
| MinimalPlugin(ResourceProvider provider) : super(provider); |
| |
| @override |
| List<String> get fileGlobsToAnalyze => <String>['**/*.dart']; |
| |
| @override |
| String get name => 'minimal'; |
| |
| @override |
| String get version => '0.0.1'; |
| |
| @override |
| AnalysisHandleWatchEventsResult handleAnalysisHandleWatchEvents( |
| Map<String, Object> parameters) => |
| new AnalysisHandleWatchEventsResult(); |
| |
| @override |
| bool isCompatibleWith(Version serverVersion) => true; |
| } |
| '''; |
| } |
| |
| /** |
| * Return the content to be used for the '.packages' file. |
| */ |
| String _getPackagesFileContent() { |
| if (_packagesFileContent == null) { |
| io.File sdkPackagesFile = new io.File(_sdkPackagesPath()); |
| List<String> sdkPackageMap = sdkPackagesFile.readAsLinesSync(); |
| _packagesFileContent = |
| _convertPackageMap(path.dirname(sdkPackagesFile.path), sdkPackageMap); |
| } |
| return _packagesFileContent; |
| } |
| |
| /** |
| * Return the path to the '.packages' file in the root of the SDK checkout. |
| */ |
| String _sdkPackagesPath() { |
| String packagesPath = io.Platform.script.toFilePath(); |
| while (packagesPath.isNotEmpty && |
| path.basename(packagesPath) != 'analysis_server') { |
| packagesPath = path.dirname(packagesPath); |
| } |
| packagesPath = path.dirname(packagesPath); |
| packagesPath = path.dirname(packagesPath); |
| return path.join(packagesPath, '.packages'); |
| } |
| } |
| |
| class TestNotificationManager implements NotificationManager { |
| List<Notification> notifications = <Notification>[]; |
| |
| @override |
| void handlePluginNotification(String pluginId, Notification notification) { |
| notifications.add(notification); |
| } |
| |
| @override |
| noSuchMethod(Invocation invocation) { |
| fail('Unexpected invocation of ${invocation.memberName}'); |
| } |
| } |
| |
| class TestServerCommunicationChannel implements ServerCommunicationChannel { |
| int closeCount = 0; |
| List<Request> sentRequests = <Request>[]; |
| |
| TestServerCommunicationChannel(PluginSession session) { |
| session.channel = this; |
| } |
| |
| @override |
| void close() { |
| closeCount++; |
| } |
| |
| @override |
| void listen(void onResponse(Response response), |
| void onNotification(Notification notification), |
| {Function onError, void onDone()}) { |
| fail('Unexpected invocation of listen'); |
| } |
| |
| @override |
| void sendRequest(Request request) { |
| sentRequests.add(request); |
| } |
| } |