// 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 'dart:async'; 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/analysis_server.dart'; import 'package:analysis_server/src/domain_analysis.dart'; import 'package:analysis_server/src/server/crash_reporting_attachments.dart'; import 'package:analysis_server/src/utilities/mocks.dart'; import 'package:analyzer/instrumentation/instrumentation.dart'; import 'package:analyzer/src/generated/sdk.dart'; import 'package:analyzer/src/test_utilities/mock_sdk.dart'; import 'package:analyzer/src/test_utilities/package_config_file_builder.dart'; import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'analysis_abstract.dart'; import 'mocks.dart'; void main() { defineReflectiveSuite(() { defineReflectiveTests(AnalysisDomainTest); defineReflectiveTests(AnalysisDomainHandlerTest); defineReflectiveTests(SetSubscriptionsTest); }); } @reflectiveTest class AnalysisDomainHandlerTest extends AbstractAnalysisTest { Future outOfRangeTest(SourceEdit edit) async { var helper = AnalysisTestHelper(); helper.createSingleFileProject('library A;'); await helper.onAnalysisComplete; helper.sendContentChange(AddContentOverlay('library B;')); await helper.onAnalysisComplete; var contentChange = ChangeContentOverlay([edit]); var request = AnalysisUpdateContentParams({helper.testFile: contentChange}) .toRequest('0'); var response = helper.handler.handleRequest(request); expect(response, isResponseFailure('0', RequestErrorCode.INVALID_OVERLAY_CHANGE)); } Future test_setAnalysisRoots_excludedFolder() async { newFile('/project/aaa/a.dart', content: '// a'); newFile('/project/bbb/b.dart', content: '// b'); var excludedPath = join(projectPath, 'bbb'); var response = testSetAnalysisRoots([projectPath], [excludedPath]); expect(response, isResponseSuccess('0')); } Future test_setAnalysisRoots_included_newFolder() async { newPubspecYamlFile('/project', 'name: project'); var file = newFile('/project/bin/test.dart', content: 'main() {}').path; var response = testSetAnalysisRoots([projectPath], []); var serverRef = server; expect(response, isResponseSuccess('0')); // verify that unit is resolved eventually await server.onAnalysisComplete; var resolvedUnit = await serverRef.getResolvedUnit(file); expect(resolvedUnit, isNotNull); } Future test_setAnalysisRoots_included_nonexistentFolder() async { var projectA = convertPath('/project_a'); var projectB = convertPath('/project_b'); var fileB = newFile('/project_b/b.dart', content: '// b').path; var response = testSetAnalysisRoots([projectA, projectB], []); var serverRef = server; expect(response, isResponseSuccess('0')); // Non-existence of /project_a should not prevent files in /project_b // from being analyzed. await server.onAnalysisComplete; var resolvedUnit = await serverRef.getResolvedUnit(fileB); expect(resolvedUnit, isNotNull); } Future test_setAnalysisRoots_included_notAbsolute() async { var response = testSetAnalysisRoots(['foo/bar'], []); expect(response, isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT)); } Future test_setAnalysisRoots_included_notNormalized() async { var response = testSetAnalysisRoots(['/foo/../bar'], []); expect(response, isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT)); } Future test_setAnalysisRoots_notAbsolute() async { var response = testSetAnalysisRoots([], ['foo/bar']); expect(response, isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT)); } Future test_setAnalysisRoots_notNormalized() async { var response = testSetAnalysisRoots([], ['/foo/../bar']); expect(response, isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT)); } void test_setPriorityFiles_invalid() { var request = AnalysisSetPriorityFilesParams( [convertPath('/project/lib.dart')], ).toRequest('0'); var response = handler.handleRequest(request); expect(response, isResponseSuccess('0')); } void test_setPriorityFiles_valid() { var p1 = convertPath('/p1'); var p2 = convertPath('/p2'); var aPath = convertPath('/p1/a.dart'); var bPath = convertPath('/p2/b.dart'); var cPath = convertPath('/p2/c.dart'); newFile(aPath, content: 'library a;'); newFile(bPath, content: 'library b;'); newFile(cPath, content: 'library c;'); var setRootsRequest = AnalysisSetAnalysisRootsParams([p1, p2], []).toRequest('0'); var setRootsResponse = handler.handleRequest(setRootsRequest); expect(setRootsResponse, isResponseSuccess('0')); void setPriorityFiles(List fileList) { var request = AnalysisSetPriorityFilesParams(fileList).toRequest('0'); var response = handler.handleRequest(request); expect(response, isResponseSuccess('0')); // TODO(brianwilkerson) Enable the line below after getPriorityFiles // has been implemented. // expect(server.getPriorityFiles(), unorderedEquals(fileList)); } setPriorityFiles([aPath, bPath]); setPriorityFiles([bPath, cPath]); setPriorityFiles([]); } Future test_updateContent_badType() async { var helper = AnalysisTestHelper(); helper.createSingleFileProject('// empty'); await helper.onAnalysisComplete; var request = Request('0', ANALYSIS_REQUEST_UPDATE_CONTENT, { ANALYSIS_REQUEST_UPDATE_CONTENT_FILES: { helper.testFile: { 'type': 'foo', } } }); var response = helper.handler.handleRequest(request); expect(response, isResponseFailure('0')); } Future test_updateContent_changeOnDisk_duringOverride() async { var helper = AnalysisTestHelper(); helper.createSingleFileProject('library A;'); await helper.onAnalysisComplete; // update code helper.sendContentChange(AddContentOverlay('library B;')); // There should be no errors await helper.onAnalysisComplete; expect(helper.getTestErrors(), hasLength(0)); // Change file on disk, adding a syntax error. helper.resourceProvider.modifyFile(helper.testFile, 'library lib'); // There should still be no errors (file should not have been reread). await helper.onAnalysisComplete; expect(helper.getTestErrors(), hasLength(0)); // Send a content change with a null content param--file should be // reread from disk. helper.sendContentChange(RemoveContentOverlay()); // There should be errors now. await helper.onAnalysisComplete; expect(helper.getTestErrors(), hasLength(1)); } Future test_updateContent_changeOnDisk_normal() async { var helper = AnalysisTestHelper(); helper.createSingleFileProject('library A;'); await helper.onAnalysisComplete; // There should be no errors expect(helper.getTestErrors(), hasLength(0)); // Change file on disk, adding a syntax error. helper.resourceProvider.modifyFile(helper.testFile, 'library lib'); // There should be errors now. await pumpEventQueue(); await helper.onAnalysisComplete; expect(helper.getTestErrors(), hasLength(1)); } Future test_updateContent_fullContent() async { var helper = AnalysisTestHelper(); helper.createSingleFileProject('// empty'); await helper.onAnalysisComplete; // no errors initially var errors = helper.getTestErrors(); expect(errors, isEmpty); // update code helper.sendContentChange(AddContentOverlay('library lib')); // wait, there is an error await helper.onAnalysisComplete; errors = helper.getTestErrors(); expect(errors, hasLength(1)); } Future test_updateContent_incremental() async { var helper = AnalysisTestHelper(); var initialContent = 'library A;'; helper.createSingleFileProject(initialContent); await helper.onAnalysisComplete; // no errors initially var errors = helper.getTestErrors(); expect(errors, isEmpty); // Add the file to the cache helper.sendContentChange(AddContentOverlay(initialContent)); // update code helper.sendContentChange(ChangeContentOverlay( [SourceEdit('library '.length, 'A;'.length, 'lib')])); // wait, there is an error await helper.onAnalysisComplete; errors = helper.getTestErrors(); expect(errors, hasLength(1)); } Future test_updateContent_outOfRange_beyondEnd() { return outOfRangeTest(SourceEdit(6, 6, 'foo')); } Future test_updateContent_outOfRange_negativeLength() { return outOfRangeTest(SourceEdit(3, -1, 'foo')); } Future test_updateContent_outOfRange_negativeOffset() { return outOfRangeTest(SourceEdit(-1, 3, 'foo')); } void test_updateOptions_invalid() { var request = Request('0', ANALYSIS_REQUEST_UPDATE_OPTIONS, { ANALYSIS_REQUEST_UPDATE_OPTIONS_OPTIONS: {'not-an-option': true} }); var response = handler.handleRequest(request); // Invalid options should be silently ignored. expect(response, isResponseSuccess('0')); } void test_updateOptions_null() { // null is allowed as a synonym for {}. var request = Request('0', ANALYSIS_REQUEST_UPDATE_OPTIONS, {ANALYSIS_REQUEST_UPDATE_OPTIONS_OPTIONS: null}); var response = handler.handleRequest(request); expect(response, isResponseSuccess('0')); } Response testSetAnalysisRoots(List included, List excluded) { var request = AnalysisSetAnalysisRootsParams(included, excluded).toRequest('0'); return handler.handleRequest(request)!; } Future xtest_getReachableSources_invalidSource() async { // TODO(brianwilkerson) Re-enable this test if we re-enable the // analysis.getReachableSources request. newFile('/project/a.dart', content: 'import "b.dart";'); server.setAnalysisRoots('0', ['/project/'], []); await server.onAnalysisComplete; var request = AnalysisGetReachableSourcesParams('/does/not/exist.dart') .toRequest('0'); var response = handler.handleRequest(request)!; var error = response.error!; expect(error.code, RequestErrorCode.GET_REACHABLE_SOURCES_INVALID_FILE); } Future xtest_getReachableSources_validSources() async { // TODO(brianwilkerson) Re-enable this test if we re-enable the // analysis.getReachableSources request. var fileA = newFile('/project/a.dart', content: 'import "b.dart";').path; newFile('/project/b.dart'); server.setAnalysisRoots('0', ['/project/'], []); await server.onAnalysisComplete; var request = AnalysisGetReachableSourcesParams(fileA).toRequest('0'); var response = handler.handleRequest(request)!; var json = response.toJson()[Response.RESULT] as Map; // Sanity checks. expect(json['sources'], hasLength(6)); expect(json['sources']['file:///project/a.dart'], unorderedEquals(['dart:core', 'file:///project/b.dart'])); expect(json['sources']['file:///project/b.dart'], ['dart:core']); } } @reflectiveTest class AnalysisDomainTest extends AbstractAnalysisTest { final Map> filesErrors = {}; /// The files for which `analysis.flushResults` was received. final List flushResults = []; String get testFilePath => '$testPackageLibPath/test.dart'; String get testPackageLibPath => '$testPackageRootPath/lib'; String get testPackageRootPath => '$workspaceRootPath/test'; String get workspaceRootPath => '/home'; void assertHasErrors(String path) { path = convertPath(path); expect(filesErrors[path], isNotEmpty, reason: path); } void assertNoErrors(String path) { path = convertPath(path); expect(filesErrors[path], isEmpty, reason: path); } void assertNoErrorsNotification(String path) { path = convertPath(path); expect(filesErrors[path], isNull, reason: path); } void forgetReceivedErrors() { filesErrors.clear(); } @override void processNotification(Notification notification) { if (notification.event == ANALYSIS_NOTIFICATION_FLUSH_RESULTS) { var decoded = AnalysisFlushResultsParams.fromNotification(notification); flushResults.addAll(decoded.files); decoded.files.forEach(filesErrors.remove); } if (notification.event == ANALYSIS_NOTIFICATION_ERRORS) { var decoded = AnalysisErrorsParams.fromNotification(notification); filesErrors[decoded.file] = decoded.errors; } } Future test_fileSystem_addFile_analysisOptions() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; _createFilesWithErrors([a_path, b_path]); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // Both a.dart and b.dart are analyzed. _assertAnalyzedFiles( hasErrors: [a_path, b_path], notAnalyzed: [], ); // Write the options file that excludes b.dart newFile('$testPackageRootPath/analysis_options.yaml', content: r''' analyzer: exclude: - lib/b.dart '''); await pumpEventQueue(); await server.onAnalysisComplete; // Errors for all files were flushed, and only a.dart is reported again. _assertFlushedResults([a_path, b_path]); _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [b_path], ); } Future test_fileSystem_addFile_analysisOptions_analysis() async { var a_path = '$testPackageLibPath/a.dart'; var options_path = '$testPackageRootPath/analysis_options.yaml'; newFile(a_path, content: 'error'); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // a.dart was analyzed _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [options_path], ); // Add 'analysis_options.yaml' that has an error. newFile(options_path, content: ''' analyzer: error: '''); await pumpEventQueue(); await server.onAnalysisComplete; // Both files were analyzed. _assertAnalyzedFiles( hasErrors: [a_path, options_path], notAnalyzed: [], ); } Future test_fileSystem_addFile_androidManifestXml() async { var path = '$testPackageRootPath/AndroidManifest.xml'; newFile('$testPackageLibPath/a.dart', content: ''); newFile('$testPackageRootPath/analysis_options.yaml', content: ''' analyzer: optional-checks: chrome-os-manifest-checks: true '''); setRoots(included: [workspaceRootPath], excluded: []); newFile(path, content: ''); await pumpEventQueue(); await server.onAnalysisComplete; // No touch-screen. assertHasErrors(path); } Future test_fileSystem_addFile_dart() async { var a_path = '$testPackageLibPath/a.dart'; // We have to create the folder, otherwise there is nothing to watch. newFolder(testPackageLibPath); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We don't have a.dart yet. assertNoErrorsNotification(a_path); _createFilesWithErrors([a_path]); await pumpEventQueue(); await server.onAnalysisComplete; // We created a.dart, so it should be analyzed. assertHasErrors(a_path); } Future test_fileSystem_addFile_dart_dotFolder() async { var a_path = '$projectPath/lib/.foo/a.dart'; var b_path = '$projectPath/lib/b.dart'; newFile(b_path, content: r''' import '.foo/a.dart'; void f(A a) {} '''); createProject(); await pumpEventQueue(); await server.onAnalysisComplete; // We don't have a.dart, so the import cannot be resolved. assertHasErrors(b_path); newFile(a_path, content: r''' class A {} '''); await pumpEventQueue(); await server.onAnalysisComplete; // 'a.dart' is in a dot-folder, so excluded from analysis. assertNoErrorsNotification(a_path); // We added a.dart with `A`, so no errors. assertNoErrors(b_path); } Future test_fileSystem_addFile_dart_excluded() async { var a_path = '$projectPath/lib/a.dart'; var b_path = '$projectPath/lib/b.dart'; newFile('$projectPath/analysis_options.yaml', content: r''' analyzer: exclude: - "**/a.dart" '''); newFile(b_path, content: r''' import 'a.dart'; void f(A a) {} '''); createProject(); await pumpEventQueue(); await server.onAnalysisComplete; // We don't have a.dart, so the import cannot be resolved. assertHasErrors(b_path); newFile(a_path, content: r''' class A {} '''); await pumpEventQueue(); await server.onAnalysisComplete; // We excluded 'a.dart' from analysis, no errors notification for it. assertNoErrorsNotification(a_path); // We added a.dart with `A`, so no errors. assertNoErrors(b_path); } Future test_fileSystem_addFile_dotPackagesFile() async { var aaaLibPath = '/packages/aaa/lib'; var a_path = '$aaaLibPath/a.dart'; newFile(a_path, content: ''' class A {} '''); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We cannot resolve `package:aaa/a.dart` assertHasErrors(testFilePath); // Write `.packages`, recreate analysis contexts. newFile('$testPackageRootPath/.packages', content: ''' aaa:${toUriStr(aaaLibPath)} '''); await pumpEventQueue(); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_fileSystem_addFile_fixDataYaml() async { var path = '$testPackageLibPath/fix_data.yaml'; newFile('$testPackageLibPath/a.dart', content: ''); // Make sure that it is a package. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder(), ); setRoots(included: [workspaceRootPath], excluded: []); // No `fix_data.yaml` to analyze yet. assertNoErrorsNotification(path); // Create it, will be analyzed. newFile(path, content: '0: 1'); await pumpEventQueue(); await server.onAnalysisComplete; // And it has errors. assertHasErrors(path); // We don't recreate analysis contexts. _assertFlushedResults([]); } Future test_fileSystem_addFile_packageConfigJsonFile() async { var aaaRootPath = '/packages/aaa'; var a_path = '$aaaRootPath/lib/a.dart'; newFile(a_path, content: ''' class A {} '''); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We cannot resolve `package:aaa/a.dart` assertHasErrors(testFilePath); // Write `package_config.json`, recreate analysis contexts. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder()..add(name: 'aaa', rootPath: aaaRootPath), ); await pumpEventQueue(); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_fileSystem_addFile_pubspec_analysis() async { var a_path = '$testPackageLibPath/a.dart'; var pubspec_path = '$testPackageRootPath/pubspec.yaml'; newFile(a_path, content: 'error'); // Write an empty file to force a new analysis context. // We look for `pubspec.yaml` files only in analysis context roots. newFile('$testPackageRootPath/analysis_options.yaml', content: ''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // a.dart was analyzed _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [pubspec_path], ); // Add a non-Dart file that we know how to analyze. newFile(pubspec_path, content: ''' name: sample dependencies: true '''); await pumpEventQueue(); await server.onAnalysisComplete; // Both files were analyzed. _assertAnalyzedFiles( hasErrors: [a_path, pubspec_path], notAnalyzed: [], ); } Future test_fileSystem_addFile_unrelated() async { var a_path = '$testPackageLibPath/a.dart'; var unrelated_path = '$testPackageRootPath/unrelated.txt'; newFile(a_path, content: 'error'); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // a.dart was analyzed _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [unrelated_path], ); // Add an unrelated file, no analysis. newFile(unrelated_path, content: 'anything'); await pumpEventQueue(); await server.onAnalysisComplete; // No analysis. _assertFlushedResults([]); _assertAnalyzedFiles(hasErrors: [], notAnalyzed: [a_path]); } Future test_fileSystem_changeFile_analysisOptions() async { var options_path = '$testPackageRootPath/analysis_options.yaml'; var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; var c_path = '$testPackageLibPath/c.dart'; _createFilesWithErrors([a_path, b_path, c_path]); // Exclude b.dart from analysis. newFile(options_path, content: r''' analyzer: exclude: - lib/b.dart '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // Only a.dart is analyzed, because b.dart is excluded. _assertAnalyzedFiles( hasErrors: [a_path, c_path], notAnalyzed: [b_path], ); // Exclude c.dart from analysis. newFile(options_path, content: r''' analyzer: exclude: - lib/c.dart '''); await pumpEventQueue(); await server.onAnalysisComplete; // Errors for all files were flushed, a.dart and b.dart analyzed. _assertFlushedResults([options_path, a_path, c_path]); _assertAnalyzedFiles( hasErrors: [a_path, b_path], noErrors: [options_path], notAnalyzed: [c_path], ); } Future test_fileSystem_changeFile_androidManifestXml() async { var path = '$testPackageRootPath/AndroidManifest.xml'; newFile('$testPackageLibPath/a.dart', content: ''); // Has an error - no touch screen. newFile(path, content: ''); newFile('$testPackageRootPath/analysis_options.yaml', content: ''' analyzer: optional-checks: chrome-os-manifest-checks: true '''); setRoots(included: [workspaceRootPath], excluded: []); // Forget and check that we did. forgetReceivedErrors(); assertNoErrorsNotification(path); // Update the file, so analyze it. newFile(path, content: ''); await pumpEventQueue(); await server.onAnalysisComplete; // An error was reported. assertHasErrors(path); } Future test_fileSystem_changeFile_dart() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; newFile(a_path, content: r''' class A2 {} '''); newFile(b_path, content: r''' import 'a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; assertNoErrors(a_path); assertHasErrors(b_path); forgetReceivedErrors(); // Update a.dart so that b.dart has no error. newFile(a_path, content: 'class A {}'); await pumpEventQueue(); await server.onAnalysisComplete; // The update of a.dart fixed the error in b.dart assertNoErrors(a_path); assertNoErrors(b_path); } Future test_fileSystem_changeFile_dart_dotFolder() async { var a_path = '$testPackageLibPath/.foo/a.dart'; var b_path = '$testPackageLibPath/b.dart'; newFile(a_path, content: r''' class B {} '''); newFile(b_path, content: r''' import '.foo/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await pumpEventQueue(); await server.onAnalysisComplete; // 'a.dart' is in a dot-folder, so excluded from analysis. assertNoErrorsNotification(a_path); // We have `B`, not `A`, in a.dart, so has errors. assertHasErrors(b_path); newFile(a_path, content: r''' class A {} '''); await pumpEventQueue(); await server.onAnalysisComplete; // 'a.dart' is in a dot-folder, so excluded from analysis. assertNoErrorsNotification(a_path); // We changed a.dart, to have `A`, so no errors. assertNoErrors(b_path); } Future test_fileSystem_changeFile_dart_excluded() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; newFile('$testPackageRootPath/analysis_options.yaml', content: r''' analyzer: exclude: - "**/a.dart" '''); newFile(a_path, content: r''' class B {} '''); newFile(b_path, content: r''' import 'a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await pumpEventQueue(); await server.onAnalysisComplete; // We excluded 'a.dart' from analysis, no errors notification for it. assertNoErrorsNotification(a_path); // We have `B`, not `A`, in a.dart, so has errors. assertHasErrors(b_path); newFile(a_path, content: r''' class A {} '''); await pumpEventQueue(); await server.onAnalysisComplete; // We changed a.dart, to have `A`, so no errors. assertNoErrors(b_path); } Future test_fileSystem_changeFile_dotPackagesFile() async { var aaaLibPath = '/packages/aaa/lib'; var a_path = '$aaaLibPath/a.dart'; newFile(a_path, content: ''' class A {} '''); // Write `.packages` empty, without `package:aaa`. newFile('$testPackageRootPath/.packages', content: ''); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We cannot resolve `package:aaa/a.dart` assertHasErrors(testFilePath); // Write `.packages`, recreate analysis contexts. newFile('$testPackageRootPath/.packages', content: ''' aaa:${toUriStr(aaaLibPath)} '''); await pumpEventQueue(); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_fileSystem_changeFile_fixDataYaml() async { var path = '$testPackageLibPath/fix_data.yaml'; newFile('$testPackageLibPath/a.dart', content: ''); // Make sure that it is a package. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder(), ); // This file has an error. newFile(path, content: '0: 1'); setRoots(included: [workspaceRootPath], excluded: []); // The file was analyzed. assertHasErrors(path); // Replace with the context that does not have errors. newFile(path, content: r''' version: 1 transforms: [] '''); await pumpEventQueue(); await server.onAnalysisComplete; // And it has errors. assertNoErrors(path); // We don't recreate analysis contexts. _assertFlushedResults([]); } Future test_fileSystem_changeFile_packageConfigJsonFile() async { var aaaRootPath = '/packages/aaa'; var a_path = '$aaaRootPath/lib/a.dart'; newFile(a_path, content: ''' class A {} '''); // Write the empty file, without `package:aaa`. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder(), ); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We cannot resolve `package:aaa/a.dart` assertHasErrors(testFilePath); // Write `package_config.json`, recreate analysis contexts. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder()..add(name: 'aaa', rootPath: aaaRootPath), ); await pumpEventQueue(); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_fileSystem_deleteFile_analysisOptions() async { var options_path = '$testPackageRootPath/analysis_options.yaml'; var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; _createFilesWithErrors([a_path, b_path]); // Exclude b.dart from analysis. newFile(options_path, content: r''' analyzer: exclude: - lib/b.dart '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // Only a.dart is analyzed, because b.dart is excluded. _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [b_path], ); // Delete the options file. deleteFile(options_path); await pumpEventQueue(); await server.onAnalysisComplete; // Errors for a.dart were flushed, a.dart and b.dart analyzed. _assertFlushedResults([options_path, a_path]); _assertAnalyzedFiles( hasErrors: [a_path, b_path], notAnalyzed: [options_path], ); } Future test_fileSystem_deleteFile_androidManifestXml() async { var path = '$testPackageRootPath/AndroidManifest.xml'; newFile('$testPackageLibPath/a.dart', content: ''); // Has an error - no touch screen. newFile(path, content: ''); newFile('$testPackageRootPath/analysis_options.yaml', content: ''' analyzer: optional-checks: chrome-os-manifest-checks: true '''); setRoots(included: [workspaceRootPath], excluded: []); // An error was reported. _assertAnalyzedFiles(hasErrors: [path], notAnalyzed: []); // Delete the file. deleteFile(path); await pumpEventQueue(); // We received a flush notification. _assertAnalyzedFiles(hasErrors: [], notAnalyzed: [path]); } Future test_fileSystem_deleteFile_dart() async { var a_path = '$testPackageLibPath/a.dart'; _createFilesWithErrors([a_path]); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // a.dart was analyzed assertHasErrors(a_path); deleteFile(a_path); await pumpEventQueue(); await server.onAnalysisComplete; // We deleted a.dart, its errors should be flushed. _assertFlushedResults([a_path]); assertNoErrorsNotification(a_path); } Future test_fileSystem_deleteFile_dart_excluded() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; newFile('$testPackageRootPath/analysis_options.yaml', content: r''' analyzer: exclude: - "**/a.dart" '''); newFile(a_path, content: r''' class A {} '''); newFile(b_path, content: r''' import 'a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await pumpEventQueue(); await server.onAnalysisComplete; // We excluded 'a.dart' from analysis, no errors notification for it. assertNoErrorsNotification(a_path); // We have `A` in a.dart, so no errors. assertNoErrors(b_path); deleteFile(a_path); await pumpEventQueue(); await server.onAnalysisComplete; // We deleted a.dart, so `A` cannot be resolved. assertHasErrors(b_path); } Future test_fileSystem_deleteFile_dotPackagesFile() async { var aaaLibPath = '/packages/aaa/lib'; var a_path = '$aaaLibPath/a.dart'; newFile(a_path, content: ''' class A {} '''); newFile('$testPackageRootPath/.packages', content: ''' aaa:${toUriStr(aaaLibPath)} '''); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // Write `.packages`, recreate analysis contexts. deleteFile('$testPackageRootPath/.packages'); await pumpEventQueue(); await server.onAnalysisComplete; // We cannot resolve `package:aaa/a.dart` assertHasErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_fileSystem_deleteFile_fixDataYaml() async { var path = '$testPackageLibPath/fix_data.yaml'; newFile('$testPackageLibPath/a.dart', content: ''); // Make sure that it is a package. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder(), ); // This file has an error. newFile(path, content: '0: 1'); setRoots(included: [workspaceRootPath], excluded: []); // The file was analyzed. _assertAnalyzedFiles(hasErrors: [path], notAnalyzed: []); // Delete the file. deleteFile(path); await pumpEventQueue(); // We received a flush notification. _assertAnalyzedFiles(hasErrors: [], notAnalyzed: [path]); } Future test_fileSystem_deleteFile_packageConfigJsonFile() async { var aaaRootPath = '/packages/aaa'; var a_path = '$aaaRootPath/lib/a.dart'; newFile(a_path, content: ''' class A {} '''); // Write the empty file, without `package:aaa`. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder()..add(name: 'aaa', rootPath: aaaRootPath), ); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // Delete `package_config.json`, recreate analysis contexts. deleteFile( '$testPackageRootPath/.dart_tool/package_config.json', ); await pumpEventQueue(); await server.onAnalysisComplete; // We cannot resolve 'package:aaa/a.dart', so errors. assertHasErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_setRoots_dotPackagesFile() async { var aaaLibPath = '/packages/aaa/lib'; var a_path = '$aaaLibPath/a.dart'; newFile(a_path, content: ''' class A {} '''); newFile('$testPackageRootPath/.packages', content: ''' aaa:${toUriStr(aaaLibPath)} '''); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); // create project and wait for analysis setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } Future test_setRoots_includedFile() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; _createFilesWithErrors([a_path, b_path]); setRoots(included: [a_path], excluded: []); // Only a.dart is included, so b.dart is not analyzed. await server.onAnalysisComplete; _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [b_path], ); } Future test_setRoots_includedFile_setRoots() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; _createFilesWithErrors([a_path, b_path]); // Include only single file. setRoots(included: [a_path], excluded: []); await server.onAnalysisComplete; // So, only a.dart is analyzed, and b.dart is not. _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [b_path], ); // Include the folder that contains both a.dart and b.dart setRoots(included: [testPackageRootPath], excluded: []); await server.onAnalysisComplete; // So, both a.dart and b.dart are analyzed. _assertAnalyzedFiles( hasErrors: [a_path, b_path], notAnalyzed: [], ); } Future test_setRoots_includedFileFolder() async { var includedFile = '$testPackageLibPath/a.dart'; var includedFolder = '$testPackageLibPath/foo'; var includedFolderFile1 = '$includedFolder/1.dart'; var includedFolderFile2 = '$includedFolder/2.dart'; var notIncludedFile = '$testPackageLibPath/b.dart'; _createFilesWithErrors([ includedFile, includedFolderFile1, includedFolderFile2, notIncludedFile, ]); setRoots(included: [includedFile, includedFolder], excluded: []); await server.onAnalysisComplete; // We can combine a file, and a folder as included paths. // And the file that is not in there is not analyzed. _assertAnalyzedFiles(hasErrors: [ includedFile, includedFolderFile1, includedFolderFile2, ], notAnalyzed: [ notIncludedFile, ]); } Future test_setRoots_includedFolder_analysisOptions_exclude() async { var a_path = '$testPackageLibPath/a.dart'; var b_path = '$testPackageLibPath/b.dart'; newAnalysisOptionsYamlFile(testPackageRootPath, content: ''' analyzer: exclude: - "**/b.dart" '''); _createFilesWithErrors([a_path, b_path]); setRoots(included: [workspaceRootPath], excluded: []); // b.dart is excluded using the options file. await server.onAnalysisComplete; _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [b_path], ); } @FailingTest(reason: 'Not implemented in ContextLocator') Future test_setRoots_includedFolder_excludedFile() async { var a_path = '$testPackageLibPath/a.dart'; var excluded_path = '$testPackageRootPath/excluded/b.dart'; _createFilesWithErrors([a_path, excluded_path]); setRoots( included: [workspaceRootPath], excluded: [excluded_path], ); await server.onAnalysisComplete; _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [excluded_path], ); } Future test_setRoots_includedFolder_excludedFolder() async { var a_path = '$testPackageLibPath/a.dart'; var excluded_path = '$testPackageRootPath/excluded/b.dart'; _createFilesWithErrors([a_path, excluded_path]); setRoots( included: [workspaceRootPath], excluded: ['$testPackageRootPath/excluded'], ); await server.onAnalysisComplete; // a.dart is analyzed, but b.dart is in the excluded folder. _assertAnalyzedFiles( hasErrors: [a_path], notAnalyzed: [excluded_path], ); } Future test_setRoots_notDartFile_androidManifestXml() async { var path = '$testPackageRootPath/AndroidManifest.xml'; newFile('$testPackageLibPath/a.dart', content: ''); newFile('$testPackageRootPath/analysis_options.yaml', content: ''' analyzer: optional-checks: chrome-os-manifest-checks: true '''); newFile(path, content: ''); setRoots(included: [workspaceRootPath], excluded: []); // No touch-screen. assertHasErrors(path); } Future test_setRoots_notDartFile_fixDataYaml() async { var path = '$testPackageLibPath/fix_data.yaml'; // Make sure that it is a package. writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder(), ); // So, `lib/fix_data.yaml` will be analyzed. newFile(path, content: '0: 1'); setRoots(included: [workspaceRootPath], excluded: []); assertHasErrors(path); } Future test_setRoots_packageConfigJsonFile() async { var aaaRootPath = '/packages/aaa'; var a_path = '$aaaRootPath/lib/a.dart'; newFile(a_path, content: ''' class A {} '''); writePackageConfig( '$testPackageRootPath/.dart_tool/package_config.json', PackageConfigFileBuilder()..add(name: 'aaa', rootPath: aaaRootPath), ); newFile(testFilePath, content: ''' import 'package:aaa/a.dart'; void f(A a) {} '''); // create project and wait for analysis setRoots(included: [workspaceRootPath], excluded: []); await server.onAnalysisComplete; // We have `A` in 'package:aaa/a.dart', so no errors. assertNoErrors(testFilePath); // errors are not reported for packages assertNoErrorsNotification(a_path); } void writePackageConfig(String path, PackageConfigFileBuilder config) { newFile(path, content: config.toContent(toUriStr: toUriStr)); } void _assertAnalyzedFiles({ required List hasErrors, List noErrors = const [], required List notAnalyzed, }) { for (var path in hasErrors) { assertHasErrors(path); } for (var path in noErrors) { assertNoErrors(path); } for (var path in notAnalyzed) { assertNoErrorsNotification(path); } filesErrors.clear(); } void _assertFlushedResults(List paths) { var convertedPaths = paths.map(convertPath).toList(); expect(flushResults, unorderedEquals(convertedPaths)); flushResults.clear(); } /// Create files with a content that has a compile time error. /// So, when analyzed, these files will satisfy [assertHasErrors]. void _createFilesWithErrors(List paths) { for (var path in paths) { newFile(path, content: 'error'); } } } /// A helper to test 'analysis.*' requests. class AnalysisTestHelper with ResourceProviderMixin { late MockServerChannel serverChannel; late AnalysisServer server; late AnalysisDomainHandler handler; Map> analysisSubscriptions = {}; Map> filesErrors = {}; Map> filesHighlights = {}; Map> filesNavigation = {}; late String projectPath; late String testFile; late String testCode; AnalysisTestHelper() { projectPath = convertPath('/project'); testFile = convertPath('/project/bin/test.dart'); serverChannel = MockServerChannel(); // Create an SDK in the mock file system. MockSdk(resourceProvider: resourceProvider); server = AnalysisServer( serverChannel, resourceProvider, AnalysisServerOptions(), DartSdkManager(convertPath('/sdk')), CrashReportingAttachmentsBuilder.empty, InstrumentationService.NULL_SERVICE); handler = AnalysisDomainHandler(server); // listen for notifications var notificationStream = serverChannel.notificationController.stream; notificationStream.listen((Notification notification) { if (notification.event == ANALYSIS_NOTIFICATION_ERRORS) { var decoded = AnalysisErrorsParams.fromNotification(notification); filesErrors[decoded.file] = decoded.errors; } if (notification.event == ANALYSIS_NOTIFICATION_HIGHLIGHTS) { var params = AnalysisHighlightsParams.fromNotification(notification); filesHighlights[params.file] = params.regions; } if (notification.event == ANALYSIS_NOTIFICATION_NAVIGATION) { var params = AnalysisNavigationParams.fromNotification(notification); filesNavigation[params.file] = params.regions; } }); } /// Returns a [Future] that completes when the server's analysis is complete. Future get onAnalysisComplete { return server.onAnalysisComplete; } void addAnalysisSubscription(AnalysisService service, String file) { // add file to subscription var files = analysisSubscriptions[service]; if (files == null) { files = []; analysisSubscriptions[service] = files; } files.add(file); // set subscriptions var request = AnalysisSetSubscriptionsParams(analysisSubscriptions).toRequest('0'); handleSuccessfulRequest(request); } void addAnalysisSubscriptionHighlights(String file) { addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file); } void addAnalysisSubscriptionNavigation(String file) { addAnalysisSubscription(AnalysisService.NAVIGATION, file); } /// Creates an empty project `/project`. void createEmptyProject() { newFolder(projectPath); var request = AnalysisSetAnalysisRootsParams([projectPath], []).toRequest('0'); handleSuccessfulRequest(request); } /// Creates a project with a single Dart file `/project/bin/test.dart` with /// the given [code]. void createSingleFileProject(code) { testCode = _getCodeString(code); newFolder(projectPath); newFile(testFile, content: testCode); var request = AnalysisSetAnalysisRootsParams([projectPath], []).toRequest('0'); handleSuccessfulRequest(request); } /// Returns the offset of [search] in [testCode]. /// Fails if not found. int findOffset(String search) { var offset = testCode.indexOf(search); expect(offset, isNot(-1)); return offset; } /// Returns [AnalysisError]s recorded for the given [file]. /// May be empty, but not `null`. List getErrors(String file) { var errors = filesErrors[file]; if (errors != null) { return errors; } return []; } /// Returns highlights recorded for the given [file]. /// May be empty, but not `null`. List getHighlights(String file) { var highlights = filesHighlights[file]; if (highlights != null) { return highlights; } return []; } /// Returns navigation regions recorded for the given [file]. /// May be empty, but not `null`. List getNavigation(String file) { var navigation = filesNavigation[file]; if (navigation != null) { return navigation; } return []; } /// Returns [AnalysisError]s recorded for the [testFile]. /// May be empty, but not `null`. List getTestErrors() { return getErrors(testFile); } /// Returns highlights recorded for the given [testFile]. /// May be empty, but not `null`. List getTestHighlights() { return getHighlights(testFile); } /// Returns navigation information recorded for the given [testFile]. /// May be empty, but not `null`. List getTestNavigation() { return getNavigation(testFile); } /// Validates that the given [request] is handled successfully. void handleSuccessfulRequest(Request request) { var response = handler.handleRequest(request); expect(response, isResponseSuccess('0')); } /// Send an `updateContent` request for [testFile]. void sendContentChange(dynamic contentChange) { var request = AnalysisUpdateContentParams({testFile: contentChange}).toRequest('0'); handleSuccessfulRequest(request); } /// Stops the associated server. void stopServer() { server.done(); } static String _getCodeString(code) { if (code is List) { code = code.join('\n'); } return code as String; } } @reflectiveTest class SetSubscriptionsTest extends AbstractAnalysisTest { Map> filesHighlights = {}; final Completer _resultsAvailable = Completer(); @override void processNotification(Notification notification) { if (notification.event == ANALYSIS_NOTIFICATION_HIGHLIGHTS) { var params = AnalysisHighlightsParams.fromNotification(notification); filesHighlights[params.file] = params.regions; _resultsAvailable.complete(); } } Future test_afterAnalysis() async { addTestFile('int V = 42;'); createProject(); // wait for analysis, no results initially await waitForTasksFinished(); expect(filesHighlights[testFile], isNull); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile); await _resultsAvailable.future; // there are results expect(filesHighlights[testFile], isNotEmpty); } Future test_afterAnalysis_noSuchFile() async { var file = convertPath('/no-such-file.dart'); addTestFile('// no matter'); createProject(); // wait for analysis, no results initially await waitForTasksFinished(); expect(filesHighlights[testFile], isNull); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file); await _resultsAvailable.future; // there are results expect(filesHighlights[file], isEmpty); } Future test_afterAnalysis_packageFile_external() async { var pkgFile = newFile('/packages/pkgA/lib/libA.dart', content: ''' library lib_a; class A {} ''').path; newFile('/project/.packages', content: 'pkgA:file:///packages/pkgA/lib'); // addTestFile(''' import 'package:pkgA/libA.dart'; main() { new A(); } '''); createProject(); // wait for analysis, no results initially await waitForTasksFinished(); expect(filesHighlights[pkgFile], isNull); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFile); await _resultsAvailable.future; // there are results expect(filesHighlights[pkgFile], isNotEmpty); } Future test_afterAnalysis_packageFile_inRoot() async { var pkgA = convertPath('/pkgA'); var pkgB = convertPath('/pkgA'); var pkgFileA = newFile('$pkgA/lib/libA.dart', content: ''' library lib_a; class A {} ''').path; newFile('$pkgA/lib/libB.dart', content: ''' import 'package:pkgA/libA.dart'; main() { new A(); } '''); // add 'pkgA' and 'pkgB' as projects newFolder(projectPath); handleSuccessfulRequest( AnalysisSetAnalysisRootsParams([pkgA, pkgB], []).toRequest('0')); // wait for analysis, no results initially await waitForTasksFinished(); expect(filesHighlights[pkgFileA], isNull); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFileA); await _resultsAvailable.future; // there are results expect(filesHighlights[pkgFileA], isNotEmpty); } Future test_afterAnalysis_packageFile_notUsed() async { var pkgFile = newFile('/packages/pkgA/lib/libA.dart', content: ''' library lib_a; class A {} ''').path; newFile('/project/.packages', content: 'pkgA:/packages/pkgA/lib'); // addTestFile('// no "pkgA" reference'); createProject(); // wait for analysis, no results initially await waitForTasksFinished(); expect(filesHighlights[pkgFile], isNull); // make it a priority file, so make analyzable server.setPriorityFiles('0', [pkgFile]); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFile); await _resultsAvailable.future; // there are results expect(filesHighlights[pkgFile], isNotEmpty); } Future test_afterAnalysis_sdkFile() async { var file = convertPath('/sdk/lib/core/core.dart'); addTestFile('// no matter'); createProject(); // wait for analysis, no results initially await waitForTasksFinished(); expect(filesHighlights[file], isNull); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file); await _resultsAvailable.future; // there are results expect(filesHighlights[file], isNotEmpty); } Future test_beforeAnalysis() async { addTestFile('int V = 42;'); createProject(); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile); // wait for analysis await waitForTasksFinished(); expect(filesHighlights[testFile], isNotEmpty); } Future test_sentToPlugins() async { addTestFile('int V = 42;'); createProject(); // subscribe addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile); // wait for analysis await waitForTasksFinished(); var params = pluginManager.analysisSetSubscriptionsParams!; var subscriptions = params.subscriptions; expect(subscriptions, hasLength(1)); var files = subscriptions[plugin.AnalysisService.HIGHLIGHTS]; expect(files, [testFile]); } }