e20704c3b0
Move PackageConfigFileBuilder into the analyzer_testing public API and deprecate the copy exposed from package:analyzer. The builder is only used by test infrastructure, so keeping it in analyzer_testing makes the ownership clearer and avoids exposing test-only utilities from analyzer. Update the builder API to accept a rootFolder instead of a rootPath. This lets callers pass the resource-provider folder directly, so the generated rootUri is derived from the same file-system abstraction that created the test files. This avoids accidentally passing POSIX paths where resource provider paths are required, such as on Windows. Update existing test utilities and callers to import the new library and pass Folder objects. Remove the production analysis server dependency on the builder by emitting the temporary plugin package config JSON directly. Change-Id: I46b14710626e0d6d5884afcdc5a05b23077acfc9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499081 Reviewed-by: Paul Berry <paulberry@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com> Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
367 lines
12 KiB
Dart
367 lines
12 KiB
Dart
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'package:analysis_server/protocol/protocol.dart';
|
|
import 'package:analysis_server/protocol/protocol_constants.dart';
|
|
import 'package:analysis_server/protocol/protocol_generated.dart';
|
|
import 'package:analysis_server/src/analytics/analytics_manager.dart';
|
|
import 'package:analysis_server/src/legacy_analysis_server.dart';
|
|
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
|
|
import 'package:analysis_server/src/server/error_notifier.dart';
|
|
import 'package:analysis_server/src/session_logger/session_logger.dart';
|
|
import 'package:analysis_server/src/utilities/mocks.dart';
|
|
import 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:analyzer/file_system/memory_file_system.dart';
|
|
import 'package:analyzer/src/generated/sdk.dart';
|
|
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart';
|
|
import 'package:analyzer_testing/package_config_file_builder.dart';
|
|
import 'package:analyzer_testing/resource_provider_mixin.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
import 'package:unified_analytics/unified_analytics.dart';
|
|
|
|
import 'constants.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(AnalysisServerTest);
|
|
});
|
|
}
|
|
|
|
@reflectiveTest
|
|
class AnalysisServerTest with ResourceProviderMixin {
|
|
@override
|
|
MemoryResourceProvider resourceProvider = MemoryResourceProvider(
|
|
// Force the in-memory file watchers to be slowly initialized to emulate
|
|
// the physical watchers (for test_concurrentContextRebuilds).
|
|
delayWatcherInitialization: Duration(milliseconds: 1),
|
|
);
|
|
|
|
late MockServerChannel channel;
|
|
late ErrorNotifier errorNotifier;
|
|
late LegacyAnalysisServer server;
|
|
|
|
void setUp() {
|
|
channel = MockServerChannel(printMessages: debugPrintCommunication);
|
|
|
|
// Create an SDK in the mock file system.
|
|
var sdkRoot = newFolder('/sdk');
|
|
createMockSdk(resourceProvider: resourceProvider, root: sdkRoot);
|
|
|
|
errorNotifier = ErrorNotifier();
|
|
server = LegacyAnalysisServer(
|
|
channel,
|
|
resourceProvider,
|
|
AnalysisServerOptions(),
|
|
DartSdkManager(sdkRoot.path),
|
|
AnalyticsManager(NoOpAnalytics()),
|
|
CrashReportingAttachmentsBuilder.empty,
|
|
errorNotifier,
|
|
SessionLogger(),
|
|
);
|
|
errorNotifier.server = server;
|
|
}
|
|
|
|
/// See https://github.com/dart-lang/sdk/issues/50496
|
|
Future<void> test_caching_mixin_superInvokedNames_setter_change() async {
|
|
var lib = convertPath('/lib');
|
|
newFolder(lib);
|
|
var foo = newFile('/lib/foo.dart', '''
|
|
class A {
|
|
set foo(int _) {}
|
|
}
|
|
mixin M on A {
|
|
void bar() {
|
|
super.boo = 0;
|
|
}
|
|
}
|
|
class X extends A with M {}
|
|
''');
|
|
await server.setAnalysisRoots('0', [lib], []);
|
|
await server.onAnalysisComplete;
|
|
expect(server.statusAnalyzing, isFalse);
|
|
channel.notificationsReceived.clear();
|
|
|
|
server.updateContent('0', {
|
|
foo.path: AddContentOverlay('''
|
|
class A {
|
|
set foo(int _) {}
|
|
}
|
|
mixin M on A {
|
|
void bar() {
|
|
super.foo = 0;
|
|
}
|
|
}
|
|
class X extends A with M {}
|
|
'''),
|
|
});
|
|
await server.onAnalysisComplete;
|
|
expect(server.statusAnalyzing, isFalse);
|
|
var notifications = channel.notificationsReceived;
|
|
expect(notifications, hasLength(1));
|
|
var notification = notifications.first;
|
|
expect(notification.event, 'analysis.errors');
|
|
var params = notification.params!;
|
|
var errors = params['errors'] as List<Map<String, Object?>>;
|
|
expect(errors, isEmpty);
|
|
}
|
|
|
|
/// Test that modifying package_config again while a context rebuild is in
|
|
/// progress does not get lost due to a gap between creating a file watcher
|
|
/// and it raising events.
|
|
/// https://github.com/Dart-Code/Dart-Code/issues/3438
|
|
Future<void> test_concurrentContextRebuilds() async {
|
|
// Subscribe to STATUS so we'll know when analysis is done.
|
|
server.serverServices = {ServerService.STATUS};
|
|
var projectRoot = convertPath('/foo');
|
|
var projectTestFile = convertPath('/foo/lib/test.dart');
|
|
var projectPackageConfigFile = convertPath(
|
|
'/foo/.dart_tool/package_config.json',
|
|
);
|
|
|
|
// Create a file that references two packages, which will we write to
|
|
// package_config.json individually.
|
|
newFolder(projectRoot);
|
|
newFile(projectTestFile, r'''
|
|
import "package:foo/foo.dart";'
|
|
import "package:bar/bar.dart";'
|
|
''');
|
|
|
|
// Ensure the packages and package_config exist.
|
|
var fooLibFolder = _addSimplePackage('foo', '');
|
|
var barLibFolder = _addSimplePackage('bar', '');
|
|
var config = PackageConfigFileBuilder();
|
|
writePackageConfig(projectPackageConfigFile, config);
|
|
|
|
// Track diagnostics that arrive.
|
|
var errorsByFile = <String, List<AnalysisError>>{};
|
|
channel.notifications
|
|
.where((notification) => notification.event == 'analysis.errors')
|
|
.listen((notification) {
|
|
var params = AnalysisErrorsParams.fromNotification(
|
|
notification,
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
errorsByFile[params.file] = params.errors;
|
|
});
|
|
|
|
/// Helper that waits for analysis then returns the relevant errors.
|
|
Future<List<AnalysisError>> getUriNotExistErrors() async {
|
|
await server.onAnalysisComplete;
|
|
expect(server.statusAnalyzing, isFalse);
|
|
return errorsByFile[projectTestFile]!
|
|
.where((error) => error.code == 'uri_does_not_exist')
|
|
.toList();
|
|
}
|
|
|
|
// Set roots and expect 2 uri_does_not_exist errors.
|
|
await server.setAnalysisRoots('0', [projectRoot], []);
|
|
expect(await getUriNotExistErrors(), hasLength(2));
|
|
|
|
// Write both packages, in two events so that the first one will trigger
|
|
// a rebuild.
|
|
config.add(name: 'foo', rootFolder: fooLibFolder.parent);
|
|
writePackageConfig(projectPackageConfigFile, config);
|
|
await pumpEventQueue(times: 1); // Allow server to begin processing.
|
|
config.add(name: 'bar', rootFolder: barLibFolder.parent);
|
|
writePackageConfig(projectPackageConfigFile, config);
|
|
|
|
// Eventually the errors are gone.
|
|
while (true) {
|
|
var errors = await getUriNotExistErrors();
|
|
if (errors.isEmpty) {
|
|
break;
|
|
}
|
|
await pumpEventQueue(times: 5000);
|
|
}
|
|
}
|
|
|
|
Future<void> test_errorNotification_errorNotifier() async {
|
|
errorNotifier.logException(Exception('dummy exception'));
|
|
|
|
var errors = channel.notificationsReceived.where(
|
|
(notification) => notification.event == serverNotificationError,
|
|
);
|
|
expect(
|
|
errors.single.params![serverNotificationErrorMessage],
|
|
contains('dummy exception'),
|
|
);
|
|
}
|
|
|
|
Future<void> test_errorNotification_sendNotification() async {
|
|
server.sendServerErrorNotification(
|
|
'message',
|
|
Exception('dummy exception'),
|
|
null,
|
|
);
|
|
|
|
var errors = channel.notificationsReceived.where(
|
|
(notification) => notification.event == serverNotificationError,
|
|
);
|
|
expect(
|
|
errors.single.params![serverNotificationErrorMessage],
|
|
contains('dummy exception'),
|
|
);
|
|
}
|
|
|
|
Future<void> test_serverStatusNotifications_hasFile() async {
|
|
server.serverServices.add(ServerService.STATUS);
|
|
|
|
newFile('/test/lib/a.dart', r'''
|
|
class A {}
|
|
''');
|
|
await server.setAnalysisRoots('0', [convertPath('/test')], []);
|
|
|
|
// Pump the event queue, so that the server has finished any analysis.
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
var notifications = channel.notificationsReceived;
|
|
expect(notifications, isNotEmpty);
|
|
|
|
// At least one notification indicating analysis is in progress.
|
|
expect(
|
|
notifications.any((Notification notification) {
|
|
if (notification.event == serverNotificationStatus) {
|
|
var params = ServerStatusParams.fromNotification(
|
|
notification,
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
var analysis = params.analysis;
|
|
if (analysis != null) {
|
|
return analysis.isAnalyzing;
|
|
}
|
|
}
|
|
return false;
|
|
}),
|
|
isTrue,
|
|
);
|
|
|
|
// The last notification should indicate that analysis is complete.
|
|
var notification = notifications[notifications.length - 1];
|
|
var params = ServerStatusParams.fromNotification(
|
|
notification,
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
expect(params.analysis!.isAnalyzing, isFalse);
|
|
}
|
|
|
|
Future<void> test_serverStatusNotifications_noFiles() async {
|
|
server.serverServices.add(ServerService.STATUS);
|
|
|
|
newFolder('/test');
|
|
await server.setAnalysisRoots('0', [convertPath('/test')], []);
|
|
|
|
// Pump the event queue, so that the server has finished any analysis.
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
var notifications = channel.notificationsReceived;
|
|
expect(notifications, isNotEmpty);
|
|
|
|
// At least one notification indicating analysis is in progress.
|
|
expect(
|
|
notifications.any((Notification notification) {
|
|
if (notification.event == serverNotificationStatus) {
|
|
var params = ServerStatusParams.fromNotification(
|
|
notification,
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
var analysis = params.analysis;
|
|
if (analysis != null) {
|
|
return analysis.isAnalyzing;
|
|
}
|
|
}
|
|
return false;
|
|
}),
|
|
isTrue,
|
|
);
|
|
|
|
// The last notification should indicate that analysis is complete.
|
|
var notification = notifications[notifications.length - 1];
|
|
var params = ServerStatusParams.fromNotification(
|
|
notification,
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
expect(params.analysis!.isAnalyzing, isFalse);
|
|
}
|
|
|
|
Future<void>
|
|
test_setAnalysisSubscriptions_fileInIgnoredFolder_newOptions() async {
|
|
var path = convertPath('/project/samples/sample.dart');
|
|
newFile(path, '');
|
|
newAnalysisOptionsYamlFile('/project', r'''
|
|
analyzer:
|
|
exclude:
|
|
- 'samples/**'
|
|
''');
|
|
await server.setAnalysisRoots('0', [convertPath('/project')], []);
|
|
server.setAnalysisSubscriptions(<AnalysisService, Set<String>>{
|
|
AnalysisService.NAVIGATION: <String>{path},
|
|
});
|
|
|
|
// We respect subscriptions, even for excluded files.
|
|
await pumpEventQueue(times: 5000);
|
|
expect(
|
|
channel.notificationsReceived.any((notification) {
|
|
return notification.event == analysisNotificationNavigation;
|
|
}),
|
|
isTrue,
|
|
);
|
|
}
|
|
|
|
Future<void>
|
|
test_setAnalysisSubscriptions_fileInIgnoredFolder_oldOptions() async {
|
|
var path = convertPath('/project/samples/sample.dart');
|
|
newFile(path, '');
|
|
newAnalysisOptionsYamlFile('/project', r'''
|
|
analyzer:
|
|
exclude:
|
|
- 'samples/**'
|
|
''');
|
|
await server.setAnalysisRoots('0', [convertPath('/project')], []);
|
|
server.setAnalysisSubscriptions(<AnalysisService, Set<String>>{
|
|
AnalysisService.NAVIGATION: <String>{path},
|
|
});
|
|
|
|
// We respect subscriptions, even for excluded files.
|
|
await pumpEventQueue(times: 5000);
|
|
expect(
|
|
channel.notificationsReceived.any((notification) {
|
|
return notification.event == analysisNotificationNavigation;
|
|
}),
|
|
isTrue,
|
|
);
|
|
}
|
|
|
|
Future<void> test_shutdown() {
|
|
var request = Request('my28', serverRequestShutdown);
|
|
return channel.simulateRequestFromClient(request).then((Response response) {
|
|
expect(response.id, equals('my28'));
|
|
expect(response.error, isNull);
|
|
});
|
|
}
|
|
|
|
Future<void> test_unknownRequest() {
|
|
var request = Request('my22', 'randomRequest');
|
|
return channel.simulateRequestFromClient(request).then((Response response) {
|
|
expect(response.id, equals('my22'));
|
|
expect(response.error, isNotNull);
|
|
});
|
|
}
|
|
|
|
void writePackageConfig(String path, PackageConfigFileBuilder config) {
|
|
newFile(path, config.toContent());
|
|
}
|
|
|
|
/// Creates a simple package named [name] with [content] in the file at
|
|
/// `package:$name/$name.dart`.
|
|
///
|
|
/// Returns a [Folder] that represents the packages `lib` folder.
|
|
Folder _addSimplePackage(String name, String content) {
|
|
var packagePath = '/packages/$name';
|
|
var file = newFile('$packagePath/lib/$name.dart', content);
|
|
return file.parent;
|
|
}
|
|
}
|