| // Copyright (c) 2018, 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 'package:analysis_server/lsp_protocol/protocol.dart'; |
| import 'package:analysis_server/src/analysis_server.dart' hide MessageType; |
| import 'package:analysis_server/src/lsp/constants.dart'; |
| import 'package:analysis_server/src/lsp/server_capabilities_computer.dart'; |
| import 'package:analysis_server/src/plugin/plugin_manager.dart'; |
| import 'package:analyzer/file_system/memory_file_system.dart'; |
| import 'package:analyzer/src/util/file_paths.dart' as file_paths; |
| import 'package:language_server_protocol/json_parsing.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import 'change_workspace_folders_test.dart'; |
| import 'server_abstract.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(InitializationTest); |
| defineReflectiveTests(SlowInitializationTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class InitializationTest extends AbstractLspAnalysisServerTest { |
| /// Waits for any in-progress analysis context rebuild. |
| /// |
| /// Pumps the event queue before and after, to ensure any server code that |
| /// runs after the rebuild has had chance to run. |
| Future<void> get contextRebuildComplete async { |
| await pumpEventQueue(times: 5000); |
| await server.analysisContextsRebuilt; |
| await pumpEventQueue(times: 5000); |
| } |
| |
| Future<void> assertDynamicRegistration( |
| String name, Set<Method> expectedResult) async { |
| setTextDocumentDynamicRegistration(name); |
| setWorkspaceDynamicRegistration(name); |
| |
| // Check that when the server calls client/registerCapability it only includes |
| // the items we advertised dynamic registration support for. |
| var registrations = <Registration>[]; |
| await monitorDynamicRegistrations( |
| registrations, |
| initialize, |
| ); |
| |
| var registeredMethods = |
| registrations.map((registration) => registration.method).toSet(); |
| var result = expectedResult.map((method) => method.toJson()).toSet(); |
| |
| expect(registeredMethods, equals(result)); |
| } |
| |
| /// Expects successful initialization, but a window/showMessage warning |
| /// [expectedMessage] for the given [experimentalCapabilities]. |
| Future<void> expectInvalidExperimentalParams( |
| Map<String, Object?> experimentalCapabilities, |
| String expectedMessage, |
| ) async { |
| var warning = showMessageNotifications |
| .firstWhere((m) => m.type == MessageType.Warning); |
| |
| await initialize(experimentalCapabilities: experimentalCapabilities); |
| var message = (await warning).message; |
| |
| expect( |
| message, |
| allOf( |
| contains('errors parsing your experimental client capabilities'), |
| contains(expectedMessage), |
| ), |
| ); |
| } |
| |
| TextDocumentRegistrationOptions registrationOptionsFor( |
| List<Registration> registrations, |
| Method method, |
| ) { |
| var options = registrationFor(registrations, method)?.registerOptions; |
| if (options == null) { |
| throw 'Registration options for $method were not found. ' |
| 'Perhaps dynamicRegistration is missing from ' |
| 'withAllSupportedTextDocumentDynamicRegistrations?'; |
| } |
| return TextDocumentRegistrationOptions.fromJson( |
| options as Map<String, Object?>); |
| } |
| |
| Future<void> test_blazeWorkspace() async { |
| var workspacePath = '/home/user/ws'; |
| // Make it a Blaze workspace. |
| newFile('$workspacePath/${file_paths.blazeWorkspaceMarker}', ''); |
| |
| var packagePath = '$workspacePath/team/project1'; |
| |
| // Make it a Blaze project. |
| newFile(convertPath('$packagePath/BUILD'), ''); |
| |
| var file1 = convertPath('$packagePath/lib/file1.dart'); |
| newFile(file1, ''); |
| |
| await initialize(allowEmptyRootUri: true); |
| |
| // Expect that context manager includes a whole package. |
| await openFile(pathContext.toUri(file1), ''); |
| expect(server.contextManager.includedPaths, |
| equals([convertPath(packagePath)])); |
| } |
| |
| Future<void> test_completionRegistrations_triggerCharacters() async { |
| // Support dynamic registration for everything we support. |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| setAllSupportedWorkspaceDynamicRegistrations(); |
| |
| var registrations = <Registration>[]; |
| var initResponse = await monitorDynamicRegistrations( |
| registrations, |
| () => initialize(), |
| ); |
| |
| var initResult = |
| InitializeResult.fromJson(initResponse.result as Map<String, Object?>); |
| expect(initResult.capabilities, isNotNull); |
| |
| // Check Dart-only registration. |
| var dartRegistration = |
| registrationForDart(registrations, Method.textDocument_completion); |
| var dartOptions = CompletionRegistrationOptions.fromJson( |
| dartRegistration.registerOptions as Map<String, Object?>); |
| expect(dartOptions.documentSelector, hasLength(1)); |
| expect(dartOptions.documentSelector![0].language, dartLanguageId); |
| expect(dartOptions.triggerCharacters, isNotEmpty); |
| |
| // Check non-Dart registration. |
| var nonDartRegistration = registrations.singleWhere((r) => |
| r.method == Method.textDocument_completion.toJson() && |
| r != dartRegistration); |
| var nonDartOptions = CompletionRegistrationOptions.fromJson( |
| nonDartRegistration.registerOptions as Map<String, Object?>); |
| var otherLanguages = nonDartOptions.documentSelector! |
| .map((selector) => selector.language) |
| .toList(); |
| expect(otherLanguages, isNot(contains('dart'))); |
| expect(nonDartOptions.triggerCharacters, isNull); |
| } |
| |
| Future<void> test_completionRegistrations_withDartPlugin() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| |
| // This tests for a bug that occurred with an analysis server plugin |
| // that works on Dart files. When computing completion registrations we |
| // usually have separate registrations for Dart + non-Dart to account for |
| // different trigger characters. However, the plugin types were all being |
| // included in the non-Dart registration even if they included Dart. |
| // |
| // The result was two registrations including Dart, which caused duplicate |
| // requests for Dart completions, which resulted in duplicate items |
| // appearing in the editor. |
| |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| |
| // Track all current registrations. |
| var registrations = <Registration>[]; |
| |
| // Perform normal registration (without plugins) to get the initial set. |
| await monitorDynamicRegistrations(registrations, initialize); |
| |
| // Expect only a single registration that includes Dart files. |
| expect( |
| registrationsForDart(registrations, Method.textDocument_completion), |
| hasLength(1), |
| ); |
| |
| // Monitor the unregistration/new registrations during the plugin activation. |
| await monitorDynamicReregistration(registrations, () async { |
| var plugin = configureTestPlugin(); |
| plugin.currentSession = PluginSession(plugin) |
| ..interestingFiles = ['*.dart']; |
| pluginManager.pluginsChangedController.add(null); |
| }); |
| |
| // Expect that there is still only a single registration for Dart. |
| expect( |
| registrationsForDart(registrations, Method.textDocument_completion), |
| hasLength(1), |
| ); |
| } |
| |
| Future<void> test_dynamicRegistration_areNotInterleaved() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| |
| // Some of the issues in https://github.com/dart-lang/sdk/issues/47851 |
| // (duplicate hovers/code actions/etc.) were caused by duplicate |
| // registrations. This happened when we tried to rebuild registrations |
| // concurrently as the code would: |
| // |
| // 1. compute the new set of registrations |
| // 2. compute which had changed |
| // 3. send _and await_ the unregistration |
| // 4. send _and await_ the new registration |
| // |
| // If the code was triggered multiple times concurrently, it was possible |
| // for step 3 in second request to occur before step 4 in the first, which |
| // would result in unregistrations being sent for registrations that had |
| // not been sent. They would be silently dropped, and then we'd end up with |
| // multiple registrations. |
| // |
| // This test triggers the rebuild via pluginManager.pluginsChangedController |
| // multiple times and ensures the events all arrive in the correct order, |
| // that is, no unregistration ever contains the ID of something that is not |
| // currently registered. |
| |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| |
| var registrations = <Registration>[]; |
| await monitorDynamicRegistrations(registrations, initialize); |
| |
| var knownRegistrationsIds = |
| registrations.map((registration) => registration.id).toSet(); |
| |
| var numberOfReregistrations = 10; |
| |
| // Listen to incoming registrations, ensure they're not in the set, and add |
| // them. |
| var registrationsDone = requestsFromServer |
| .where((n) => n.method == Method.client_registerCapability) |
| .take(numberOfReregistrations) |
| .listen((request) { |
| respondTo(request, null); |
| var registrations = |
| RegistrationParams.fromJson(request.params as Map<String, Object?>) |
| .registrations; |
| for (var registration in registrations) { |
| var id = registration.id; |
| if (!knownRegistrationsIds.add(id)) { |
| throw 'Registration $id was already in the existing set!'; |
| } |
| } |
| }).asFuture(); |
| |
| // Listen to incoming unregistrations, verify they're in the set, and remove |
| // them. |
| var unregistrationsDone = requestsFromServer |
| .where((n) => n.method == Method.client_unregisterCapability) |
| .take(numberOfReregistrations) |
| .listen((request) { |
| respondTo(request, null); |
| var unregistrations = |
| UnregistrationParams.fromJson(request.params as Map<String, Object?>) |
| .unregisterations; |
| for (var unregistration in unregistrations) { |
| var id = unregistration.id; |
| if (!knownRegistrationsIds.remove(id)) { |
| throw 'Registration $id was not in the existing set!'; |
| } |
| } |
| }).asFuture(); |
| |
| // Trigger multiple plugin events that will rebuild the registrations. |
| for (var i = 0; i < numberOfReregistrations; i++) { |
| var plugin = configureTestPlugin(); |
| plugin.currentSession = PluginSession(plugin); |
| // Ensure they have different file types so the registrations change, |
| // otherwise they will be optimised out as not changing. |
| plugin.currentSession!.interestingFiles = ['*.foo$i']; |
| pluginManager.pluginsChangedController.add(null); |
| await null; // Allow the server to begin processing the change. |
| } |
| |
| // Wait for both streams to have handled all of the numberOfReregistrations |
| // expected events (without throwing). |
| await Future.wait([registrationsDone, unregistrationsDone]); |
| } |
| |
| Future<void> test_dynamicRegistration_config_allHierarchy() => |
| assertDynamicRegistration( |
| 'callHierarchy', {Method.textDocument_prepareCallHierarchy}); |
| |
| Future<void> test_dynamicRegistration_config_codeAction() => |
| assertDynamicRegistration('codeAction', {Method.textDocument_codeAction}); |
| |
| Future<void> test_dynamicRegistration_config_codeLens() => |
| assertDynamicRegistration('codeLens', {Method.textDocument_codeLens}); |
| |
| Future<void> test_dynamicRegistration_config_colorProvider() => |
| assertDynamicRegistration( |
| 'colorProvider', {Method.textDocument_documentColor}); |
| |
| Future<void> test_dynamicRegistration_config_completion() => |
| assertDynamicRegistration('completion', {Method.textDocument_completion}); |
| |
| Future<void> test_dynamicRegistration_config_definition() => |
| assertDynamicRegistration('definition', {Method.textDocument_definition}); |
| |
| Future<void> test_dynamicRegistration_config_didChangeConfiguration() => |
| assertDynamicRegistration( |
| 'didChangeConfiguration', {Method.workspace_didChangeConfiguration}); |
| |
| Future<void> test_dynamicRegistration_config_documentHighlight() => |
| assertDynamicRegistration( |
| 'documentHighlight', {Method.textDocument_documentHighlight}); |
| |
| Future<void> test_dynamicRegistration_config_documentLink() => |
| assertDynamicRegistration( |
| 'documentLink', {Method.textDocument_documentLink}); |
| |
| Future<void> test_dynamicRegistration_config_documentSymbol() => |
| assertDynamicRegistration( |
| 'documentSymbol', {Method.textDocument_documentSymbol}); |
| |
| Future<void> test_dynamicRegistration_config_fileOperations() => |
| assertDynamicRegistration( |
| 'fileOperations', {Method.workspace_willRenameFiles}); |
| |
| Future<void> test_dynamicRegistration_config_foldingRange() => |
| assertDynamicRegistration( |
| 'foldingRange', {Method.textDocument_foldingRange}); |
| |
| Future<void> test_dynamicRegistration_config_formatting() => |
| assertDynamicRegistration('formatting', {Method.textDocument_formatting}); |
| |
| Future<void> test_dynamicRegistration_config_hover() => |
| assertDynamicRegistration('hover', {Method.textDocument_hover}); |
| |
| Future<void> test_dynamicRegistration_config_implementation() => |
| assertDynamicRegistration( |
| 'implementation', {Method.textDocument_implementation}); |
| |
| Future<void> test_dynamicRegistration_config_inlayHint() => |
| assertDynamicRegistration('inlayHint', {Method.textDocument_inlayHint}); |
| |
| Future<void> test_dynamicRegistration_config_onTypeFormatting() => |
| assertDynamicRegistration( |
| 'onTypeFormatting', {Method.textDocument_onTypeFormatting}); |
| |
| Future<void> test_dynamicRegistration_config_rangeFormatting() => |
| assertDynamicRegistration( |
| 'rangeFormatting', {Method.textDocument_rangeFormatting}); |
| |
| Future<void> test_dynamicRegistration_config_references() => |
| assertDynamicRegistration('references', {Method.textDocument_references}); |
| |
| Future<void> test_dynamicRegistration_config_rename() => |
| assertDynamicRegistration('rename', {Method.textDocument_rename}); |
| |
| Future<void> test_dynamicRegistration_config_selectionRange() => |
| assertDynamicRegistration( |
| 'selectionRange', {Method.textDocument_selectionRange}); |
| |
| Future<void> test_dynamicRegistration_config_semanticTokens() => |
| assertDynamicRegistration( |
| 'semanticTokens', {CustomMethods.semanticTokenDynamicRegistration}); |
| |
| Future<void> test_dynamicRegistration_config_signatureHelp() => |
| assertDynamicRegistration( |
| 'signatureHelp', {Method.textDocument_signatureHelp}); |
| |
| Future<void> test_dynamicRegistration_config_synchronization() => |
| assertDynamicRegistration('synchronization', { |
| Method.textDocument_didOpen, |
| Method.textDocument_didChange, |
| Method.textDocument_didClose |
| }); |
| |
| Future<void> test_dynamicRegistration_config_typeDefinition() => |
| assertDynamicRegistration( |
| 'typeDefinition', {Method.textDocument_typeDefinition}); |
| |
| Future<void> test_dynamicRegistration_config_typeHierarchy() => |
| assertDynamicRegistration( |
| 'typeHierarchy', {Method.textDocument_prepareTypeHierarchy}); |
| |
| Future<void> test_dynamicRegistration_containsAppropriateSettings() async { |
| // Support file operations. |
| setFileOperationDynamicRegistration(); |
| // Support dynamic registration for both text sync + hovers. |
| setTextSyncDynamicRegistration(); |
| setHoverDynamicRegistration(); |
| |
| // Basic check that the server responds with the capabilities we'd expect, |
| // for ex including analysis_options.yaml in text synchronization but not |
| // for hovers. |
| var registrations = <Registration>[]; |
| var initResponse = await monitorDynamicRegistrations( |
| registrations, |
| initialize, |
| ); |
| |
| // Because we support dynamic registration for synchronization, we won't send |
| // static registrations for them. |
| // https://github.com/dart-lang/sdk/issues/38490 |
| var initResult = |
| InitializeResult.fromJson(initResponse.result as Map<String, Object?>); |
| expect(initResult.serverInfo!.name, 'Dart SDK LSP Analysis Server'); |
| expect(initResult.serverInfo!.version, isNotNull); |
| expect(initResult.capabilities, isNotNull); |
| expect(initResult.capabilities.textDocumentSync, isNull); |
| |
| // Should contain Hover, DidOpen, DidClose, DidChange, WillRenameFiles. |
| expect(registrations, hasLength(5)); |
| var hover = |
| registrationOptionsFor(registrations, Method.textDocument_hover); |
| var change = |
| registrationOptionsFor(registrations, Method.textDocument_didChange); |
| var rename = FileOperationRegistrationOptions.fromJson( |
| registrationFor(registrations, Method.workspace_willRenameFiles) |
| ?.registerOptions as Map<String, Object?>); |
| expect(registrationOptionsFor(registrations, Method.textDocument_didOpen), |
| isNotNull); |
| expect(registrationOptionsFor(registrations, Method.textDocument_didClose), |
| isNotNull); |
| |
| // The hover capability should only specific Dart. |
| expect(hover, isNotNull); |
| expect(hover.documentSelector, hasLength(1)); |
| expect(hover.documentSelector!.single.language, equals('dart')); |
| |
| // didChange should also include pubspec + analysis_options. |
| expect(change, isNotNull); |
| expect(change.documentSelector, hasLength(greaterThanOrEqualTo(3))); |
| expect(change.documentSelector!.any((ds) => ds.language == 'dart'), isTrue); |
| expect( |
| change.documentSelector!.any((ds) => ds.pattern == '**/pubspec.yaml'), |
| isTrue); |
| expect( |
| change.documentSelector! |
| .any((ds) => ds.pattern == '**/analysis_options.yaml'), |
| isTrue); |
| |
| expect(rename, equals(fileOperationRegistrationOptions)); |
| } |
| |
| Future<void> test_dynamicRegistration_notSupportedByClient() async { |
| // If the client doesn't send any dynamicRegistration settings then there |
| // should be no `client/registerCapability` calls. |
| |
| // Set a flag if any registerCapability request comes through. |
| var didGetRegisterCapabilityRequest = false; |
| requestsFromServer |
| .where((n) => n.method == Method.client_registerCapability) |
| .listen((_) => didGetRegisterCapabilityRequest = true); |
| |
| // Initialize with no dynamic registrations advertised. |
| var initResponse = await initialize(); |
| await pumpEventQueue(); |
| |
| var initResult = |
| InitializeResult.fromJson(initResponse.result as Map<String, Object?>); |
| expect(initResult.capabilities, isNotNull); |
| // When dynamic registration is not supported, we will always statically |
| // request text document open/close and incremental updates. |
| expect(initResult.capabilities.textDocumentSync, isNotNull); |
| initResult.capabilities.textDocumentSync!.map( |
| (_) => |
| throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions', |
| (options) { |
| expect(options.openClose, isTrue); |
| expect(options.change, equals(TextDocumentSyncKind.Incremental)); |
| }, |
| ); |
| expect(initResult.capabilities.callHierarchyProvider, isNotNull); |
| expect(initResult.capabilities.completionProvider, isNotNull); |
| expect(initResult.capabilities.hoverProvider, isNotNull); |
| expect(initResult.capabilities.signatureHelpProvider, isNotNull); |
| expect(initResult.capabilities.referencesProvider, isNotNull); |
| expect(initResult.capabilities.colorProvider, isNotNull); |
| expect(initResult.capabilities.documentHighlightProvider, isNotNull); |
| expect(initResult.capabilities.documentFormattingProvider, isNotNull); |
| expect(initResult.capabilities.documentOnTypeFormattingProvider, isNotNull); |
| expect(initResult.capabilities.documentRangeFormattingProvider, isNotNull); |
| expect(initResult.capabilities.definitionProvider, isNotNull); |
| expect(initResult.capabilities.codeActionProvider, isNotNull); |
| expect(initResult.capabilities.renameProvider, isNotNull); |
| expect(initResult.capabilities.foldingRangeProvider, isNotNull); |
| expect(initResult.capabilities.workspace!.fileOperations!.willRename, |
| equals(fileOperationRegistrationOptions)); |
| expect(initResult.capabilities.selectionRangeProvider, isNotNull); |
| expect(initResult.capabilities.semanticTokensProvider, isNotNull); |
| |
| expect(didGetRegisterCapabilityRequest, isFalse); |
| } |
| |
| Future<void> test_dynamicRegistration_suppressesStaticRegistration() async { |
| // Support dynamic registration for everything we support. |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| setAllSupportedWorkspaceDynamicRegistrations(); |
| |
| // If the client sends dynamicRegistration settings then there |
| // should not be static registrations for the same capabilities. |
| var registrations = <Registration>[]; |
| var initResponse = await monitorDynamicRegistrations( |
| registrations, |
| initialize, |
| ); |
| |
| var initResult = |
| InitializeResult.fromJson(initResponse.result as Map<String, Object?>); |
| expect(initResult.capabilities, isNotNull); |
| |
| // Ensure no static registrations. This list should include all server equivalents |
| // of the dynamic registrations listed in `ClientDynamicRegistrations.supported`. |
| expect(initResult.capabilities.textDocumentSync, isNull); |
| expect(initResult.capabilities.callHierarchyProvider, isNull); |
| expect(initResult.capabilities.completionProvider, isNull); |
| expect(initResult.capabilities.hoverProvider, isNull); |
| expect(initResult.capabilities.inlayHintProvider, isNull); |
| expect(initResult.capabilities.signatureHelpProvider, isNull); |
| expect(initResult.capabilities.referencesProvider, isNull); |
| expect(initResult.capabilities.colorProvider, isNull); |
| expect(initResult.capabilities.documentHighlightProvider, isNull); |
| expect(initResult.capabilities.documentFormattingProvider, isNull); |
| expect(initResult.capabilities.documentOnTypeFormattingProvider, isNull); |
| expect(initResult.capabilities.documentRangeFormattingProvider, isNull); |
| expect(initResult.capabilities.definitionProvider, isNull); |
| expect(initResult.capabilities.codeActionProvider, isNull); |
| expect(initResult.capabilities.renameProvider, isNull); |
| expect(initResult.capabilities.foldingRangeProvider, isNull); |
| expect(initResult.capabilities.workspace!.fileOperations, isNull); |
| expect(initResult.capabilities.selectionRangeProvider, isNull); |
| expect(initResult.capabilities.semanticTokensProvider, isNull); |
| |
| // Ensure all expected dynamic registrations. |
| for (var expectedRegistration in ClientDynamicRegistrations.supported) { |
| // We have two completion registrations (to handle different trigger |
| // characters), so exclude that here and check it manually below. |
| if (expectedRegistration == Method.textDocument_completion) { |
| continue; |
| } |
| |
| var registration = |
| registrationOptionsFor(registrations, expectedRegistration); |
| expect(registration, isNotNull, |
| reason: 'Missing dynamic registration for $expectedRegistration'); |
| } |
| |
| // Check the were two completion registrations. |
| var completionRegistrations = registrations |
| .where((reg) => reg.method == Method.textDocument_completion.toJson()); |
| expect(completionRegistrations, hasLength(2)); |
| } |
| |
| Future<void> test_dynamicRegistration_unregistersOutdatedAfterChange() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| |
| // Initialize by supporting dynamic registrations everywhere |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| |
| var registrations = <Registration>[]; |
| await monitorDynamicRegistrations( |
| registrations, |
| initialize, |
| ); |
| |
| var unregisterRequest = |
| await expectRequest(Method.client_unregisterCapability, () { |
| var plugin = configureTestPlugin(); |
| plugin.currentSession = PluginSession(plugin) |
| ..interestingFiles = ['*.foo']; |
| pluginManager.pluginsChangedController.add(null); |
| }); |
| var unregistrations = UnregistrationParams.fromJson( |
| unregisterRequest.params as Map<String, Object?>) |
| .unregisterations; |
| |
| // folding method should have been unregistered as the server now supports |
| // *.foo files for it as well. |
| var registrationIdForFolding = registrations |
| .singleWhere((r) => r.method == 'textDocument/foldingRange') |
| .id; |
| expect( |
| unregistrations, |
| contains(isA<Unregistration>() |
| .having((r) => r.method, 'method', 'textDocument/foldingRange') |
| .having((r) => r.id, 'id', registrationIdForFolding)), |
| ); |
| } |
| |
| Future<void> test_dynamicRegistration_updatesWithPlugins() async { |
| if (!AnalysisServer.supportsPlugins) return; |
| setTextDocumentDynamicRegistration('foldingRange'); |
| await initialize(); |
| |
| // The server will send an unregister request followed by another register |
| // request to change document filter on folding. We need to respond to the |
| // unregister request as the server awaits that. |
| // This is set up as a future callback and should not be awaited here. |
| unawaited(requestsFromServer |
| .firstWhere((r) => r.method == Method.client_unregisterCapability) |
| .then((request) { |
| respondTo(request, null); |
| })); |
| |
| var request = await expectRequest(Method.client_registerCapability, () { |
| var plugin = configureTestPlugin(); |
| plugin.currentSession = PluginSession(plugin) |
| ..interestingFiles = ['*.sql']; |
| pluginManager.pluginsChangedController.add(null); |
| }); |
| |
| var registrations = |
| RegistrationParams.fromJson(request.params as Map<String, Object?>) |
| .registrations; |
| |
| var documentFilterSql = |
| TextDocumentFilterWithScheme(scheme: 'file', pattern: '**/*.sql'); |
| var documentFilterDart = |
| TextDocumentFilterWithScheme(language: 'dart', scheme: 'file'); |
| |
| expect( |
| registrations, |
| contains(isA<Registration>() |
| .having((r) => r.method, 'method', 'textDocument/foldingRange') |
| .having( |
| (r) => TextDocumentRegistrationOptions.fromJson( |
| r.registerOptions as Map<String, Object?>) |
| .documentSelector, |
| 'registerOptions.documentSelector', |
| containsAll([documentFilterSql, documentFilterDart]), |
| )), |
| ); |
| } |
| |
| /// Tests that when there are no explicit analysis roots (instead they are |
| /// implied by open files), requests for open files are successful even if |
| /// sent _immediately_ after opening the file. |
| /// |
| /// https://github.com/Dart-Code/Dart-Code/issues/3929 |
| /// |
| /// This test uses getDocumentSymbols which requires a resolved result. |
| Future<void> |
| test_emptyAnalysisRoots_handlesFileRequestsImmediately_resolved() async { |
| const content = 'void f() {}'; |
| var file1 = join(projectFolderPath, 'file1.dart'); |
| var file1Uri = pathContext.toUri(file1); |
| newFile(file1, content); |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| await initialize(allowEmptyRootUri: true); |
| |
| unawaited(openFile(file1Uri, content)); // Don't wait |
| var result = await getDocumentSymbols(file1Uri); |
| var symbols = result.map( |
| (docSymbols) => docSymbols, |
| (symbolInfos) => symbolInfos, |
| ); |
| expect(symbols, hasLength(1)); |
| } |
| |
| /// Tests that when there are no explicit analysis roots (instead they are |
| /// implied by open files), requests for open files are successful even if |
| /// sent _immediately_ after opening the file. |
| /// |
| /// https://github.com/Dart-Code/Dart-Code/issues/3929 |
| /// |
| /// This test uses getSelectionRanges which requires only a parsed result. |
| Future<void> |
| test_emptyAnalysisRoots_handlesFileRequestsImmediately_unresolved() async { |
| const content = 'void f() {}'; |
| var file1 = join(projectFolderPath, 'file1.dart'); |
| var file1Uri = pathContext.toUri(file1); |
| newFile(file1, content); |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| await initialize(allowEmptyRootUri: true); |
| |
| unawaited(openFile(file1Uri, content)); // Don't wait |
| var result = await getSelectionRanges(file1Uri, [startOfDocPos]); |
| expect(result, hasLength(1)); |
| } |
| |
| Future<void> test_emptyAnalysisRoots_multipleOpenFiles() async { |
| var file1 = join(projectFolderPath, 'file1.dart'); |
| var file1Uri = pathContext.toUri(file1); |
| newFile(file1, ''); |
| var file2 = join(projectFolderPath, 'file2.dart'); |
| var file2Uri = pathContext.toUri(file2); |
| newFile(file2, ''); |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| await initialize(allowEmptyRootUri: true); |
| |
| // Opening both files should only add the project folder once. |
| await openFile(file1Uri, ''); |
| await openFile(file2Uri, ''); |
| await contextRebuildComplete; |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| |
| // Closing only one of the files should not remove the project folder |
| // since there are still open files. |
| resetContextBuildCounter(); |
| await closeFile(file1Uri); |
| await pumpEventQueue(times: 5000); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expectNoContextBuilds(); |
| |
| // Closing the last file should remove the project folder and remove |
| // the context. |
| resetContextBuildCounter(); |
| await closeFile(file2Uri); |
| await contextRebuildComplete; |
| expect(server.contextManager.includedPaths, equals([])); |
| expect(server.contextManager.driverMap, hasLength(0)); |
| expectContextBuilds(); |
| } |
| |
| Future<void> test_emptyAnalysisRoots_projectWithoutPubspec() async { |
| projectFolderPath = convertPath('/home/empty'); |
| var nestedFilePath = join( |
| projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart'); |
| var nestedFileUri = pathContext.toUri(nestedFilePath); |
| newFile(nestedFilePath, ''); |
| |
| // The project folder shouldn't be added to start with. |
| await initialize(allowEmptyRootUri: true); |
| expect(server.contextManager.includedPaths, equals([])); |
| |
| // Opening the file will add a root for it. |
| await openFile(nestedFileUri, ''); |
| expect(server.contextManager.includedPaths, equals([nestedFilePath])); |
| } |
| |
| Future<void> test_emptyAnalysisRoots_projectWithPubspec() async { |
| var nestedFilePath = join( |
| projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart'); |
| var nestedFileUri = pathContext.toUri(nestedFilePath); |
| newFile(nestedFilePath, ''); |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| // The project folder shouldn't be added to start with. |
| await initialize(allowEmptyRootUri: true); |
| expect(server.contextManager.includedPaths, equals([])); |
| |
| // Opening a file nested within the project should add the project folder. |
| await openFile(nestedFileUri, ''); |
| await contextRebuildComplete; |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| |
| // Ensure the file was cached in each driver. This happens as a result of |
| // adding to priority files, but if that's done before the file is in an |
| // analysis root it will not occur. |
| // https://github.com/dart-lang/sdk/issues/37338 |
| for (var driver in server.driverMap.values) { |
| expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull); |
| } |
| |
| // Closing the file should remove it. |
| await closeFile(nestedFileUri); |
| expect(server.contextManager.includedPaths, equals([])); |
| } |
| |
| Future<void> test_excludedFolders_absolute() async { |
| var excludedFolderPath = join(projectFolderPath, 'excluded'); |
| |
| await provideConfig( |
| initialize, |
| // Exclude the folder with a relative path. |
| { |
| 'analysisExcludedFolders': [excludedFolderPath] |
| }, |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); |
| } |
| |
| Future<void> test_excludedFolders_nonList() async { |
| var excludedFolderPath = join(projectFolderPath, 'excluded'); |
| |
| await provideConfig( |
| initialize, |
| // Include a single string instead of an array since it's an easy mistake |
| // to make without editor validation of settings. |
| {'analysisExcludedFolders': 'excluded'}, |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); |
| } |
| |
| Future<void> test_excludedFolders_relative() async { |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); |
| |
| await provideConfig( |
| initialize, |
| // Exclude the folder with a relative path. |
| { |
| 'analysisExcludedFolders': ['aaa', 'bbb'].join(pathContext.separator) |
| }, |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); |
| } |
| |
| /// Check exclusions using explicit posix paths even if we're running |
| /// on Windows. It's common for shared configs in VS Code to always use |
| /// forward slashes in paths. |
| Future<void> test_excludedFolders_relative_posix() async { |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); |
| |
| await provideConfig( |
| initialize, |
| // Exclude the folder with a relative path using forward slashes. |
| { |
| 'analysisExcludedFolders': ['aaa', 'bbb'].join('/') |
| }, |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); |
| } |
| |
| Future<void> test_excludedFolders_trailingSlash() async { |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); |
| |
| await provideConfig( |
| initialize, |
| // Exclude the folder with a trailing slash. |
| { |
| 'analysisExcludedFolders': |
| ['aaa', 'bbb', ''].join(pathContext.separator), |
| }, |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); |
| } |
| |
| /// Check exclusions using explicit posix paths even if we're running |
| /// on Windows. It's common for shared configs in VS Code to always use |
| /// forward slashes in paths. |
| Future<void> test_excludedFolders_trailingSlash_posix() async { |
| var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb'); |
| |
| await provideConfig( |
| initialize, |
| // Exclude the folder with a forward trailing slash. |
| { |
| 'analysisExcludedFolders': ['aaa', 'bbb', ''].join('/'), |
| }, |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.excludedPaths, equals([excludedFolderPath])); |
| } |
| |
| /// Tests that requests that requires a unit result are handled correctly even |
| /// if sent immediately after the `initialized` notification and do not result |
| /// in "File not Analyzed"-style errors because roots are set asynchronously. |
| Future<void> test_immediateRequests() async { |
| newFile(mainFilePath, 'void f() {}'); |
| late Future<Either2<List<DocumentSymbol>, List<SymbolInformation>>> result; |
| await provideConfig( |
| () => initialize( |
| immediatelyAfterInitialized: () { |
| result = getDocumentSymbols(mainFileUri); |
| }, |
| ), |
| {}, |
| ); |
| |
| var symbols = (await result).map( |
| (docSymbols) => docSymbols, |
| (symbolInfos) => symbolInfos, |
| ); |
| expect(symbols, hasLength(1)); |
| } |
| |
| Future<void> test_initialize() async { |
| var response = await initialize(); |
| expect(response, isNotNull); |
| expect(response.error, isNull); |
| expect(response.result, isNotNull); |
| expect(InitializeResult.canParse(response.result, nullLspJsonReporter), |
| isTrue); |
| var result = |
| InitializeResult.fromJson(response.result as Map<String, Object?>); |
| expect(result.capabilities, isNotNull); |
| // Check some basic capabilities that are unlikely to change. |
| expect(result.capabilities.textDocumentSync, isNotNull); |
| result.capabilities.textDocumentSync!.map( |
| (_) => |
| throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions', |
| (options) { |
| // We'll always request open/closed notifications and incremental updates. |
| expect(options.openClose, isTrue); |
| expect(options.change, equals(TextDocumentSyncKind.Incremental)); |
| }, |
| ); |
| } |
| |
| Future<void> test_initialize_invalidParams() async { |
| var params = {'processId': 'invalid'}; |
| var request = RequestMessage( |
| id: Either2<int, String>.t1(1), |
| method: Method.initialize, |
| params: params, |
| jsonrpc: jsonRpcVersion, |
| ); |
| var response = await sendRequestToServer(request); |
| expect(response.id, equals(request.id)); |
| expect(response.error, isNotNull); |
| expect(response.error!.code, equals(ErrorCodes.InvalidParams)); |
| expect(response.result, isNull); |
| } |
| |
| Future<void> test_initialize_onlyAllowedOnce() async { |
| await initialize(); |
| var response = await initialize(throwOnFailure: false); |
| expect(response, isNotNull); |
| expect(response.result, isNull); |
| expect(response.error, isNotNull); |
| expect(response.error!.code, |
| equals(ServerErrorCodes.ServerAlreadyInitialized)); |
| } |
| |
| Future<void> test_initialize_rootPath() async { |
| await initialize(rootPath: projectFolderPath); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_initialize_rootPath_trailingSlash() async { |
| await initialize(rootPath: withTrailingSlash(projectFolderPath)); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_initialize_rootUri() async { |
| await initialize(rootUri: projectFolderUri); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_initialize_rootUri_encodedDriveLetterColon() async { |
| await initialize(rootUri: withEncodedDriveLetterColon(projectFolderUri)); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_initialize_rootUri_trailingSlash() async { |
| await initialize(rootUri: withTrailingSlashUri(projectFolderUri)); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_initialize_workspaceFolders() async { |
| await initialize(workspaceFolders: [projectFolderUri]); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> |
| test_initialize_workspaceFolders_encodedDriveLetterColon() async { |
| await initialize( |
| workspaceFolders: [withEncodedDriveLetterColon(projectFolderUri)]); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_initialize_workspaceFolders_trailingSlash() async { |
| await initialize( |
| workspaceFolders: [withTrailingSlashUri(projectFolderUri)]); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_invalidExperimental_commands() async { |
| await expectInvalidExperimentalParams( |
| {'commands': 1}, |
| 'ClientCapabilities.experimental.commands must be a List<String>?', |
| ); |
| } |
| |
| Future<void> test_invalidExperimental_dartCodeAction() async { |
| await expectInvalidExperimentalParams( |
| {'dartCodeAction': 1}, |
| 'ClientCapabilities.experimental.dartCodeAction must be a Map<String, Object?>?', |
| ); |
| } |
| |
| Future<void> |
| test_invalidExperimental_dartCodeAction_commandParameterSupport() async { |
| await expectInvalidExperimentalParams( |
| { |
| 'dartCodeAction': {'commandParameterSupport': 1} |
| }, |
| 'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport must be a Map<String, Object?>?', |
| ); |
| } |
| |
| Future<void> |
| test_invalidExperimental_dartCodeAction_commandParameterSupport_supportedKinds() async { |
| await expectInvalidExperimentalParams( |
| { |
| 'dartCodeAction': { |
| 'commandParameterSupport': {'supportedKinds': 1} |
| } |
| }, |
| 'ClientCapabilities.experimental.dartCodeAction.commandParameterSupport.supportedKinds must be a List<String>?', |
| ); |
| } |
| |
| Future<void> test_invalidExperimental_snippetTextEdit() async { |
| await expectInvalidExperimentalParams( |
| {'snippetTextEdit': 1}, |
| 'ClientCapabilities.experimental.snippetTextEdit must be a bool?', |
| ); |
| } |
| |
| Future<void> |
| test_invalidExperimental_supportsDartTextDocumentContentProvider() async { |
| await expectInvalidExperimentalParams( |
| {'supportsDartTextDocumentContentProvider': 1}, |
| 'ClientCapabilities.experimental.supportsDartTextDocumentContentProvider must be a bool?', |
| ); |
| } |
| |
| Future<void> test_nonFileScheme_rootUri() async { |
| // We expect an error notification about the invalid file we try to open. |
| failTestOnAnyErrorNotification = false; |
| |
| var rootUri = Uri.parse('vsls://'); |
| var fileUri = rootUri.replace(path: '/file1.dart'); |
| |
| await initialize(rootUri: rootUri); |
| expect(server.contextManager.includedPaths, equals([])); |
| |
| // Also open a non-file file to ensure it doesn't cause the root to be added. |
| await openFile(fileUri, ''); |
| expect(server.contextManager.includedPaths, equals([])); |
| } |
| |
| /// Verifies a workspace that contains non-file workspace folders is handled. |
| /// |
| /// Related tests for didChangeWorkspaceFolders are in |
| /// [ChangeWorkspaceFoldersTest]. |
| Future<void> test_nonFileScheme_workspaceFolders() async { |
| // We expect an error notification about the invalid file we try to open. |
| failTestOnAnyErrorNotification = false; |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| var rootUri = Uri.parse('vsls://'); |
| var fileUri = rootUri.replace(path: '/file1.dart'); |
| |
| await initialize( |
| workspaceFolders: [ |
| rootUri, |
| pathContext.toUri(projectFolderPath), |
| ], |
| ); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| |
| // Also open a non-file file to ensure it doesn't cause the root to be added. |
| await openFile(fileUri, ''); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| } |
| |
| Future<void> test_nonProjectFiles_basicWorkspace() async { |
| var file1 = convertPath('/home/nonProject/file1.dart'); |
| newFile(file1, ''); |
| |
| await initialize(allowEmptyRootUri: true); |
| |
| // Because the file is not in a project, it should be added itself. |
| await openFile(pathContext.toUri(file1), ''); |
| expect(server.contextManager.includedPaths, equals([file1])); |
| } |
| |
| Future<void> test_nonProjectFiles_blazeWorkspace() async { |
| var file1 = convertPath('/home/nonProject/file1.dart'); |
| newFile(file1, ''); |
| |
| // Make /home a Blaze workspace. |
| newFile('/home/${file_paths.blazeWorkspaceMarker}', ''); |
| |
| await initialize(allowEmptyRootUri: true); |
| |
| // Because the file is not in a project, it should be added itself. |
| await openFile(pathContext.toUri(file1), ''); |
| expect(server.contextManager.includedPaths, equals([file1])); |
| } |
| |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_fullyInitializes() async { |
| // Enable some dynamic registrations, else registerCapability will not |
| // be called. |
| setAllSupportedTextDocumentDynamicRegistrations(); |
| |
| // Ensure when we use onlyAnalyzeProjectsWithOpenFiles that we still |
| // fully initialize (eg. capabilities are registered). |
| projectFolderPath = convertPath('/home/empty'); |
| |
| await expectRequest( |
| Method.client_registerCapability, |
| () => initialize( |
| rootUri: projectFolderUri, |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, |
| ), |
| ); |
| } |
| |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_multipleFiles() async { |
| var file1 = join(projectFolderPath, 'file1.dart'); |
| var file1Uri = pathContext.toUri(file1); |
| newFile(file1, ''); |
| var file2 = join(projectFolderPath, 'file2.dart'); |
| var file2Uri = pathContext.toUri(file2); |
| newFile(file2, ''); |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| await initialize( |
| rootUri: projectFolderUri, |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, |
| ); |
| |
| // Opening both files should only add the project folder once. |
| await openFile(file1Uri, ''); |
| await openFile(file2Uri, ''); |
| await contextRebuildComplete; |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.driverMap, hasLength(1)); |
| |
| // Closing only one of the files should not remove the root or rebuild the context. |
| resetContextBuildCounter(); |
| await closeFile(file1Uri); |
| await pumpEventQueue(times: 5000); |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| expect(server.contextManager.driverMap, hasLength(1)); |
| expectNoContextBuilds(); |
| |
| // Closing the last file should remove the project folder and remove |
| // the context. |
| resetContextBuildCounter(); |
| await closeFile(file2Uri); |
| await contextRebuildComplete; |
| expect(server.contextManager.includedPaths, equals([])); |
| expect(server.contextManager.driverMap, hasLength(0)); |
| expectContextBuilds(); |
| } |
| |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withoutPubpsec() async { |
| projectFolderPath = convertPath('/home/empty'); |
| var nestedFilePath = join( |
| projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart'); |
| var nestedFileUri = pathContext.toUri(nestedFilePath); |
| newFile(nestedFilePath, ''); |
| |
| // The project folder shouldn't be added to start with. |
| await initialize( |
| rootUri: projectFolderUri, |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, |
| ); |
| expect(server.contextManager.includedPaths, equals([])); |
| |
| // Opening the file should trigger it to be added. |
| await openFile(nestedFileUri, ''); |
| expect(server.contextManager.includedPaths, equals([nestedFilePath])); |
| } |
| |
| Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withPubpsec() async { |
| var nestedFilePath = join( |
| projectFolderPath, 'nested', 'deeply', 'in', 'folders', 'test.dart'); |
| var nestedFileUri = pathContext.toUri(nestedFilePath); |
| newFile(nestedFilePath, ''); |
| newPubspecYamlFile(projectFolderPath, ''); |
| |
| // The project folder shouldn't be added to start with. |
| await initialize( |
| rootUri: projectFolderUri, |
| initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true}, |
| ); |
| expect(server.contextManager.includedPaths, equals([])); |
| |
| // Opening a file nested within the project should cause the project folder |
| // to be added |
| await openFile(nestedFileUri, ''); |
| await contextRebuildComplete; |
| expect(server.contextManager.includedPaths, equals([projectFolderPath])); |
| |
| // Ensure the file was cached in each driver. This happens as a result of |
| // adding to priority files, but if that's done before the file is in an |
| // analysis root it will not occur. |
| // https://github.com/dart-lang/sdk/issues/37338 |
| for (var driver in server.driverMap.values) { |
| expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull); |
| } |
| |
| // Closing the file should remove it. |
| await closeFile(nestedFileUri); |
| expect(server.contextManager.includedPaths, equals([])); |
| } |
| |
| Future<void> test_survey_alwaysEnabledForLsp() async { |
| await initialize(); |
| expect(server.surveyManager, isNotNull); |
| } |
| |
| Future<void> test_uninitialized_dropsNotifications() async { |
| var notification = |
| makeNotification(Method.fromJson('randomNotification'), null); |
| var nextNotification = errorNotificationsFromServer.first; |
| channel.sendNotificationToServer(notification); |
| |
| // Wait up to 1sec to ensure no error/log notifications were sent back. |
| var didTimeout = false; |
| var notificationFromServer = await nextNotification |
| .then<NotificationMessage?>((notification) => notification) |
| .timeout( |
| const Duration(seconds: 1), |
| onTimeout: () { |
| didTimeout = true; |
| return null; |
| }, |
| ); |
| |
| expect(notificationFromServer, isNull); |
| expect(didTimeout, isTrue); |
| } |
| |
| Future<void> test_uninitialized_rejectsRequests() async { |
| var request = makeRequest(Method.fromJson('randomRequest'), null); |
| var response = await channel.sendRequestToServer(request); |
| expect(response.id, equals(request.id)); |
| expect(response.result, isNull); |
| expect(response.error, isNotNull); |
| expect(response.error!.code, ErrorCodes.ServerNotInitialized); |
| } |
| } |
| |
| /// Runs all initialization tests with a resource provider that slowly |
| /// initializes watchers to simulate delays in real fs watchers. |
| @reflectiveTest |
| class SlowInitializationTest extends InitializationTest { |
| @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), |
| ); |
| } |