// Copyright (c) 2014, 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 'package:analysis_server/protocol/protocol.dart'; import 'package:analysis_server/protocol/protocol_constants.dart'; import 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analysis_server/src/analytics/analytics_manager.dart'; import 'package:analysis_server/src/legacy_analysis_server.dart'; import 'package:analysis_server/src/server/crash_reporting_attachments.dart'; import 'package:analysis_server/src/server/error_notifier.dart'; import 'package:analysis_server/src/utilities/mocks.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/file_system/memory_file_system.dart'; import 'package:analyzer/src/generated/sdk.dart'; import 'package:analyzer/src/test_utilities/mock_sdk.dart'; import 'package:analyzer/utilities/package_config_file_builder.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:analyzer_testing/resource_provider_mixin.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'package:unified_analytics/unified_analytics.dart'; import 'constants.dart'; void main() { defineReflectiveSuite(() { defineReflectiveTests(AnalysisServerTest); }); } @reflectiveTest class AnalysisServerTest with ResourceProviderMixin { @override MemoryResourceProvider resourceProvider = MemoryResourceProvider( // Force the in-memory file watchers to be slowly initialized to emulate // the physical watchers (for test_concurrentContextRebuilds). delayWatcherInitialization: Duration(milliseconds: 1), ); late MockServerChannel channel; late ErrorNotifier errorNotifier; late LegacyAnalysisServer server; void setUp() { channel = MockServerChannel(printMessages: debugPrintCommunication); // Create an SDK in the mock file system. var sdkRoot = newFolder('/sdk'); createMockSdk(resourceProvider: resourceProvider, root: sdkRoot); errorNotifier = ErrorNotifier(); server = LegacyAnalysisServer( channel, resourceProvider, AnalysisServerOptions(), DartSdkManager(sdkRoot.path), AnalyticsManager(NoOpAnalytics()), CrashReportingAttachmentsBuilder.empty, errorNotifier, ); errorNotifier.server = server; } /// See https://github.com/dart-lang/sdk/issues/50496 Future test_caching_mixin_superInvokedNames_setter_change() async { var lib = convertPath('/lib'); newFolder(lib); var foo = newFile('/lib/foo.dart', ''' class A { set foo(int _) {} } mixin M on A { void bar() { super.boo = 0; } } class X extends A with M {} '''); await server.setAnalysisRoots('0', [lib], []); await server.onAnalysisComplete; expect(server.statusAnalyzing, isFalse); channel.notificationsReceived.clear(); server.updateContent('0', { foo.path: AddContentOverlay(''' class A { set foo(int _) {} } mixin M on A { void bar() { super.foo = 0; } } class X extends A with M {} '''), }); await server.onAnalysisComplete; expect(server.statusAnalyzing, isFalse); var notifications = channel.notificationsReceived; expect(notifications, hasLength(1)); var notification = notifications.first; expect(notification.event, 'analysis.errors'); var params = notification.params!; var errors = params['errors'] as List>; expect(errors, isEmpty); } /// Test that modifying package_config again while a context rebuild is in /// progress does not get lost due to a gap between creating a file watcher /// and it raising events. /// https://github.com/Dart-Code/Dart-Code/issues/3438 Future test_concurrentContextRebuilds() async { // Subscribe to STATUS so we'll know when analysis is done. server.serverServices = {ServerService.STATUS}; var projectRoot = convertPath('/foo'); var projectTestFile = convertPath('/foo/lib/test.dart'); var projectPackageConfigFile = convertPath( '/foo/.dart_tool/package_config.json', ); // Create a file that references two packages, which will we write to // package_config.json individually. newFolder(projectRoot); newFile(projectTestFile, r''' import "package:foo/foo.dart";' import "package:bar/bar.dart";' '''); // Ensure the packages and package_config exist. var fooLibFolder = _addSimplePackage('foo', ''); var barLibFolder = _addSimplePackage('bar', ''); var config = PackageConfigFileBuilder(); writePackageConfig(projectPackageConfigFile, config); // Track diagnostics that arrive. var errorsByFile = >{}; channel.notifications .where((notification) => notification.event == 'analysis.errors') .listen((notification) { var params = AnalysisErrorsParams.fromNotification( notification, clientUriConverter: server.uriConverter, ); errorsByFile[params.file] = params.errors; }); /// Helper that waits for analysis then returns the relevant errors. Future> getUriNotExistErrors() async { await server.onAnalysisComplete; expect(server.statusAnalyzing, isFalse); return errorsByFile[projectTestFile]! .where((error) => error.code == 'uri_does_not_exist') .toList(); } // Set roots and expect 2 uri_does_not_exist errors. await server.setAnalysisRoots('0', [projectRoot], []); expect(await getUriNotExistErrors(), hasLength(2)); // Write both packages, in two events so that the first one will trigger // a rebuild. config.add(name: 'foo', rootPath: fooLibFolder.parent.path); writePackageConfig(projectPackageConfigFile, config); await pumpEventQueue(times: 1); // Allow server to begin processing. config.add(name: 'bar', rootPath: barLibFolder.parent.path); writePackageConfig(projectPackageConfigFile, config); // Eventually the errors are gone. while (true) { var errors = await getUriNotExistErrors(); if (errors.isEmpty) { break; } await pumpEventQueue(times: 5000); } } Future test_errorNotification_errorNotifier() async { errorNotifier.logException(Exception('dummy exception')); var errors = channel.notificationsReceived.where( (notification) => notification.event == SERVER_NOTIFICATION_ERROR, ); expect( errors.single.params![SERVER_NOTIFICATION_ERROR_MESSAGE], contains('dummy exception'), ); } Future test_errorNotification_sendNotification() async { server.sendServerErrorNotification( 'message', Exception('dummy exception'), null, ); var errors = channel.notificationsReceived.where( (notification) => notification.event == SERVER_NOTIFICATION_ERROR, ); expect( errors.single.params![SERVER_NOTIFICATION_ERROR_MESSAGE], contains('dummy exception'), ); } Future test_serverStatusNotifications_hasFile() async { server.serverServices.add(ServerService.STATUS); newFile('/test/lib/a.dart', r''' class A {} '''); await server.setAnalysisRoots('0', [convertPath('/test')], []); // Pump the event queue, so that the server has finished any analysis. await pumpEventQueue(times: 5000); var notifications = channel.notificationsReceived; expect(notifications, isNotEmpty); // At least one notification indicating analysis is in progress. expect( notifications.any((Notification notification) { if (notification.event == SERVER_NOTIFICATION_STATUS) { var params = ServerStatusParams.fromNotification( notification, clientUriConverter: server.uriConverter, ); var analysis = params.analysis; if (analysis != null) { return analysis.isAnalyzing; } } return false; }), isTrue, ); // The last notification should indicate that analysis is complete. var notification = notifications[notifications.length - 1]; var params = ServerStatusParams.fromNotification( notification, clientUriConverter: server.uriConverter, ); expect(params.analysis!.isAnalyzing, isFalse); } Future test_serverStatusNotifications_noFiles() async { server.serverServices.add(ServerService.STATUS); newFolder('/test'); await server.setAnalysisRoots('0', [convertPath('/test')], []); // Pump the event queue, so that the server has finished any analysis. await pumpEventQueue(times: 5000); var notifications = channel.notificationsReceived; expect(notifications, isNotEmpty); // At least one notification indicating analysis is in progress. expect( notifications.any((Notification notification) { if (notification.event == SERVER_NOTIFICATION_STATUS) { var params = ServerStatusParams.fromNotification( notification, clientUriConverter: server.uriConverter, ); var analysis = params.analysis; if (analysis != null) { return analysis.isAnalyzing; } } return false; }), isTrue, ); // The last notification should indicate that analysis is complete. var notification = notifications[notifications.length - 1]; var params = ServerStatusParams.fromNotification( notification, clientUriConverter: server.uriConverter, ); expect(params.analysis!.isAnalyzing, isFalse); } Future test_setAnalysisSubscriptions_fileInIgnoredFolder_newOptions() async { var path = convertPath('/project/samples/sample.dart'); newFile(path, ''); newAnalysisOptionsYamlFile('/project', r''' analyzer: exclude: - 'samples/**' '''); await server.setAnalysisRoots('0', [convertPath('/project')], []); server.setAnalysisSubscriptions(>{ AnalysisService.NAVIGATION: {path}, }); // We respect subscriptions, even for excluded files. await pumpEventQueue(times: 5000); expect( channel.notificationsReceived.any((notification) { return notification.event == ANALYSIS_NOTIFICATION_NAVIGATION; }), isTrue, ); } Future test_setAnalysisSubscriptions_fileInIgnoredFolder_oldOptions() async { var path = convertPath('/project/samples/sample.dart'); newFile(path, ''); newAnalysisOptionsYamlFile('/project', r''' analyzer: exclude: - 'samples/**' '''); await server.setAnalysisRoots('0', [convertPath('/project')], []); server.setAnalysisSubscriptions(>{ AnalysisService.NAVIGATION: {path}, }); // We respect subscriptions, even for excluded files. await pumpEventQueue(times: 5000); expect( channel.notificationsReceived.any((notification) { return notification.event == ANALYSIS_NOTIFICATION_NAVIGATION; }), isTrue, ); } Future test_shutdown() { var request = Request('my28', SERVER_REQUEST_SHUTDOWN); return channel.simulateRequestFromClient(request).then((Response response) { expect(response.id, equals('my28')); expect(response.error, isNull); }); } Future test_unknownRequest() { var request = Request('my22', 'randomRequest'); return channel.simulateRequestFromClient(request).then((Response response) { expect(response.id, equals('my22')); expect(response.error, isNotNull); }); } void writePackageConfig(String path, PackageConfigFileBuilder config) { newFile(path, config.toContent(pathContext: pathContext)); } /// Creates a simple package named [name] with [content] in the file at /// `package:$name/$name.dart`. /// /// Returns a [Folder] that represents the packages `lib` folder. Folder _addSimplePackage(String name, String content) { var packagePath = '/packages/$name'; var file = newFile('$packagePath/lib/$name.dart', content); return file.parent; } }