DAS plugins: Run plugin code zoned

In this CL, I run plugin code that responds to a request in an error
zone. The bulk of this change though is the tests that verify how we
handle asynchronous and synchronous thrown errors.

I separate some shared code from plugin_server_test into a shared lint
rules file and a base class. Then a second test is introduced with a
few error cases.

Change-Id: I4e252ae0d3bec0cf6625c0044681677fba3132bd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/384140
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server_plugin/lib/src/plugin_server.dart b/pkg/analysis_server_plugin/lib/src/plugin_server.dart
index 9b78633..9df1ebb 100644
--- a/pkg/analysis_server_plugin/lib/src/plugin_server.dart
+++ b/pkg/analysis_server_plugin/lib/src/plugin_server.dart
@@ -24,7 +24,6 @@
 import 'package:analyzer/src/dart/analysis/byte_store.dart';
 import 'package:analyzer/src/dart/analysis/file_content_cache.dart';
 import 'package:analyzer/src/dart/analysis/session.dart';
-import 'package:analyzer/src/dart/ast/utilities.dart';
 import 'package:analyzer/src/dart/element/type_system.dart';
 import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl;
 import 'package:analyzer/src/lint/lint_rule_timers.dart';
@@ -81,9 +80,6 @@
   }
 
   /// Handles an 'analysis.setContextRoots' request.
-  ///
-  /// Throws a [RequestFailure] if the request could not be handled.
-  // TODO(srawlins): Unnecessary??
   Future<protocol.AnalysisSetContextRootsResult> handleAnalysisSetContextRoots(
       protocol.AnalysisSetContextRootsParams parameters) async {
     var currentContextCollection = _contextCollection;
@@ -111,7 +107,6 @@
   /// Throws a [RequestFailure] if the request could not be handled.
   Future<protocol.EditGetFixesResult> handleEditGetFixes(
       protocol.EditGetFixesParams parameters) async {
-    // TODO(srawlins): Run this all in `runZonedGuarded`.
     var path = parameters.file;
     var offset = parameters.offset;
 
@@ -171,8 +166,6 @@
   }
 
   /// Handles a 'plugin.versionCheck' request.
-  ///
-  /// Throws a [RequestFailure] if the request could not be handled.
   Future<protocol.PluginVersionCheckResult> handlePluginVersionCheck(
       protocol.PluginVersionCheckParams parameters) async {
     // TODO(srawlins): It seems improper for _this_ method to be the point where
@@ -191,7 +184,7 @@
   /// Starts this plugin by listening to the given communication [channel].
   void start(PluginCommunicationChannel channel) {
     _channel = channel;
-    _channel.listen(_handleRequest,
+    _channel.listen(_handleRequestZoned,
         // TODO(srawlins): Implement.
         onError: () {},
         // TODO(srawlins): Implement.
@@ -214,21 +207,6 @@
     });
   }
 
-  /// Analyzes the file at the given [path].
-  Future<void> _analyzeFile(
-      {required AnalysisContext analysisContext, required String path}) async {
-    // TODO(srawlins): Run this all in `runZonedGuarded`.
-    var file = _resourceProvider.getFile(path);
-    var analysisOptions = analysisContext.getAnalysisOptionsForFile(file);
-    var lints = await _computeLints(
-      analysisContext,
-      path,
-      analysisOptions: analysisOptions as AnalysisOptionsImpl,
-    );
-    _channel.sendNotification(
-        protocol.AnalysisErrorsParams(path, lints).toNotification());
-  }
-
   /// Analyzes the files at the given [paths].
   Future<void> _analyzeFiles({
     required AnalysisContext analysisContext,
@@ -237,10 +215,15 @@
     // TODO(srawlins): Implement "priority files" and analyze them first.
     // TODO(srawlins): Analyze libraries instead of files, for efficiency.
     for (var path in paths.toSet()) {
-      await _analyzeFile(
-        analysisContext: analysisContext,
-        path: path,
+      var file = _resourceProvider.getFile(path);
+      var analysisOptions = analysisContext.getAnalysisOptionsForFile(file);
+      var lints = await _computeLints(
+        analysisContext,
+        path,
+        analysisOptions: analysisOptions as AnalysisOptionsImpl,
       );
+      _channel.sendNotification(
+          protocol.AnalysisErrorsParams(path, lints).toNotification());
     }
   }
 
@@ -290,6 +273,7 @@
       // TODO(srawlins): Support 'package' parameter.
       null,
     );
+
     // TODO(srawlins): Distinguish between registered rules and enabled rules.
     for (var rule in _registry.registeredRules) {
       rule.reporter = errorReporter;
@@ -299,10 +283,7 @@
       timer?.stop();
     }
 
-    var exceptionHandler = LinterExceptionHandler(
-            propagateExceptions: analysisOptions.propagateLinterExceptions)
-        .logException;
-    currentUnit.unit.accept(LinterVisitor(nodeRegistry, exceptionHandler));
+    currentUnit.unit.accept(LinterVisitor(nodeRegistry));
     // The list of the `AnalysisError`s and their associated
     // `protocol.AnalysisError`s.
     var errorsAndProtocolErrors = [
@@ -396,6 +377,17 @@
     }
   }
 
+  Future<void> _handleRequestZoned(Request request) async {
+    await runZonedGuarded(
+      () => _handleRequest(request),
+      (error, stackTrace) {
+        _channel.sendNotification(protocol.PluginErrorParams(
+                false /* isFatal */, error.toString(), stackTrace.toString())
+            .toNotification());
+      },
+    );
+  }
+
   static protocol.Location _locationFor(
       CompilationUnit unit, String path, AnalysisError error) {
     var lineInfo = unit.lineInfo;
diff --git a/pkg/analysis_server_plugin/pubspec.yaml b/pkg/analysis_server_plugin/pubspec.yaml
index f6be036..bac09b8 100644
--- a/pkg/analysis_server_plugin/pubspec.yaml
+++ b/pkg/analysis_server_plugin/pubspec.yaml
@@ -14,6 +14,7 @@
 
 # Use 'any' constraints here; we get our versions from the DEPS file.
 dev_dependencies:
+  async: any
   lints: any
   test_reflective_loader: any
   test: any
diff --git a/pkg/analysis_server_plugin/test/src/lint_rules.dart b/pkg/analysis_server_plugin/test/src/lint_rules.dart
new file mode 100644
index 0000000..4e550c1
--- /dev/null
+++ b/pkg/analysis_server_plugin/test/src/lint_rules.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2024, 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:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/error/error.dart';
+import 'package:analyzer/src/lint/linter.dart';
+
+class NoBoolsRule extends LintRule {
+  static const LintCode code = LintCode('no_bools', 'No bools message');
+
+  NoBoolsRule()
+      : super(
+          name: 'no_bools',
+          description: 'No bools desc',
+          details: 'No bools details',
+        );
+
+  @override
+  LintCode get lintCode => code;
+
+  @override
+  void registerNodeProcessors(
+      NodeLintRegistry registry, LinterContext context) {
+    var visitor = _NoBoolsVisitor(this);
+    registry.addBooleanLiteral(this, visitor);
+  }
+}
+
+class _NoBoolsVisitor extends SimpleAstVisitor<void> {
+  final LintRule rule;
+
+  _NoBoolsVisitor(this.rule);
+
+  @override
+  void visitBooleanLiteral(BooleanLiteral node) {
+    rule.reportLint(node);
+  }
+}
diff --git a/pkg/analysis_server_plugin/test/src/plugin_server_error_test.dart b/pkg/analysis_server_plugin/test/src/plugin_server_error_test.dart
new file mode 100644
index 0000000..100a61c
--- /dev/null
+++ b/pkg/analysis_server_plugin/test/src/plugin_server_error_test.dart
@@ -0,0 +1,272 @@
+// Copyright (c) 2024, 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_plugin/edit/dart/correction_producer.dart';
+import 'package:analysis_server_plugin/plugin.dart';
+import 'package:analysis_server_plugin/registry.dart';
+import 'package:analysis_server_plugin/src/plugin_server.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/error/error.dart';
+import 'package:analyzer/src/lint/linter.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
+import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol;
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'lint_rules.dart';
+import 'plugin_server_test_base.dart';
+
+void main() async {
+  defineReflectiveTests(PluginServerErrorTest);
+}
+
+@reflectiveTest
+class PluginServerErrorTest extends PluginServerTestBase {
+  Future<void> test_handleAnalysisSetContextRoots_throwingAsyncError() async {
+    pluginServer = PluginServer(
+      resourceProvider: resourceProvider,
+      plugins: [_RuleThrowsAsyncErrorPlugin()],
+    );
+    await startPlugin();
+
+    var packagePath = convertPath('/package1');
+    var filePath = join(packagePath, 'lib', 'test.dart');
+    newFile(filePath, 'bool b = false;');
+    var contextRoot = protocol.ContextRoot(packagePath, []);
+
+    await channel
+        .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot]));
+
+    var notifications = StreamQueue(channel.notifications);
+    var analysisErrorsParams = protocol.AnalysisErrorsParams.fromNotification(
+        await notifications.next);
+    expect(analysisErrorsParams.errors, isEmpty);
+
+    var pluginErrorParams =
+        protocol.PluginErrorParams.fromNotification(await notifications.next);
+    expect(pluginErrorParams.isFatal, false);
+    expect(pluginErrorParams.message, 'Bad state: A message.');
+    // TODO(srawlins): Does `StackTrace.toString()` not do what I think?
+    expect(pluginErrorParams.stackTrace, '');
+  }
+
+  Future<void> test_handleAnalysisSetContextRoots_throwingSyncError() async {
+    pluginServer = PluginServer(
+      resourceProvider: resourceProvider,
+      plugins: [_RuleThrowsSyncErrorPlugin()],
+    );
+    await startPlugin();
+
+    var packagePath = convertPath('/package1');
+    var filePath = join(packagePath, 'lib', 'test.dart');
+    newFile(filePath, 'bool b = false;');
+    var contextRoot = protocol.ContextRoot(packagePath, []);
+
+    var response = await channel
+        .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot]));
+
+    expect(
+      response.error,
+      isA<protocol.RequestError>()
+          .having((e) => e.message, 'message', 'Bad state: A message.')
+          .having((e) => e.stackTrace, 'stackTrace', isNotNull),
+    );
+  }
+
+  Future<void> test_handleEditGetFixes_throwingAsyncError() async {
+    pluginServer = PluginServer(
+      resourceProvider: resourceProvider,
+      plugins: [_FixThrowsAsyncErrorPlugin()],
+    );
+    await startPlugin();
+
+    var packagePath = convertPath('/package1');
+    var filePath = join(packagePath, 'lib', 'test.dart');
+    newFile(filePath, 'bool b = false;');
+    var contextRoot = protocol.ContextRoot(packagePath, []);
+
+    await channel
+        .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot]));
+    await channel
+        .sendRequest(protocol.EditGetFixesParams(filePath, 'bool b = '.length));
+
+    var notifications = StreamQueue(channel.notifications);
+    var analysisErrorsParams = protocol.AnalysisErrorsParams.fromNotification(
+        await notifications.next);
+    expect(analysisErrorsParams.errors.single, isA<protocol.AnalysisError>());
+
+    var pluginErrorParams =
+        protocol.PluginErrorParams.fromNotification(await notifications.next);
+    expect(pluginErrorParams.isFatal, false);
+    expect(pluginErrorParams.message, 'Bad state: A message.');
+    // TODO(srawlins): Does `StackTrace.toString()` not do what I think?
+    expect(pluginErrorParams.stackTrace, '');
+  }
+
+  Future<void> test_handleEditGetFixes_throwingSyncError() async {
+    pluginServer = PluginServer(
+      resourceProvider: resourceProvider,
+      plugins: [_FixThrowsSyncErrorPlugin()],
+    );
+    await startPlugin();
+
+    var packagePath = convertPath('/package1');
+    var filePath = join(packagePath, 'lib', 'test.dart');
+    newFile(filePath, 'bool b = false;');
+    var contextRoot = protocol.ContextRoot(packagePath, []);
+
+    await channel
+        .sendRequest(protocol.AnalysisSetContextRootsParams([contextRoot]));
+
+    var response = await channel
+        .sendRequest(protocol.EditGetFixesParams(filePath, 'bool b = '.length));
+    expect(
+      response.error,
+      isA<protocol.RequestError>()
+          .having((e) => e.message, 'message', 'Bad state: A message.')
+          .having((e) => e.stackTrace, 'stackTrace', isNotNull),
+    );
+  }
+}
+
+class _FixThrowsAsyncErrorPlugin extends Plugin {
+  @override
+  void register(PluginRegistry registry) {
+    registry.registerRule(NoBoolsRule());
+    registry.registerFixForRule(NoBoolsRule.code, _ThrowsAsyncErrorFix.new);
+  }
+}
+
+class _FixThrowsSyncErrorPlugin extends Plugin {
+  @override
+  void register(PluginRegistry registry) {
+    registry.registerRule(NoBoolsRule());
+    registry.registerFixForRule(NoBoolsRule.code, _ThrowsSyncErrorFix.new);
+  }
+}
+
+class _RuleThrowsAsyncErrorPlugin extends Plugin {
+  @override
+  void register(PluginRegistry registry) {
+    registry.registerRule(_ThrowsAsyncErrorRule());
+  }
+}
+
+class _RuleThrowsSyncErrorPlugin extends Plugin {
+  @override
+  void register(PluginRegistry registry) {
+    registry.registerRule(_ThrowsSyncErrorRule());
+  }
+}
+
+/// A correction producer that throws an async error while computing a
+/// correction.
+class _ThrowsAsyncErrorFix extends ResolvedCorrectionProducer {
+  _ThrowsAsyncErrorFix({required super.context});
+
+  @override
+  CorrectionApplicability get applicability =>
+      CorrectionApplicability.acrossFiles;
+
+  @override
+  FixKind get fixKind => FixKind('unused', 50, 'Unused');
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    // Raise an async error that can only be caught by an error zone's `onError`
+    // handler.
+    // ignore: unawaited_futures
+    Future<void>.error(StateError('A message.'));
+  }
+}
+
+class _ThrowsAsyncErrorRule extends LintRule {
+  static const LintCode code = LintCode('no_bools', 'No bools message');
+
+  _ThrowsAsyncErrorRule()
+      : super(
+          name: 'no_bools',
+          description: 'No bools desc',
+          details: 'No bools details',
+        );
+
+  @override
+  LintCode get lintCode => code;
+
+  @override
+  void registerNodeProcessors(
+      NodeLintRegistry registry, LinterContext context) {
+    var visitor = _ThrowsAsyncErrorVisitor(this);
+    registry.addBooleanLiteral(this, visitor);
+  }
+}
+
+class _ThrowsAsyncErrorVisitor extends SimpleAstVisitor<void> {
+  final LintRule rule;
+
+  _ThrowsAsyncErrorVisitor(this.rule);
+
+  @override
+  void visitBooleanLiteral(BooleanLiteral node) {
+    // Raise an async error that can only be caught by an error zone's `onError`
+    // handler.
+    // ignore: unawaited_futures
+    Future<void>.error(StateError('A message.'));
+  }
+}
+
+/// A correction producer that throws a sync error while computing a correction.
+class _ThrowsSyncErrorFix extends ResolvedCorrectionProducer {
+  _ThrowsSyncErrorFix({required super.context});
+
+  @override
+  CorrectionApplicability get applicability =>
+      CorrectionApplicability.acrossFiles;
+
+  @override
+  FixKind get fixKind => FixKind('unused', 50, 'Unused');
+
+  @override
+  Future<void> compute(ChangeBuilder builder) async {
+    throw StateError('A message.');
+  }
+}
+
+class _ThrowsSyncErrorRule extends LintRule {
+  static const LintCode code = LintCode('no_bools', 'No bools message');
+
+  _ThrowsSyncErrorRule()
+      : super(
+          name: 'no_bools',
+          description: 'No bools desc',
+          details: 'No bools details',
+        );
+
+  @override
+  LintCode get lintCode => code;
+
+  @override
+  void registerNodeProcessors(
+      NodeLintRegistry registry, LinterContext context) {
+    var visitor = _ThrowsSyncErrorVisitor(this);
+    registry.addBooleanLiteral(this, visitor);
+  }
+}
+
+class _ThrowsSyncErrorVisitor extends SimpleAstVisitor<void> {
+  final LintRule rule;
+
+  _ThrowsSyncErrorVisitor(this.rule);
+
+  @override
+  void visitBooleanLiteral(BooleanLiteral node) {
+    throw StateError('A message.');
+  }
+}
diff --git a/pkg/analysis_server_plugin/test/src/plugin_server_test.dart b/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
index ab552be..2f36449 100644
--- a/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
+++ b/pkg/analysis_server_plugin/test/src/plugin_server_test.dart
@@ -7,17 +7,7 @@
 import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
 import 'package:analysis_server_plugin/plugin.dart';
 import 'package:analysis_server_plugin/registry.dart';
-import 'package:analysis_server_plugin/src/correction/fix_generators.dart';
 import 'package:analysis_server_plugin/src/plugin_server.dart';
-import 'package:analyzer/dart/ast/ast.dart';
-import 'package:analyzer/dart/ast/visitor.dart';
-import 'package:analyzer/error/error.dart';
-import 'package:analyzer/file_system/file_system.dart';
-import 'package:analyzer/src/lint/linter.dart';
-import 'package:analyzer/src/test_utilities/mock_sdk.dart';
-import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
-import 'package:analyzer_plugin/channel/channel.dart';
-import 'package:analyzer_plugin/protocol/protocol.dart' as protocol;
 import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
 import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol;
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
@@ -25,41 +15,24 @@
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
+import 'lint_rules.dart';
+import 'plugin_server_test_base.dart';
+
 void main() async {
   defineReflectiveTests(PluginServerTest);
 }
 
 @reflectiveTest
-class PluginServerTest with ResourceProviderMixin {
-  final channel = _FakeChannel();
-
-  late final PluginServer pluginServer;
-
-  Folder get byteStoreRoot => getFolder('/byteStore');
-
-  Folder get sdkRoot => getFolder('/sdk');
-
+class PluginServerTest extends PluginServerTestBase {
+  @override
   Future<void> setUp() async {
-    createMockSdk(resourceProvider: resourceProvider, root: sdkRoot);
+    await super.setUp();
 
     pluginServer = PluginServer(
-      resourceProvider: resourceProvider,
-      plugins: [_NoBoolsPlugin()],
-    );
-    await pluginServer.initialize();
-    pluginServer.start(channel);
-
-    await pluginServer.handlePluginVersionCheck(
-      protocol.PluginVersionCheckParams(
-        byteStoreRoot.path,
-        sdkRoot.path,
-        '0.0.1',
-      ),
-    );
+        resourceProvider: resourceProvider, plugins: [_NoBoolsPlugin()]);
+    await startPlugin();
   }
 
-  void tearDown() => registeredFixGenerators.clearLintProducers();
-
   Future<void> test_handleAnalysisSetContextRoots() async {
     var packagePath = convertPath('/package1');
     var filePath = join(packagePath, 'lib', 'test.dart');
@@ -102,76 +75,11 @@
   }
 }
 
-class _FakeChannel implements PluginCommunicationChannel {
-  final _completers = <String, Completer<protocol.Response>>{};
-
-  final StreamController<protocol.Notification> _notificationsController =
-      StreamController();
-
-  Stream<protocol.Notification> get notifications =>
-      _notificationsController.stream;
-
-  @override
-  void close() {}
-
-  @override
-  void listen(void Function(protocol.Request request)? onRequest,
-      {void Function()? onDone, Function? onError, Function? onNotification}) {}
-
-  @override
-  void sendNotification(protocol.Notification notification) {
-    _notificationsController.add(notification);
-  }
-
-  @override
-  void sendResponse(protocol.Response response) {
-    var completer = _completers.remove(response.id);
-    completer?.complete(response);
-  }
-}
-
 class _NoBoolsPlugin extends Plugin {
   @override
   void register(PluginRegistry registry) {
-    registry.registerRule(_NoBoolsRule());
-    registry.registerFixForRule(_NoBoolsRule.code, _WrapInQuotes.new);
-  }
-}
-
-class _NoBoolsRule extends LintRule {
-  static const LintCode code = LintCode(
-    'no_bools',
-    'No bools message',
-    correctionMessage: 'No bools correction',
-  );
-
-  _NoBoolsRule()
-      : super(
-          name: 'no_bools',
-          description: 'No bools desc',
-          details: 'No bools details',
-          categories: {LintRuleCategory.errorProne},
-        );
-
-  @override
-  LintCode get lintCode => code;
-
-  @override
-  void registerNodeProcessors(
-      NodeLintRegistry registry, LinterContext context) {
-    var visitor = _NoBoolsVisitor(this);
-    registry.addBooleanLiteral(this, visitor);
-  }
-}
-
-class _NoBoolsVisitor extends SimpleAstVisitor<void> {
-  final LintRule rule;
-
-  _NoBoolsVisitor(this.rule);
-
-  @override
-  void visitBooleanLiteral(BooleanLiteral node) {
-    rule.reportLint(node);
+    registry.registerRule(NoBoolsRule());
+    registry.registerFixForRule(NoBoolsRule.code, _WrapInQuotes.new);
   }
 }
 
@@ -179,22 +87,16 @@
   static const _wrapInQuotesKind =
       FixKind('dart.fix.wrapInQuotes', 50, 'Wrap in quotes');
 
-  static const _wrapInQuotesMultiKind = FixKind(
-      'dart.fix.wrapInQuotes.multi', 40, 'Wrap in quotes everywhere in file');
+  _WrapInQuotes({required super.context});
 
   @override
-  final CorrectionApplicability applicability;
-
-  _WrapInQuotes({required super.context})
-      : applicability = CorrectionApplicability.acrossFiles;
+  CorrectionApplicability get applicability =>
+      CorrectionApplicability.acrossFiles;
 
   @override
   FixKind get fixKind => _wrapInQuotesKind;
 
   @override
-  FixKind get multiFixKind => _wrapInQuotesMultiKind;
-
-  @override
   Future<void> compute(ChangeBuilder builder) async {
     var literal = node;
     await builder.addDartFileEdit(file, (builder) {
diff --git a/pkg/analysis_server_plugin/test/src/plugin_server_test_base.dart b/pkg/analysis_server_plugin/test/src/plugin_server_test_base.dart
new file mode 100644
index 0000000..a42d919
--- /dev/null
+++ b/pkg/analysis_server_plugin/test/src/plugin_server_test_base.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2024, 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_plugin/src/correction/fix_generators.dart';
+import 'package:analysis_server_plugin/src/plugin_server.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/src/test_utilities/mock_sdk.dart';
+import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:analyzer_plugin/channel/channel.dart';
+import 'package:analyzer_plugin/protocol/protocol.dart' as protocol;
+import 'package:analyzer_plugin/protocol/protocol_generated.dart' as protocol;
+import 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
+    as protocol;
+import 'package:meta/meta.dart';
+import 'package:test/test.dart';
+
+class PluginServerTestBase with ResourceProviderMixin {
+  final channel = _FakeChannel();
+
+  late final PluginServer pluginServer;
+
+  Folder get byteStoreRoot => getFolder('/byteStore');
+
+  Folder get sdkRoot => getFolder('/sdk');
+
+  @mustCallSuper
+  Future<void> setUp() async {
+    createMockSdk(resourceProvider: resourceProvider, root: sdkRoot);
+  }
+
+  Future<void> startPlugin() async {
+    await pluginServer.initialize();
+    pluginServer.start(channel);
+
+    await pluginServer.handlePluginVersionCheck(
+      protocol.PluginVersionCheckParams(
+          byteStoreRoot.path, sdkRoot.path, '0.0.1'),
+    );
+  }
+
+  void tearDown() => registeredFixGenerators.clearLintProducers();
+}
+
+class _FakeChannel implements PluginCommunicationChannel {
+  final _completers = <String, Completer<protocol.Response>>{};
+
+  final StreamController<protocol.Notification> _notificationsController =
+      StreamController();
+
+  void Function(protocol.Request)? _onRequest;
+
+  int _idCounter = 0;
+
+  Stream<protocol.Notification> get notifications =>
+      _notificationsController.stream;
+
+  @override
+  void close() {}
+
+  @override
+  void listen(void Function(protocol.Request request)? onRequest,
+      {void Function()? onDone, Function? onError, Function? onNotification}) {
+    _onRequest = onRequest;
+  }
+
+  @override
+  void sendNotification(protocol.Notification notification) {
+    _notificationsController.add(notification);
+  }
+
+  Future<protocol.Response> sendRequest(protocol.RequestParams params) {
+    if (_onRequest == null) {
+      fail(
+          '_onReuest is null! `listen` has not yet been called on this channel.');
+    }
+    var id = (_idCounter++).toString();
+    var request = params.toRequest(id);
+    var completer = Completer<protocol.Response>();
+    _completers[request.id] = completer;
+    _onRequest!(request);
+    return completer.future;
+  }
+
+  @override
+  void sendResponse(protocol.Response response) {
+    var completer = _completers.remove(response.id);
+    completer?.complete(response);
+  }
+}
diff --git a/pkg/analysis_server_plugin/test/src/test_all.dart b/pkg/analysis_server_plugin/test/src/test_all.dart
new file mode 100644
index 0000000..d75bfad
--- /dev/null
+++ b/pkg/analysis_server_plugin/test/src/test_all.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2024, 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:test_reflective_loader/test_reflective_loader.dart';
+
+import 'plugin_server_error_test.dart' as plugin_server_error_test;
+import 'plugin_server_test.dart' as plugin_server_test;
+
+void main() {
+  defineReflectiveSuite(() {
+    plugin_server_error_test.main();
+    plugin_server_test.main();
+  }, name: 'src');
+}
diff --git a/pkg/analysis_server_plugin/test/test_all.dart b/pkg/analysis_server_plugin/test/test_all.dart
index 7471798..56970e8 100644
--- a/pkg/analysis_server_plugin/test/test_all.dart
+++ b/pkg/analysis_server_plugin/test/test_all.dart
@@ -5,9 +5,11 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import 'edit/test_all.dart' as edit;
+import 'src/test_all.dart' as src;
 
 void main() {
   defineReflectiveSuite(() {
     edit.main();
-  }, name: 'edit');
+    src.main();
+  });
 }