// 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:convert'; import 'dart:isolate'; import 'package:analyzer/dart/analysis/analysis_context.dart'; import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/src/test_utilities/mock_sdk.dart'; import 'package:analyzer_plugin/plugin/plugin.dart'; import 'package:analyzer_plugin/protocol/protocol.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'; import 'package:analyzer_testing/resource_provider_mixin.dart'; import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'mocks.dart'; void main() { defineReflectiveTests(ServerPluginTest); } abstract class AbstractPluginTest with ResourceProviderMixin { final MockChannel channel = MockChannel(); late final ServerPlugin plugin; Folder get byteStoreRoot => getFolder('/byteStore'); Version get pluginSpecificationVersion => Version(0, 1, 0); Folder get sdkRoot => getFolder('/sdk'); ServerPlugin createPlugin(); @mustCallSuper Future setUp() async { createMockSdk(resourceProvider: resourceProvider, root: sdkRoot); plugin = createPlugin(); plugin.start(channel); await plugin.handlePluginVersionCheck( PluginVersionCheckParams( byteStoreRoot.path, sdkRoot.path, pluginSpecificationVersion.canonicalizedVersion, ), ); } } @reflectiveTest class ServerPluginTest extends AbstractPluginTest { late String packagePath1; late String filePath1; late ContextRoot contextRoot1; late String packagePath2; late String filePath2; late ContextRoot contextRoot2; /// Asserts that [params] is valid to send to an [Isolate] started with /// [Isolate.spawnUri]. Future assertValidForIsolateSend(RequestParams params) async { const isolateSource = r''' import 'dart:isolate'; void main(List args, SendPort sendPort) { final receivePort = ReceivePort(); sendPort.send(receivePort.sendPort); receivePort.listen((msg) => sendPort.send('ECHO: $msg')); } '''; var isolateUri = Uri.dataFromString(isolateSource, encoding: utf8); var receivePort = ReceivePort(); var isolate = await Isolate.spawnUri(isolateUri, [], receivePort.sendPort); var sendPort = (await receivePort.first) as SendPort; try { sendPort.send(params.toJson()); } catch (e) { fail('Failed to send ${params.runtimeType} across Isolate: $e'); } isolate.kill(); } @override ServerPlugin createPlugin() { return _TestServerPlugin(resourceProvider); } @override Future setUp() async { await super.setUp(); packagePath1 = convertPath('/package1'); filePath1 = join(packagePath1, 'lib', 'test.dart'); newFile(filePath1, ''); contextRoot1 = ContextRoot(packagePath1, []); packagePath2 = convertPath('/package2'); filePath2 = join(packagePath2, 'lib', 'test.dart'); newFile(filePath2, ''); contextRoot2 = ContextRoot(packagePath2, []); } Future test_handleAnalysisGetNavigation() async { var result = await plugin.handleAnalysisGetNavigation( AnalysisGetNavigationParams('', 1, 2), ); expect(result, isNotNull); } Future test_handleAnalysisHandleWatchEvents() async { var result = await plugin.handleAnalysisHandleWatchEvents( AnalysisHandleWatchEventsParams([]), ); expect(result, isNotNull); } Future test_handleAnalysisSetPriorityFiles() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var result = await plugin.handleAnalysisSetPriorityFiles( AnalysisSetPriorityFilesParams([filePath1]), ); expect(result, isNotNull); } Future test_handleAnalysisSetSubscriptions() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); expect(plugin.subscriptionManager.servicesForFile(filePath1), isEmpty); var result = await plugin.handleAnalysisSetSubscriptions( AnalysisSetSubscriptionsParams({ AnalysisService.OUTLINE: [filePath1], }), ); expect(result, isNotNull); expect(plugin.subscriptionManager.servicesForFile(filePath1), [ AnalysisService.OUTLINE, ]); } Future test_handleAnalysisUpdateContent_addChangeRemove() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var addResult = await plugin.handleAnalysisUpdateContent( AnalysisUpdateContentParams({filePath1: AddContentOverlay('class C {}')}), ); expect(addResult, isNotNull); var changeResult = await plugin.handleAnalysisUpdateContent( AnalysisUpdateContentParams({ filePath1: ChangeContentOverlay([SourceEdit(7, 0, ' extends Object')]), }), ); expect(changeResult, isNotNull); var removeResult = await plugin.handleAnalysisUpdateContent( AnalysisUpdateContentParams({filePath1: RemoveContentOverlay()}), ); expect(removeResult, isNotNull); } Future test_handleAnalysisUpdateContent_changeNoAdd() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); try { await plugin.handleAnalysisUpdateContent( AnalysisUpdateContentParams({ filePath1: ChangeContentOverlay([ SourceEdit(7, 0, ' extends Object'), ]), }), ); fail('Expected RequestFailure'); } on RequestFailure { // Expected } } Future test_handleAnalysisUpdateContent_invalidChange() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); await plugin.handleAnalysisUpdateContent( AnalysisUpdateContentParams({filePath1: AddContentOverlay('class C {}')}), ); try { await plugin.handleAnalysisUpdateContent( AnalysisUpdateContentParams({ filePath1: ChangeContentOverlay([SourceEdit(20, 5, 'class D {}')]), }), ); fail('Expected RequestFailure'); } on RequestFailure { // Expected } } Future test_handleCompletionGetSuggestions() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var result = await plugin.handleCompletionGetSuggestions( CompletionGetSuggestionsParams(filePath1, 12), ); expect(result, isNotNull); } Future test_handleEditGetAssists() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var result = await plugin.handleEditGetAssists( EditGetAssistsParams(filePath1, 10, 0), ); expect(result, isNotNull); } Future test_handleEditGetAvailableRefactorings() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var result = await plugin.handleEditGetAvailableRefactorings( EditGetAvailableRefactoringsParams(filePath1, 10, 0), ); expect(result, isNotNull); } Future test_handleEditGetFixes() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var result = await plugin.handleEditGetFixes( EditGetFixesParams(filePath1, 13), ); expect(result, isNotNull); } @failingTest Future test_handleEditGetRefactoring() async { await plugin.handleAnalysisSetContextRoots( AnalysisSetContextRootsParams([contextRoot1]), ); var result = await plugin.handleEditGetRefactoring( EditGetRefactoringParams(RefactoringKind.RENAME, filePath1, 7, 0, false), ); expect(result, isNotNull); } Future test_handlePluginShutdown() async { var result = await plugin.handlePluginShutdown(PluginShutdownParams()); expect(result, isNotNull); } Future test_handlePluginVersionCheck() async { var result = await plugin.handlePluginVersionCheck( PluginVersionCheckParams('byteStorePath', 'sdkPath', '0.1.0'), ); expect(result, isNotNull); expect(result.interestingFiles, ['*.dart']); expect(result.isCompatible, isTrue); expect(result.name, 'Test Plugin'); expect(result.version, '0.1.0'); } @failingTest void test_isCompatibleWith() { fail('Not yet implemented.'); } Future test_isolateSend_analysisGetNavigation() async { await assertValidForIsolateSend(AnalysisGetNavigationParams('', 1, 2)); } Future test_isolateSend_analysisHandleWatchEvents() async { await assertValidForIsolateSend(AnalysisHandleWatchEventsParams([])); } Future test_isolateSend_analysisSetPriorityFiles() async { await assertValidForIsolateSend( AnalysisSetPriorityFilesParams([filePath1]), ); } Future test_isolateSend_analysisSetSubscriptions() async { await assertValidForIsolateSend( AnalysisSetSubscriptionsParams({ AnalysisService.OUTLINE: [filePath1], }), ); } Future test_isolateSend_analysisUpdateContent_add() async { await assertValidForIsolateSend( AnalysisUpdateContentParams({filePath1: AddContentOverlay('class C {}')}), ); } Future test_isolateSend_analysisUpdateContent_change() async { await assertValidForIsolateSend( AnalysisUpdateContentParams({ filePath1: ChangeContentOverlay([SourceEdit(7, 0, ' extends Object')]), }), ); } Future test_isolateSend_analysisUpdateContent_remove() async { await assertValidForIsolateSend( AnalysisUpdateContentParams({filePath1: RemoveContentOverlay()}), ); } Future test_isolateSend_completionGetSuggestions() async { await assertValidForIsolateSend( CompletionGetSuggestionsParams(filePath1, 12), ); } Future test_isolateSend_editGetAssists() async { await assertValidForIsolateSend(EditGetAssistsParams(filePath1, 10, 0)); } Future test_isolateSend_editGetAvailableRefactorings() async { await assertValidForIsolateSend( EditGetAvailableRefactoringsParams(filePath1, 10, 0), ); } Future test_isolateSend_editGetFixes() async { await assertValidForIsolateSend(EditGetFixesParams(filePath1, 13)); } Future test_isolateSend_editGetRefactoring() async { await assertValidForIsolateSend( EditGetRefactoringParams(RefactoringKind.RENAME, filePath1, 7, 0, false), ); } Future test_isolateSend_pluginShutdown() async { await assertValidForIsolateSend(PluginShutdownParams()); } Future test_isolateSend_pluginVersionCheck() async { await assertValidForIsolateSend( PluginVersionCheckParams('byteStorePath', 'sdkPath', '0.1.0'), ); } Future test_isolateSend_setContextRoots() async { await assertValidForIsolateSend( AnalysisSetContextRootsParams([contextRoot1]), ); } void test_onDone() { channel.sendDone(); } void test_onError() { channel.sendError(ArgumentError(), StackTrace.fromString('')); } Future test_onRequest_analysisGetNavigation() async { var result = await channel.sendRequest( AnalysisGetNavigationParams('', 1, 2), ); expect(result, isNotNull); } Future test_onRequest_analysisHandleWatchEvents() async { var result = await channel.sendRequest(AnalysisHandleWatchEventsParams([])); expect(result, isNotNull); } Future test_onRequest_analysisSetContextRoots() async { var plugin = this.plugin as _TestServerPlugin; var analyzedPaths = []; plugin.analyzeFileHandler = ({required AnalysisContext analysisContext, required String path}) { analyzedPaths.add(path); }; var result = await channel.sendRequest( AnalysisSetContextRootsParams([contextRoot1]), ); expect(result, isNotNull); expect(plugin.invoked_afterNewContextCollection, isTrue); expect(analyzedPaths, [getFile('/package1/lib/test.dart').path]); } Future test_onRequest_analysisSetPriorityFiles() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var result = await channel.sendRequest( AnalysisSetPriorityFilesParams([filePath1]), ); expect(result, isNotNull); } Future test_onRequest_analysisSetSubscriptions() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); expect(plugin.subscriptionManager.servicesForFile(filePath1), isEmpty); var result = await channel.sendRequest( AnalysisSetSubscriptionsParams({ AnalysisService.OUTLINE: [filePath1], }), ); expect(result, isNotNull); expect(plugin.subscriptionManager.servicesForFile(filePath1), [ AnalysisService.OUTLINE, ]); } Future test_onRequest_analysisUpdateContent_addChangeRemove() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var addResult = await channel.sendRequest( AnalysisUpdateContentParams({filePath1: AddContentOverlay('class C {}')}), ); expect(addResult, isNotNull); var changeResult = await channel.sendRequest( AnalysisUpdateContentParams({ filePath1: ChangeContentOverlay([SourceEdit(7, 0, ' extends Object')]), }), ); expect(changeResult, isNotNull); var removeResult = await channel.sendRequest( AnalysisUpdateContentParams({filePath1: RemoveContentOverlay()}), ); expect(removeResult, isNotNull); } Future test_onRequest_analyzeFileIsNotContextConfused() async { var plugin = this.plugin as _TestServerPlugin; var confusedPaths = []; plugin.analyzeFileHandler = ({required AnalysisContext analysisContext, required String path}) { // Test that path actually belongs to analysisContext if (!analysisContext.contextRoot.isAnalyzed(path)) { confusedPaths.add( '$path analyzed in context for' ' ${analysisContext.contextRoot.root.path}', ); } }; var result = await channel.sendRequest( AnalysisSetPriorityFilesParams([filePath1, filePath2]), ); expect(result, isNotNull); await channel.sendRequest( AnalysisSetContextRootsParams([contextRoot1, contextRoot2]), ); expect(confusedPaths, []); } Future test_onRequest_completionGetSuggestions() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var result = await channel.sendRequest( CompletionGetSuggestionsParams(filePath1, 12), ); expect(result, isNotNull); } Future test_onRequest_editGetAssists() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var result = await channel.sendRequest( EditGetAssistsParams(filePath1, 10, 0), ); expect(result, isNotNull); } Future test_onRequest_editGetAvailableRefactorings() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var result = await channel.sendRequest( EditGetAvailableRefactoringsParams(filePath1, 10, 0), ); expect(result, isNotNull); } Future test_onRequest_editGetFixes() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var result = await channel.sendRequest(EditGetFixesParams(filePath1, 13)); expect(result, isNotNull); } Future test_onRequest_editGetRefactoring() async { await channel.sendRequest(AnalysisSetContextRootsParams([contextRoot1])); var result = await channel.sendRequest( EditGetRefactoringParams(RefactoringKind.RENAME, filePath1, 7, 0, false), ); expect(result, isNotNull); } Future test_onRequest_pluginShutdown() async { var result = await channel.sendRequest(PluginShutdownParams()); expect(result, isNotNull); } Future test_onRequest_pluginVersionCheck() async { var response = await channel.sendRequest( PluginVersionCheckParams('byteStorePath', 'sdkPath', '0.1.0'), ); var result = PluginVersionCheckResult.fromResponse(response); expect(result, isNotNull); expect(result.interestingFiles, ['*.dart']); expect(result.isCompatible, isTrue); expect(result.name, 'Test Plugin'); expect(result.version, '0.1.0'); } void test_sendNotificationsForFile() { var service1 = AnalysisService.FOLDING; var service2 = AnalysisService.NAVIGATION; var service3 = AnalysisService.OUTLINE; plugin.subscriptionManager.setSubscriptions({ service1: [filePath1, filePath2], service2: [filePath1], service3: [filePath2], }); plugin.sendNotificationsForFile(filePath1); var notifications = (plugin as _TestServerPlugin).sentNotifications; expect(notifications, hasLength(1)); var services = notifications[filePath1]; expect(services, unorderedEquals([service1, service2])); } void test_sendNotificationsForSubscriptions() { var subscriptions = >{}; plugin.sendNotificationsForSubscriptions(subscriptions); var notifications = (plugin as _TestServerPlugin).sentNotifications; expect(notifications, hasLength(subscriptions.length)); for (var path in subscriptions.keys) { var subscribedServices = subscriptions[path]; var notifiedServices = notifications[path]; expect( notifiedServices, isNotNull, reason: 'Not notified for file $path', ); expect( notifiedServices, unorderedEquals(subscribedServices!), reason: 'Wrong notifications for file $path', ); } } } class _TestServerPlugin extends MockServerPlugin { Map> sentNotifications = >{}; bool invoked_afterNewContextCollection = false; void Function({ required AnalysisContext analysisContext, required String path, })? analyzeFileHandler; _TestServerPlugin(super.resourceProvider); @override Future afterNewContextCollection({ required AnalysisContextCollection contextCollection, }) { invoked_afterNewContextCollection = true; return super.afterNewContextCollection( contextCollection: contextCollection, ); } @override Future analyzeFile({ required AnalysisContext analysisContext, required String path, }) async { analyzeFileHandler?.call(analysisContext: analysisContext, path: path); } @override Future sendFoldingNotification(String path) { _sent(path, AnalysisService.FOLDING); return Future.value(); } @override Future sendHighlightsNotification(String path) { _sent(path, AnalysisService.HIGHLIGHTS); return Future.value(); } @override Future sendNavigationNotification(String path) { _sent(path, AnalysisService.NAVIGATION); return Future.value(); } @override Future sendOccurrencesNotification(String path) { _sent(path, AnalysisService.OCCURRENCES); return Future.value(); } @override Future sendOutlineNotification(String path) { _sent(path, AnalysisService.OUTLINE); return Future.value(); } void _sent(String path, AnalysisService service) { sentNotifications.putIfAbsent(path, () => []).add(service); } }