[frontend_server] Add FrontendServer wrapper in test

This adds a FrontendServer class that wraps steam communication,
reducing the amount of boilerplate code needed to write frontend
server tests.

Change-Id: I9ff0fe6e04a83503e00eea9fc764231e50d6207a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/253666
Reviewed-by: Jens Johansen <jensj@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/frontend_server/test/frontend_server_test.dart b/pkg/frontend_server/test/frontend_server_test.dart
index d4ad295..06e04e1 100644
--- a/pkg/frontend_server/test/frontend_server_test.dart
+++ b/pkg/frontend_server/test/frontend_server_test.dart
@@ -529,23 +529,11 @@
         '--packages=${packageConfig.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         if (count == 0) {
@@ -553,33 +541,15 @@
           expect(result.errorsCount, equals(0));
           expect(dillFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          streamController.add('accept\n'.codeUnits);
-
-          // 'compile-expression <boundarykey>
-          // expression
-          // definitions (one per line)
-          // ...
-          // <boundarykey>
-          // type-definitions (one per line)
-          // ...
-          // <boundarykey>
-          // <libraryUri: String>
-          // <klass: String>
-          // <isStatic: true|false>
-          outputParser.expectSources = false;
-          streamController.add(
-              'compile-expression abc\n2+2\nabc\nabc\n${file.uri}\n\n\n'
-                  .codeUnits);
+          frontendServer.accept();
+          frontendServer.compileExpression('2+2', file.uri, isStatic: null);
           count += 1;
         } else if (count == 1) {
           expect(result.errorsCount, isNull);
           // Previous request should have failed because isStatic was blank
           expect(compiledResult.status, isNull);
 
-          outputParser.expectSources = false;
-          streamController.add(
-              'compile-expression abc\n2+2\nabc\nabc\n${file.uri}\n\nfalse\n'
-                  .codeUnits);
+          frontendServer.compileExpression('2+2', file.uri, isStatic: false);
           count += 1;
         } else if (count == 2) {
           expect(result.errorsCount, equals(0));
@@ -589,19 +559,20 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          streamController.add('compile foo.bar\n'.codeUnits);
+          frontendServer.compile('foo.bar');
           count += 1;
         } else {
           expect(count, 3);
           // Third request is to 'compile' non-existent file, that should fail.
           expect(result.errorsCount, greaterThan(0));
 
-          streamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
       expect(count, 3);
+      frontendServer.close();
     });
 
     test('mixed compile expression commands with non-web target', () async {
@@ -619,23 +590,11 @@
       var library = 'package:hello/foo.dart';
       var module = 'packages/hello/foo.dart';
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         if (count == 0) {
@@ -643,23 +602,9 @@
           expect(result.errorsCount, equals(0));
           expect(dillFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          streamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
-          // 'compile-expression <boundarykey>
-          // expression
-          // definitions (one per line)
-          // ...
-          // <boundarykey>
-          // type-definitions (one per line)
-          // ...
-          // <boundarykey>
-          // <libraryUri: String>
-          // <klass: String>
-          // <isStatic: true|false>
-          outputParser.expectSources = false;
-          streamController.add('compile-expression abc\n'
-                  '2+2\nabc\nabc\n${file.uri}\n\nfalse\n'
-              .codeUnits);
+          frontendServer.compileExpression('2+2', file.uri, isStatic: false);
           count += 1;
         } else if (count == 1) {
           expect(result.errorsCount, equals(0));
@@ -669,22 +614,7 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          // 'compile-expression-to-js <boundarykey>
-          // libraryUri
-          // line
-          // column
-          // jsModules (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // jsFrameValues (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // moduleName
-          // expression
-          outputParser.expectSources = false;
-          streamController.add('compile-expression-to-js abc\n'
-                  '$library\n1\n1\nabc\nabc\n$module\n\n'
-              .codeUnits);
+          frontendServer.compileExpressionToJs('', library, 1, 1, module);
           count += 1;
         } else if (count == 2) {
           // Third request is to 'compile-expression-to-js' that fails
@@ -692,10 +622,7 @@
           expect(result.errorsCount, isNull);
           expect(compiledResult.status, isNull);
 
-          outputParser.expectSources = false;
-          streamController.add('compile-expression abc\n'
-                  '2+2\nabc\nabc\n${file.uri}\n\nfalse\n'
-              .codeUnits);
+          frontendServer.compileExpression('2+2', file.uri, isStatic: false);
           count += 1;
         } else if (count == 3) {
           expect(result.errorsCount, equals(0));
@@ -705,12 +632,13 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          streamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
       expect(count, 3);
+      frontendServer.close();
     });
 
     test('compiler reports correct sources added', () async {
@@ -729,47 +657,31 @@
         '--output-dill=${dillFile.path}'
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      final Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${mainFile.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      final Future<int> result = frontendServer.open(args);
+      frontendServer.compile(mainFile.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         compiledResult.expectNoErrors();
         if (count == 0) {
           expect(compiledResult.sources.length, equals(1));
           expect(compiledResult.sources, contains('+${mainFile.uri}'));
 
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
           mainFile
               .writeAsStringSync("import 'lib.dart';  main() => print(foo);\n");
-          inputStreamController.add('recompile ${mainFile.path} abc\n'
-                  '${mainFile.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(mainFile.uri, entryPoint: mainFile.path);
           count += 1;
         } else if (count == 1) {
           expect(compiledResult.sources.length, equals(1));
           expect(compiledResult.sources, contains('+${libFile.uri}'));
-          inputStreamController.add('accept\n'.codeUnits);
-          inputStreamController.add('quit\n'.codeUnits);
+          frontendServer.accept();
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     }, timeout: Timeout.factor(100));
 
     test('compiler reports correct sources removed', () async {
@@ -788,47 +700,31 @@
         '--output-dill=${dillFile.path}'
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      final Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${mainFile.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      final Future<int> result = frontendServer.open(args);
+      frontendServer.compile(mainFile.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         compiledResult.expectNoErrors();
         if (count == 0) {
           expect(compiledResult.sources.length, equals(2));
           expect(compiledResult.sources,
               allOf(contains('+${mainFile.uri}'), contains('+${libFile.uri}')));
 
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
           mainFile.writeAsStringSync("main() => print('foo');\n");
-          inputStreamController.add('recompile ${mainFile.path} abc\n'
-                  '${mainFile.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(mainFile.uri, entryPoint: mainFile.path);
           count += 1;
         } else if (count == 1) {
           expect(compiledResult.sources.length, equals(1));
           expect(compiledResult.sources, contains('-${libFile.uri}'));
-          inputStreamController.add('accept\n'.codeUnits);
-          inputStreamController.add('quit\n'.codeUnits);
+          frontendServer.accept();
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     }, timeout: Timeout.factor(100));
 
     test('compile expression when delta is rejected', () async {
@@ -845,24 +741,11 @@
         '--output-dill=${dillFile.path}'
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      final Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      final Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         if (count == 0) {
@@ -870,30 +753,9 @@
           expect(result.errorsCount, 0);
           expect(dillFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
-          // 'compile-expression <boundarykey>
-          // expression
-          // definitions (one per line)
-          // ...
-          // <boundarykey>
-          // type-definitions (one per line)
-          // ...
-          // <boundarykey>
-          // <libraryUri: String>
-          // <klass: String>
-          // <isStatic: true|false>
-          outputParser.expectSources = false;
-          inputStreamController.add('''
-compile-expression abc
-main1
-abc
-abc
-${file.uri}
-
-true
-'''
-              .codeUnits);
+          frontendServer.compileExpression('main1', file.uri, isStatic: true);
           count += 1;
         } else if (count == 1) {
           // Second request was to 'compile-expression', which resulted in
@@ -904,36 +766,29 @@
           expect(outputFile.lengthSync(), isPositive);
 
           file.writeAsStringSync("import 'lib.dart'; main() => foo();\n");
-          inputStreamController.add('recompile ${file.path} abc\n'
-                  '${file.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(file.uri, entryPoint: file.path);
 
           count += 1;
         } else if (count == 2) {
           // Third request was to recompile the script after renaming a function.
           expect(result.errorsCount, 0);
-          outputParser.expectSources = false;
-          inputStreamController.add('reject\n'.codeUnits);
+          frontendServer.reject();
           count += 1;
         } else if (count == 3) {
           // Fourth request was to reject the compilation results.
-          outputParser.expectSources = false;
-          inputStreamController.add(
-              'compile-expression abc\nmain1\nabc\nabc\n${file.uri}\n\ntrue\n'
-                  .codeUnits);
+          frontendServer.compileExpression('main1', file.uri, isStatic: true);
           count += 1;
         } else {
           expect(count, 4);
           // Fifth request was to 'compile-expression' that references original
           // function, which should still be successful.
           expect(result.errorsCount, 0);
-          inputStreamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     }, timeout: Timeout.factor(100));
 
     test('recompile request keeps incremental output dill filename', () async {
@@ -948,36 +803,20 @@
         '--output-dill=${dillFile.path}'
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         if (count == 0) {
           // First request is to 'compile', which results in full kernel file.
           expect(dillFile.existsSync(), equals(true));
           compiledResult.expectNoErrors(filename: dillFile.path);
           count += 1;
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
           var file2 = File('${tempDir.path}/bar.dart')..createSync();
           file2.writeAsStringSync("main() {}\n");
-          inputStreamController.add('recompile ${file2.path} abc\n'
-                  '${file2.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(file2.uri, entryPoint: file2.path);
         } else {
           expect(count, 1);
           // Second request is to 'recompile', which results in incremental
@@ -985,11 +824,12 @@
           var dillIncFile = File('${dillFile.path}.incremental.dill');
           compiledResult.expectNoErrors(filename: dillIncFile.path);
           expect(dillIncFile.existsSync(), equals(true));
-          inputStreamController.add('quit\n'.codeUnits);
+          frontendServer.accept();
+          frontendServer.quit();
         }
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     test(
@@ -1043,30 +883,17 @@
         '--packages=${config.path}',
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         if (count == 0) {
           // First request is to 'compile', which results in full kernel file.
           expect(dillFile.existsSync(), equals(true));
           compiledResult.expectNoErrors(filename: dillFile.path);
           count += 1;
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
           file.writeAsStringSync("""
 import "package:flutter/src/widgets/framework.dart";
 
@@ -1080,10 +907,7 @@
 
 class BarState extends State<FizzWidget> {}
 """);
-          inputStreamController.add('recompile ${file.path} abc\n'
-                  '${file.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(file.uri, entryPoint: file.path);
         } else if (count == 1) {
           expect(count, 1);
           // Second request is to 'recompile', which results in incremental
@@ -1096,7 +920,7 @@
           expect(widgetCacheFile.existsSync(), equals(true));
           expect(widgetCacheFile.readAsStringSync(), 'FooWidget');
           count += 1;
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
           file.writeAsStringSync("""
 import "package:flutter/src/widgets/framework.dart";
@@ -1113,10 +937,7 @@
 
 class BarState extends State<FizzWidget> {}
 """);
-          inputStreamController.add('recompile ${file.path} abc\n'
-                  '${file.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(file.uri, entryPoint: file.path);
         } else if (count == 2) {
           // Second request is to 'recompile', which results in incremental
           // kernel file and invalidation of StatelessWidget.
@@ -1128,7 +949,7 @@
           expect(widgetCacheFile.existsSync(), equals(true));
           expect(widgetCacheFile.readAsStringSync(), 'FizzWidget');
           count += 1;
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
           file.writeAsStringSync("""
 import "package:flutter/src/widgets/framework.dart";
@@ -1147,10 +968,7 @@
   // Added.
 }
 """);
-          inputStreamController.add('recompile ${file.path} abc\n'
-                  '${file.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(file.uri, entryPoint: file.path);
         } else if (count == 3) {
           // Third request is to 'recompile', which results in incremental
           // kernel file and invalidation of State class.
@@ -1162,7 +980,7 @@
           expect(widgetCacheFile.existsSync(), equals(true));
           expect(widgetCacheFile.readAsStringSync(), 'FizzWidget');
           count += 1;
-          inputStreamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
           file.writeAsStringSync("""
 import "package:flutter/src/widgets/framework.dart";
@@ -1183,10 +1001,7 @@
   // Added.
 }
 """);
-          inputStreamController.add('recompile ${file.path} abc\n'
-                  '${file.uri}\n'
-                  'abc\n'
-              .codeUnits);
+          frontendServer.recompile(file.uri, entryPoint: file.path);
         } else if (count == 4) {
           // Fourth request is to 'recompile', which results in incremental
           // kernel file and no widget cache
@@ -1196,11 +1011,12 @@
           compiledResult.expectNoErrors(filename: dillIncFile.path);
           expect(dillIncFile.existsSync(), equals(true));
           expect(widgetCacheFile.existsSync(), equals(false));
-          inputStreamController.add('quit\n'.codeUnits);
+          frontendServer.accept();
+          frontendServer.quit();
         }
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     test('unsafe-package-serialization', () async {
@@ -1262,23 +1078,11 @@
         '--no-incremental-serialization',
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         switch (count) {
@@ -1287,18 +1091,16 @@
             expect(result.filename, dillFile.path);
             expect(result.errorsCount, 0);
             count += 1;
-            inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController.add('reset\n'.codeUnits);
+            frontendServer.accept();
+            frontendServer.reset();
 
-            inputStreamController.add('recompile ${fileB.path} abc\n'
-                    '${fileB.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            frontendServer.recompile(fileB.uri, entryPoint: fileB.path);
             break;
           case 1:
             expect(result.filename, dillFile.path);
             expect(result.errorsCount, 0);
-            inputStreamController.add('quit\n'.codeUnits);
+            frontendServer.accept();
+            frontendServer.quit();
 
             // Loadable.
             Component component = loadComponentFromBinary(dillFile.path);
@@ -1319,7 +1121,7 @@
         }
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     test('incremental-serialization', () async {
@@ -1396,23 +1198,11 @@
         '--incremental-serialization',
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         switch (count) {
@@ -1421,18 +1211,16 @@
             expect(result.filename, dillFile.path);
             expect(result.errorsCount, 0);
             count += 1;
-            inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController.add('reset\n'.codeUnits);
+            frontendServer.accept();
+            frontendServer.reset();
 
-            inputStreamController.add('recompile ${fileB.path} abc\n'
-                    '${fileB.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            frontendServer.recompile(fileB.uri, entryPoint: fileB.path);
             break;
           case 1:
             expect(result.filename, dillFile.path);
             expect(result.errorsCount, 0);
-            inputStreamController.add('quit\n'.codeUnits);
+            frontendServer.accept();
+            frontendServer.quit();
 
             // Loadable.
             Component component = loadComponentFromBinary(dillFile.path);
@@ -1453,7 +1241,7 @@
         }
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     test('incremental-serialization with reject', () async {
@@ -1484,23 +1272,11 @@
         '--incremental-serialization',
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.path}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.path);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         switch (count) {
@@ -1523,16 +1299,12 @@
             file.writeAsStringSync("pkgA() {} pkgA_2() {}");
 
             count += 1;
-            outputParser.expectSources = false;
-            inputStreamController.add('reject\n'.codeUnits);
+            frontendServer.reject();
             break;
           case 1:
             count += 1;
-            inputStreamController.add('reset\n'.codeUnits);
-            inputStreamController.add('recompile ${file.path} abc\n'
-                    '${file.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            frontendServer.reset();
+            frontendServer.recompile(file.uri, entryPoint: file.path);
             break;
           case 2:
             expect(dillFile.existsSync(), equals(true));
@@ -1553,17 +1325,15 @@
             file.writeAsStringSync("pkgA() {} pkgA_2() {} pkgA_3() {}");
 
             count += 1;
-            inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController.add('reset\n'.codeUnits);
-            inputStreamController.add('recompile ${file.path} abc\n'
-                    '${file.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            frontendServer.accept();
+            frontendServer.reset();
+            frontendServer.recompile(file.uri, entryPoint: file.path);
             break;
           case 3:
             expect(result.filename, dillFile.path);
             expect(result.errorsCount, 0);
-            inputStreamController.add('quit\n'.codeUnits);
+            frontendServer.accept();
+            frontendServer.quit();
 
             // Loadable.
             Component component = loadComponentFromBinary(dillFile.path);
@@ -1578,7 +1348,7 @@
         }
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     test('compile and recompile report non-zero error count', () async {
@@ -1593,24 +1363,11 @@
         '--output-dill=${dillFile.path}'
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${file.uri}\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(file.uri.toString());
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         switch (count) {
@@ -1619,37 +1376,33 @@
             expect(result.filename, dillFile.path);
             expect(result.errorsCount, 2);
             count += 1;
-            inputStreamController.add('accept\n'.codeUnits);
+            frontendServer.accept();
             var file2 = File('${tempDir.path}/bar.dart')..createSync();
             file2.writeAsStringSync("main() { baz(); }\n");
-            inputStreamController.add('recompile ${file2.uri} abc\n'
-                    '${file2.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            frontendServer.recompile(file2.uri,
+                entryPoint: file2.uri.toString());
             break;
           case 1:
             var dillIncFile = File('${dillFile.path}.incremental.dill');
             expect(result.filename, dillIncFile.path);
             expect(result.errorsCount, 1);
             count += 1;
-            inputStreamController.add('accept\n'.codeUnits);
+            frontendServer.accept();
             var file2 = File('${tempDir.path}/bar.dart')..createSync();
             file2.writeAsStringSync("main() { }\n");
-            inputStreamController.add('recompile ${file2.uri} abc\n'
-                    '${file2.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            frontendServer.recompile(file2.uri,
+                entryPoint: file2.uri.toString());
             break;
           case 2:
             var dillIncFile = File('${dillFile.path}.incremental.dill');
             expect(result.filename, dillIncFile.path);
             expect(result.errorsCount, 0);
             expect(dillIncFile.existsSync(), equals(true));
-            inputStreamController.add('quit\n'.codeUnits);
+            frontendServer.quit();
         }
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     test('compile and recompile with MultiRootFileSystem', () async {
@@ -1699,23 +1452,10 @@
         '--output-dill=${dillFile.path}'
       ];
 
-      final StreamController<List<int>> inputStreamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: inputStreamController.stream, output: ioSink);
-      inputStreamController.add('compile ${src1.uri}\n'.codeUnits);
-      receivedResults.stream.listen((Result compiledResult) {
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(src1.uri.toString());
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         expect(dillFile.existsSync(), equals(true));
@@ -1730,10 +1470,10 @@
                 .where((lib) => srcUris.contains(lib.fileUri))
                 .length,
             srcUris.length);
-        inputStreamController.add('quit\n'.codeUnits);
+        frontendServer.quit();
       });
       expect(await result, 0);
-      inputStreamController.close();
+      frontendServer.close();
     });
 
     group('http uris', () {
@@ -2152,23 +1892,11 @@
         '--packages=${packages.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       var count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         count++;
@@ -2176,18 +1904,19 @@
           // First request is to 'compile', which results in full JavaScript
           expect(result.errorsCount, equals(0));
           expect(result.filename, dillFile.path);
-          streamController.add('accept\n'.codeUnits);
-          streamController.add('compile foo.bar\n'.codeUnits);
+          frontendServer.accept();
+          frontendServer.compile('foo.bar');
         } else {
           expect(count, 2);
           // Second request is to 'compile' non-existent file, that should fail.
           expect(result.errorsCount, greaterThan(0));
-          streamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
       expect(count, 2);
+      frontendServer.close();
     }, skip: 'https://github.com/dart-lang/sdk/issues/43959');
 
     test('compile to JavaScript with no metadata', () async {
@@ -2232,23 +1961,11 @@
         '--packages=${packageConfig.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       var count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         count++;
@@ -2260,13 +1977,13 @@
         expect(sourceMapsFile.existsSync(), equals(true));
         expect(metadataFile.existsSync(), equals(false));
         expect(result.filename, dillFile.path);
-        streamController.add('accept\n'.codeUnits);
-        outputParser.expectSources = false;
-        streamController.add('quit\n'.codeUnits);
+        frontendServer.accept();
+        frontendServer.quit();
       });
 
       expect(await result, 0);
       expect(count, 1);
+      frontendServer.close();
     });
 
     test('compile to JavaScript with metadata', () async {
@@ -2314,23 +2031,11 @@
         '--emit-debug-symbols',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         count++;
@@ -2342,13 +2047,13 @@
         expect(metadataFile.existsSync(), equals(true));
         expect(symbolsFile.existsSync(), equals(true));
         expect(result.filename, dillFile.path);
-        streamController.add('accept\n'.codeUnits);
-        outputParser.expectSources = false;
-        streamController.add('quit\n'.codeUnits);
+        frontendServer.accept();
+        frontendServer.quit();
       });
 
       expect(await result, 0);
       expect(count, 1);
+      frontendServer.close();
     });
 
     // This test exercises what happens when a change occurs with a single
@@ -2446,23 +2151,11 @@
         '--emit-debug-symbols',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $entryPoint\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(entryPoint);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         switch (count) {
           case 0:
             CompilationResult result =
@@ -2488,7 +2181,7 @@
             expect(jsModules[1], contains('<<d>>'));
             expect(jsModules[1], contains('<<e>>'));
 
-            streamController.add('accept\n'.codeUnits);
+            frontendServer.accept();
 
             fileD.writeAsStringSync("""
 import 'e.dart';
@@ -2498,12 +2191,9 @@
   e();
 }
 """);
-            // Trigger a recompile that invalidates 'd.dart'. The input uri
-            // (a.dart) is passed explicitly.
-            streamController.add('recompile ${entryPoint} abc\n'
-                    '${fileD.uri}\n'
-                    'abc\n'
-                .codeUnits);
+            // Trigger a recompile that invalidates 'd.dart'. The entry point
+            // uri (a.dart) is passed explicitly.
+            frontendServer.recompile(fileD.uri, entryPoint: entryPoint);
             break;
           case 1:
             CompilationResult result =
@@ -2527,7 +2217,7 @@
             expect(jsModules[0], contains('<<d2>>'));
             expect(jsModules[0], contains('<<e>>'));
 
-            streamController.add('accept\n'.codeUnits);
+            frontendServer.accept();
 
             fileE.writeAsStringSync("""
 import 'c.dart';
@@ -2537,13 +2227,9 @@
   "<<e2>>";
 }
 """);
-            // Trigger a recompile that invalidates 'd.dart'. The input uri
-            // (a.dart) is omitted.
-            streamController.add('recompile abc\n'
-                    '${fileE.uri}\n'
-                    'abc\n'
-                .codeUnits);
-            break;
+            // Trigger a recompile that invalidates 'd.dart'. The entry point
+            // uri (a.dart) is omitted.
+            frontendServer.recompile(fileE.uri);
             break;
           case 2:
             CompilationResult result =
@@ -2569,9 +2255,8 @@
             expect(jsModules[0], contains('<<e1>>'));
             expect(jsModules[0], contains('<<e2>>'));
 
-            streamController.add('accept\n'.codeUnits);
-            outputParser.expectSources = false;
-            streamController.add('quit\n'.codeUnits);
+            frontendServer.accept();
+            frontendServer.quit();
             break;
           default:
             break;
@@ -2581,6 +2266,7 @@
 
       expect(await result, 0);
       expect(count, 3);
+      frontendServer.close();
     });
 
     test('compile to JavaScript all modules with unsound null safety',
@@ -2622,24 +2308,12 @@
         '--packages=${packageConfig.path}'
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       var count = 0;
       var expectationCompleter = Completer<bool>();
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         count++;
@@ -2658,15 +2332,15 @@
             jsModules[0], contains('dart._checkModuleNullSafetyMode(false);'));
         expect(
             jsModules[1], contains('dart._checkModuleNullSafetyMode(false);'));
-        streamController.add('accept\n'.codeUnits);
-        outputParser.expectSources = false;
-        streamController.add('quit\n'.codeUnits);
+        frontendServer.accept();
+        frontendServer.quit();
         expectationCompleter.complete(true);
       });
 
       await expectationCompleter.future;
       expect(await result, 0);
       expect(count, 1);
+      frontendServer.close();
     }, timeout: Timeout.none);
 
     test('compile to JavaScript, all modules with sound null safety', () async {
@@ -2706,24 +2380,12 @@
         '--packages=${packageConfig.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       var count = 0;
       var expectationCompleter = Completer<bool>();
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         count++;
@@ -2742,15 +2404,15 @@
             jsModules[0], contains('dart._checkModuleNullSafetyMode(true);'));
         expect(
             jsModules[1], contains('dart._checkModuleNullSafetyMode(true);'));
-        streamController.add('accept\n'.codeUnits);
-        outputParser.expectSources = false;
-        streamController.add('quit\n'.codeUnits);
+        frontendServer.accept();
+        frontendServer.quit();
         expectationCompleter.complete(true);
       });
 
       await expectationCompleter.future;
       expect(await result, 0);
       expect(count, 1);
+      frontendServer.close();
     });
 
     test('compile expression to Javascript', () async {
@@ -2790,23 +2452,11 @@
         '--packages=${packageConfig.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         if (count == 0) {
@@ -2816,24 +2466,9 @@
           expect(manifestFile.existsSync(), equals(true));
           expect(sourceMapsFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          streamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
-          // 'compile-expression-to-js <boundarykey>
-          // libraryUri
-          // line
-          // column
-          // jsModules (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // jsFrameValues (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // moduleName
-          // expression
-          outputParser.expectSources = false;
-          streamController.add('compile-expression-to-js abc\n'
-                  '$library\n2\n1\nabc\nabc\n$module\n\n'
-              .codeUnits);
+          frontendServer.compileExpressionToJs('', library, 2, 1, module);
           count += 1;
         } else if (count == 1) {
           // Second request is to 'compile-expression-to-js' that fails
@@ -2843,10 +2478,7 @@
             return status.endsWith(' 1');
           });
 
-          outputParser.expectSources = false;
-          streamController.add('compile-expression-to-js abc\n'
-                  '$library\n2\n1\nabc\nabc\n$module\n2+2\n'
-              .codeUnits);
+          frontendServer.compileExpressionToJs('2+2', library, 2, 1, module);
           count += 1;
         } else if (count == 2) {
           expect(result.errorsCount, equals(0));
@@ -2856,14 +2488,14 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          streamController.add('compile foo.bar\n'.codeUnits);
+          frontendServer.compile('foo.bar');
           count += 1;
         } else {
           expect(count, 3);
           // Fourth request is to 'compile' non-existent file, that should fail.
           expect(result.errorsCount, greaterThan(0));
 
-          streamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
 
@@ -2910,23 +2542,11 @@
         '--packages=${packageConfig.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         if (count == 0) {
@@ -2942,25 +2562,14 @@
           // literal.
           expect(compiledOutput, contains('print(true);'));
 
-          streamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
-          // 'compile-expression-to-js <boundarykey>
-          // libraryUri
-          // line
-          // column
-          // jsModules (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // jsFrameValues (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // moduleName
-          // expression
-          outputParser.expectSources = false;
-          streamController.add('compile-expression-to-js abc\n'
-                  '$library\n2\n1\nabc\nabc\n$module\n'
-                  'const bool.fromEnvironment("dart.library.html")\n'
-              .codeUnits);
+          frontendServer.compileExpressionToJs(
+              'const bool.fromEnvironment("dart.library.html")',
+              library,
+              2,
+              1,
+              module);
           count += 1;
         } else {
           expect(count, 1);
@@ -2971,13 +2580,13 @@
           // The constant environment variable should be inlined as a boolean
           // literal.
           expect(resultFile.readAsStringSync(), contains('return true;'));
-          outputParser.expectSources = false;
           count += 1;
-          streamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
       expect(await result, 0);
       expect(count, 2);
+      frontendServer.close();
     });
 
     test('mixed compile expression commands with web target', () async {
@@ -3016,23 +2625,11 @@
         '--packages=${packageConfig.path}',
       ];
 
-      final StreamController<List<int>> streamController =
-          StreamController<List<int>>();
-      final StreamController<List<int>> stdoutStreamController =
-          StreamController<List<int>>();
-      final IOSink ioSink = IOSink(stdoutStreamController.sink);
-      StreamController<Result> receivedResults = StreamController<Result>();
-      final outputParser = OutputParser(receivedResults);
-      stdoutStreamController.stream
-          .transform(utf8.decoder)
-          .transform(const LineSplitter())
-          .listen(outputParser.listener);
-
-      Future<int> result =
-          starter(args, input: streamController.stream, output: ioSink);
-      streamController.add('compile $library\n'.codeUnits);
+      final frontendServer = FrontendServer();
+      Future<int> result = frontendServer.open(args);
+      frontendServer.compile(library);
       int count = 0;
-      receivedResults.stream.listen((Result compiledResult) {
+      frontendServer.listen((Result compiledResult) {
         CompilationResult result =
             CompilationResult.parse(compiledResult.status);
         if (count == 0) {
@@ -3042,24 +2639,9 @@
           expect(manifestFile.existsSync(), equals(true));
           expect(sourceMapsFile.existsSync(), equals(true));
           expect(result.filename, dillFile.path);
-          streamController.add('accept\n'.codeUnits);
+          frontendServer.accept();
 
-          // 'compile-expression-to-js <boundarykey>
-          // libraryUri
-          // line
-          // column
-          // jsModules (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // jsFrameValues (one k-v pair per line)
-          // ...
-          // <boundarykey>
-          // moduleName
-          // expression
-          outputParser.expectSources = false;
-          streamController.add('compile-expression-to-js abc\n'
-                  '$library\n2\n1\nabc\nabc\n$module\n2+2\n'
-              .codeUnits);
+          frontendServer.compileExpressionToJs('2+2', library, 2, 1, module);
           count += 1;
         } else if (count == 1) {
           expect(result.errorsCount, equals(0));
@@ -3069,21 +2651,7 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          // 'compile-expression <boundarykey>
-          // expression
-          // definitions (one per line)
-          // ...
-          // <boundarykey>
-          // type-definitions (one per line)
-          // ...
-          // <boundarykey>
-          // <libraryUri: String>
-          // <klass: String>
-          // <isStatic: true|false>
-          outputParser.expectSources = false;
-          streamController.add('compile-expression abc\n'
-                  '2+2\nabc\nabc\n${file.uri}\n\nfalse\n'
-              .codeUnits);
+          frontendServer.compileExpression('2+2', file.uri, isStatic: false);
           count += 1;
         } else if (count == 2) {
           expect(result.errorsCount, equals(0));
@@ -3093,10 +2661,7 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          outputParser.expectSources = false;
-          streamController.add('compile-expression-to-js abc\n'
-                  '$library\n2\n1\nabc\nabc\n$module\n2+2\n'
-              .codeUnits);
+          frontendServer.compileExpressionToJs('2+2', library, 2, 1, module);
           count += 1;
         } else if (count == 3) {
           expect(result.errorsCount, equals(0));
@@ -3106,12 +2671,13 @@
           expect(outputFile.existsSync(), equals(true));
           expect(outputFile.lengthSync(), isPositive);
 
-          streamController.add('quit\n'.codeUnits);
+          frontendServer.quit();
         }
       });
 
       expect(await result, 0);
       expect(count, 3);
+      frontendServer.close();
     });
 
     test('compile "package:"-file', () async {
@@ -3212,24 +2778,11 @@
       List<List<int>> compiledKernels = <List<int>>[];
       for (int serverCloses = 0; serverCloses < 2; ++serverCloses) {
         print("Restart #$serverCloses");
-        final StreamController<List<int>> inputStreamController =
-            StreamController<List<int>>();
-        final StreamController<List<int>> stdoutStreamController =
-            StreamController<List<int>>();
-        final IOSink ioSink = IOSink(stdoutStreamController.sink);
-        StreamController<Result> receivedResults = StreamController<Result>();
-
-        final outputParser = OutputParser(receivedResults);
-        stdoutStreamController.stream
-            .transform(utf8.decoder)
-            .transform(const LineSplitter())
-            .listen(outputParser.listener);
-
-        Future<int> result =
-            starter(args, input: inputStreamController.stream, output: ioSink);
-        inputStreamController.add('compile ${dart2js.path}\n'.codeUnits);
+        final frontendServer = FrontendServer();
+        Future<int> result = frontendServer.open(args);
+        frontendServer.compile(dart2js.path);
         int count = 0;
-        receivedResults.stream.listen((Result compiledResult) {
+        frontendServer.listen((Result compiledResult) {
           CompilationResult result =
               CompilationResult.parse(compiledResult.status);
           String outputFilename = result.filename;
@@ -3275,11 +2828,10 @@
             count += 1;
 
             // Restart with no changes
-            inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController.add('reset\n'.codeUnits);
-            inputStreamController.add('recompile ${dart2js.path} x$count\n'
-                    'x$count\n'
-                .codeUnits);
+            frontendServer.accept();
+            frontendServer.reset();
+            frontendServer.recompile(null,
+                entryPoint: dart2js.path, boundaryKey: 'x$count');
           } else if (count == 1) {
             // Restart. Expect full kernel file.
             expect(dillFile.existsSync(), equals(true));
@@ -3301,10 +2853,9 @@
             count += 1;
 
             // Reload with no changes
-            inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController.add('recompile ${dart2js.path} x$count\n'
-                    'x$count\n'
-                .codeUnits);
+            frontendServer.accept();
+            frontendServer.recompile(null,
+                entryPoint: dart2js.path, boundaryKey: 'x$count');
           } else if (count == 2) {
             // Partial file. Expect to be empty.
             expect(incrementalDillFile.existsSync(), equals(true));
@@ -3318,11 +2869,9 @@
             count += 1;
 
             // Reload with 1 change
-            inputStreamController.add('accept\n'.codeUnits);
-            inputStreamController.add('recompile ${dart2js.path} x$count\n'
-                    '${dart2jsOtherFile.uri}\n'
-                    'x$count\n'
-                .codeUnits);
+            frontendServer.accept();
+            frontendServer.recompile(dart2jsOtherFile.uri,
+                entryPoint: dart2js.path, boundaryKey: 'x$count');
           } else if (count == 3) {
             // Partial file. Expect to not be empty.
             expect(incrementalDillFile.existsSync(), equals(true));
@@ -3338,11 +2887,11 @@
 
             count += 1;
 
-            inputStreamController.add('quit\n'.codeUnits);
+            frontendServer.quit();
           }
         });
         expect(await result, 0);
-        inputStreamController.close();
+        frontendServer.close();
       }
     }, timeout: Timeout.factor(8));
 
@@ -3525,3 +3074,171 @@
     return !matcher.matches(item, matchState);
   }
 }
+
+/// Wrapper for the frontend server communication.
+class FrontendServer {
+  final StreamController<List<int>> inputStreamController;
+  final StreamController<List<int>> stdoutStreamController;
+  final IOSink ioSink;
+  final StreamController<Result> receivedResults;
+  final OutputParser outputParser;
+
+  factory FrontendServer() {
+    final StreamController<List<int>> inputStreamController =
+        StreamController<List<int>>();
+    final StreamController<List<int>> stdoutStreamController =
+        StreamController<List<int>>();
+    final IOSink ioSink = IOSink(stdoutStreamController.sink);
+    StreamController<Result> receivedResults = StreamController<Result>();
+    final outputParser = OutputParser(receivedResults);
+    stdoutStreamController.stream
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen(outputParser.listener);
+    return new FrontendServer._internal(inputStreamController,
+        stdoutStreamController, ioSink, receivedResults, outputParser);
+  }
+
+  FrontendServer._internal(
+      this.inputStreamController,
+      this.stdoutStreamController,
+      this.ioSink,
+      this.receivedResults,
+      this.outputParser);
+
+  /// Sets up the front end server using the provided commandline arguments
+  /// [args].
+  Future<int> open(List<String> args) =>
+      starter(args, input: inputStreamController.stream, output: ioSink);
+
+  /// Closes the listener stream.
+  void close() => inputStreamController.close();
+
+  /// Sets up [f] to be called for each [Result] received from the frontend
+  /// server.
+  void listen(void Function(Result) f) => receivedResults.stream.listen(f);
+
+  /// Accepts the last compilation delta.
+  void accept() {
+    outputParser.expectSources = false;
+    inputStreamController.add('accept\n'.codeUnits);
+  }
+
+  /// Rejects the last compilation delta.
+  void reject() {
+    outputParser.expectSources = false;
+    inputStreamController.add('reject\n'.codeUnits);
+  }
+
+  /// Resets the incremental compiler.
+  void reset() {
+    outputParser.expectSources = false;
+    inputStreamController.add('reset\n'.codeUnits);
+  }
+
+  /// Terminates the frontend server.
+  void quit() {
+    outputParser.expectSources = false;
+    inputStreamController.add('quit\n'.codeUnits);
+  }
+
+  /// Compiles the program from entry point [path].
+  // TODO(johnniwinther): Use (required) named arguments.
+  void compile(String path) {
+    outputParser.expectSources = true;
+    inputStreamController.add('compile ${path}\n'.codeUnits);
+  }
+
+  /// Recompiles the program.
+  ///
+  /// [invalidatedUri] and [invalidatedUris] define which libraries that
+  /// need recompilation.
+  ///
+  /// [entryPoint] defines the program entry-point. If not provided, the
+  /// original entry point is used.
+  ///
+  /// [boundaryKey] is used as the boundary-key in the communication with the
+  /// frontend server.
+  // TODO(johnniwinther): Use (required) named arguments.
+  void recompile(Uri invalidatedUri,
+      {String boundaryKey: 'abc',
+      List<Uri> invalidatedUris,
+      String entryPoint}) {
+    invalidatedUris ??= [if (invalidatedUri != null) invalidatedUri];
+    outputParser.expectSources = true;
+    inputStreamController.add('recompile '
+            '${entryPoint != null ? '${entryPoint} ' : ''}'
+            '${boundaryKey}\n'
+            '${invalidatedUris.map((uri) => '$uri\n').join()}'
+            '${boundaryKey}\n'
+        .codeUnits);
+  }
+
+  /// Compiles the [expression] as if it occurs in [library].
+  ///
+  /// If [className] is provided, [expression] is compiled as if it occurs in
+  /// the class of that name.
+  ///
+  /// If [isStatic] is `true`, the expression is compiled in the static scope
+  /// of [className]. If [className] is `null`, this must be `false`.
+  ///
+  /// [boundaryKey] is used as the boundary-key in the communication with the
+  /// frontend server.
+  // TODO(johnniwinther): Use (required) named arguments.
+  void compileExpression(String expression, Uri library,
+      {String boundaryKey: 'abc', String className: '', bool isStatic}) {
+    // 'compile-expression <boundarykey>
+    // expression
+    // definitions (one per line)
+    // ...
+    // <boundarykey>
+    // type-definitions (one per line)
+    // ...
+    // <boundarykey>
+    // <libraryUri: String>
+    // <klass: String>
+    // <isStatic: true|false>
+    outputParser.expectSources = false;
+    inputStreamController.add('compile-expression ${boundaryKey}\n'
+            '${expression}\n'
+            '${boundaryKey}\n'
+            '${boundaryKey}\n'
+            '${library}\n'
+            '${className}\n'
+            '${isStatic != null ? '$isStatic' : ''}\n'
+        .codeUnits);
+  }
+
+  /// Compiles the [expression] to JavaScript as if it occurs in [line] and
+  /// [column] of [library].
+  ///
+  /// [boundaryKey] is used as the boundary-key in the communication with the
+  /// frontend server.
+  // TODO(johnniwinther): Use (required) named arguments.
+  void compileExpressionToJs(String expression, String libraryUri, int line,
+      int column, String moduleName,
+      {String boundaryKey: 'abc'}) {
+    // 'compile-expression-to-js <boundarykey>
+    // libraryUri
+    // line
+    // column
+    // jsModules (one k-v pair per line)
+    // ...
+    // <boundarykey>
+    // jsFrameValues (one k-v pair per line)
+    // ...
+    // <boundarykey>
+    // moduleName
+    // expression
+    outputParser.expectSources = false;
+    inputStreamController.add('compile-expression-to-js ${boundaryKey}\n'
+            '${libraryUri}\n'
+            '${line}\n'
+            '${column}\n'
+            '${boundaryKey}\n'
+            '${boundaryKey}\n'
+            '${moduleName}\n'
+            '${expression}\n'
+        .codeUnits);
+  }
+}