| // Copyright (c) 2017, 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. | 
 |  | 
 | /// Integration test that runs the incremental compiler, runs the compiled | 
 | /// program, incrementally rebuild portions of the app, and triggers a hot | 
 | /// reload on the running program. | 
 | library front_end.incremental.hot_reload_e2e_test; | 
 |  | 
 | import 'dart:async' show Completer; | 
 |  | 
 | import 'dart:convert' show LineSplitter, utf8; | 
 |  | 
 | import 'dart:io' show Directory, File, Platform, Process; | 
 |  | 
 | import 'package:async_helper/async_helper.dart' show asyncTest; | 
 |  | 
 | import 'package:expect/expect.dart' show Expect; | 
 |  | 
 | import 'package:kernel/ast.dart' show Component; | 
 |  | 
 | import 'package:kernel/binary/ast_to_binary.dart'; | 
 |  | 
 | import 'package:front_end/src/api_prototype/compiler_options.dart' | 
 |     show CompilerOptions; | 
 |  | 
 | import 'package:front_end/src/api_prototype/file_system.dart' show FileSystem; | 
 |  | 
 | import 'package:front_end/src/api_prototype/incremental_kernel_generator.dart' | 
 |     show IncrementalKernelGenerator; | 
 |  | 
 | import 'package:front_end/src/api_prototype/memory_file_system.dart' | 
 |     show MemoryFileSystem; | 
 |  | 
 | import 'package:front_end/src/compute_platform_binaries_location.dart' | 
 |     show computePlatformBinariesLocation; | 
 |  | 
 | import 'package:front_end/src/fasta/hybrid_file_system.dart' | 
 |     show HybridFileSystem; | 
 | import 'package:kernel/target/targets.dart'; | 
 | import 'package:vm/target/vm.dart'; | 
 |  | 
 | import 'tool/reload.dart' show RemoteVm; | 
 |  | 
 | abstract class TestCase { | 
 |   late IncrementalKernelGenerator compiler; | 
 |   late MemoryFileSystem fs; | 
 |   late Directory outDir; | 
 |   late Uri outputUri; | 
 |   List<Future<String>> lines = const []; | 
 |   late Future programIsDone; | 
 |  | 
 |   String get name; | 
 |  | 
 |   Future run(); | 
 |  | 
 |   Future test() async { | 
 |     await setUp(); | 
 |     try { | 
 |       await run(); | 
 |       print("$name done"); | 
 |     } finally { | 
 |       await tearDown(); | 
 |     } | 
 |   } | 
 |  | 
 |   Future<void> setUp() async { | 
 |     outDir = Directory.systemTemp.createTempSync('hotreload_test'); | 
 |     outputUri = outDir.uri.resolve('test.dill'); | 
 |     var root = Uri.parse('org-dartlang-test:///'); | 
 |     fs = new MemoryFileSystem(root); | 
 |     fs.entityForUri(root).createDirectory(); | 
 |     writeFile(fs, 'a.dart', sourceA); | 
 |     writeFile(fs, 'b.dart', sourceB); | 
 |     writeFile(fs, 'c.dart', sourceC); | 
 |     writeFile(fs, '.packages', ''); | 
 |     compiler = createIncrementalCompiler( | 
 |         'org-dartlang-test:///a.dart', new HybridFileSystem(fs)); | 
 |     await rebuild(compiler, outputUri); // this is a full compile. | 
 |   } | 
 |  | 
 |   Future<void> tearDown() async { | 
 |     outDir.deleteSync(recursive: true); | 
 |     lines = const []; | 
 |   } | 
 |  | 
 |   Future<int> computeVmPort() async { | 
 |     var portLine = await lines[0]; | 
 |     Expect.isTrue(dartVMServicePortRegExp.hasMatch(portLine)); | 
 |     var match = dartVMServicePortRegExp.firstMatch(portLine); | 
 |     return int.parse(match!.group(1)!); | 
 |   } | 
 |  | 
 |   /// Request vm to resume execution | 
 |   Future resume() async { | 
 |     var port = await computeVmPort(); | 
 |     var remoteVm = new RemoteVm(port); | 
 |     await remoteVm.resume(); | 
 |   } | 
 |  | 
 |   /// Start the VM with the first version of the program compiled by the | 
 |   /// incremental compiler. | 
 |   Future<void> startProgram(int reloadCount) async { | 
 |     var vmArgs = [ | 
 |       '--enable-vm-service=0', // Note: use 0 to avoid port collisions. | 
 |       '--pause_isolates_on_start', | 
 |       '--disable-service-auth-codes', | 
 |       // TODO(bkonyi): The service isolate starts before DartDev has a chance | 
 |       // to spawn DDS. We should suppress the Observatory message until DDS | 
 |       // starts (#42727). | 
 |       '--disable-dart-dev', | 
 |       outputUri.toFilePath() | 
 |     ]; | 
 |     vmArgs.add('$reloadCount'); | 
 |     var vm = await Process.start(Platform.executable, vmArgs); | 
 |     var splitter = new LineSplitter(); | 
 |  | 
 |     /// The program prints at most 2 + reloadCount lines: | 
 |     ///  - a line displaying the observatory port | 
 |     ///  - a line before waiting for a reload | 
 |     ///  - a line after each hot-reload | 
 |     int i = 0; | 
 |     int expectedLines = 2 + reloadCount; | 
 |     var completers = | 
 |         new List.generate(expectedLines, (_) => new Completer<String>()); | 
 |     lines = completers.map((c) => c.future).toList(); | 
 |     vm.stdout.transform(utf8.decoder).transform(splitter).listen((line) { | 
 |       Expect.isTrue(i < expectedLines); | 
 |       completers[i++].complete(line); | 
 |     }, onDone: () { | 
 |       Expect.equals(expectedLines, i); | 
 |     }); | 
 |  | 
 |     // ignore: unawaited_futures | 
 |     vm.stderr.transform(utf8.decoder).transform(splitter).toList().then((err) { | 
 |       Expect.isTrue(err.isEmpty, err.join('\n')); | 
 |     }); | 
 |  | 
 |     programIsDone = vm.exitCode; | 
 |     await resume(); | 
 |   } | 
 |  | 
 |   /// Request a hot reload on the running program. | 
 |   Future hotReload() async { | 
 |     var port = await computeVmPort(); | 
 |     var remoteVm = new RemoteVm(port); | 
 |     var reloadResult = await remoteVm.reload(outputUri); | 
 |     Expect.isTrue(reloadResult['success']); | 
 |     await remoteVm.disconnect(); | 
 |   } | 
 | } | 
 |  | 
 | class InitialProgramIsValid extends TestCase { | 
 |   @override | 
 |   String get name => 'initial program is valid'; | 
 |  | 
 |   @override | 
 |   Future run() async { | 
 |     await startProgram(0); | 
 |     await programIsDone; | 
 |     Expect.stringEquals("part1 part2", await lines[1]); | 
 |   } | 
 | } | 
 |  | 
 | class ReloadAfterLeafLibraryModification extends TestCase { | 
 |   @override | 
 |   String get name => 'reload after leaf library modification'; | 
 |  | 
 |   @override | 
 |   Future run() async { | 
 |     await startProgram(1); | 
 |     Expect.stringEquals("part1 part2", await lines[1]); | 
 |  | 
 |     writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part3")); | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     await programIsDone; | 
 |     Expect.stringEquals("part3 part2", await lines[2]); | 
 |   } | 
 | } | 
 |  | 
 | class ReloadAfterNonLeafLibraryModification extends TestCase { | 
 |   @override | 
 |   String get name => "reload after non-leaf library modification"; | 
 |  | 
 |   @override | 
 |   Future run() async { | 
 |     await startProgram(1); | 
 |     Expect.stringEquals("part1 part2", await lines[1]); | 
 |  | 
 |     writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part4")); | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     await programIsDone; | 
 |     Expect.stringEquals("part1 part4", await lines[2]); | 
 |   } | 
 | } | 
 |  | 
 | class ReloadAfterWholeProgramModification extends TestCase { | 
 |   @override | 
 |   String get name => "reload after whole program modification"; | 
 |  | 
 |   @override | 
 |   Future run() async { | 
 |     await startProgram(1); | 
 |     Expect.stringEquals("part1 part2", await lines[1]); | 
 |  | 
 |     writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part5")); | 
 |     writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part6")); | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     await programIsDone; | 
 |     Expect.stringEquals("part5 part6", await lines[2]); | 
 |   } | 
 | } | 
 |  | 
 | class ReloadTwice extends TestCase { | 
 |   @override | 
 |   String get name => "reload twice"; | 
 |  | 
 |   @override | 
 |   Future run() async { | 
 |     await startProgram(2); | 
 |     Expect.stringEquals("part1 part2", await lines[1]); | 
 |  | 
 |     writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part5")); | 
 |     writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part6")); | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     Expect.stringEquals("part5 part6", await lines[2]); | 
 |  | 
 |     writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part7")); | 
 |     writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part8")); | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     await programIsDone; | 
 |     Expect.stringEquals("part7 part8", await lines[3]); | 
 |   } | 
 | } | 
 |  | 
 | class ReloadToplevelField extends TestCase { | 
 |   @override | 
 |   String get name => "reload top level field"; | 
 |  | 
 |   @override | 
 |   Future run() async { | 
 |     await startProgram(2); | 
 |     Expect.stringEquals("part1 part2", await lines[1]); | 
 |  | 
 |     writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part3")); | 
 |     writeFile(fs, 'c.dart', r""" | 
 |       void g() { | 
 |         bField.a("a"); | 
 |       } | 
 |  | 
 |       class B { | 
 |         dynamic a; | 
 |         B({this.a}); | 
 |       } | 
 |  | 
 |       var bField = new B(a: (String s) => "$s"); | 
 |     """); | 
 |  | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     Expect.stringEquals("part3 part2", await lines[2]); | 
 |  | 
 |     writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part4")); | 
 |     writeFile(fs, 'c.dart', r""" | 
 |       void g() { | 
 |         bField.a("a"); | 
 |       } | 
 |  | 
 |       class B { | 
 |         dynamic a; | 
 |         dynamic b; | 
 |         B({this.a}); | 
 |       } | 
 |  | 
 |       var bField = new B(a: (String s) => "$s"); | 
 |     """); | 
 |  | 
 |     await rebuild(compiler, outputUri); | 
 |     await hotReload(); | 
 |     Expect.stringEquals("part4 part2", await lines[3]); | 
 |     await programIsDone; | 
 |   } | 
 | } | 
 |  | 
 | void main() { | 
 |   asyncTest(() async { | 
 |     await new InitialProgramIsValid().test(); | 
 |     await new ReloadAfterLeafLibraryModification().test(); | 
 |     await new ReloadAfterNonLeafLibraryModification().test(); | 
 |     await new ReloadAfterWholeProgramModification().test(); | 
 |     await new ReloadTwice().test(); | 
 |     await new ReloadToplevelField().test(); | 
 |   }); | 
 | } | 
 |  | 
 | final Uri sdkRoot = computePlatformBinariesLocation(forceBuildDir: true); | 
 |  | 
 | IncrementalKernelGenerator createIncrementalCompiler( | 
 |     String entry, FileSystem fs) { | 
 |   var entryUri = Uri.base.resolve(entry); | 
 |   var options = new CompilerOptions() | 
 |     ..sdkRoot = sdkRoot | 
 |     ..librariesSpecificationUri = Uri.base.resolve("sdk/lib/libraries.json") | 
 |     ..fileSystem = fs | 
 |     ..target = new VmTarget(new TargetFlags()) | 
 |     ..environmentDefines = {}; | 
 |   return new IncrementalKernelGenerator(options, [entryUri]); | 
 | } | 
 |  | 
 | Future<bool> rebuild(IncrementalKernelGenerator compiler, Uri outputUri) async { | 
 |   compiler.invalidate(Uri.parse("org-dartlang-test:///a.dart")); | 
 |   compiler.invalidate(Uri.parse("org-dartlang-test:///b.dart")); | 
 |   compiler.invalidate(Uri.parse("org-dartlang-test:///c.dart")); | 
 |   var compilerResult = await compiler.computeDelta(); | 
 |   var component = compilerResult.component; | 
 |   if (!component.libraries.isEmpty) { | 
 |     await writeProgram(component, outputUri); | 
 |     return true; | 
 |   } | 
 |   return false; | 
 | } | 
 |  | 
 | Future<Null> writeProgram(Component component, Uri outputUri) async { | 
 |   var sink = new File.fromUri(outputUri).openWrite(); | 
 |   // TODO(sigmund): the incremental generator should always filter these | 
 |   // libraries instead. | 
 |   new BinaryPrinter(sink, | 
 |           libraryFilter: (library) => !library.importUri.isScheme('dart')) | 
 |       .writeComponentFile(component); | 
 |   await sink.close(); | 
 | } | 
 |  | 
 | void writeFile(MemoryFileSystem fs, String fileName, String contents) { | 
 |   fs | 
 |       .entityForUri(Uri.parse('org-dartlang-test:///$fileName')) | 
 |       .writeAsStringSync(contents); | 
 | } | 
 |  | 
 | /// This program calls a function periodically and tracks when the function | 
 | /// returns a different value than before (which only happens after a | 
 | /// hot-reload). The program exits after certain number of reloads, specified as | 
 | /// an argument to main. | 
 | const sourceA = r''' | 
 | import 'dart:async'; | 
 | import 'b.dart'; | 
 | import 'c.dart'; | 
 |  | 
 | void main(List<String> args) { | 
 |   var last = f(); | 
 |   print(last); | 
 |   g(); | 
 |  | 
 |   // The argument indicates how many "visible" hot-reloads to run | 
 |   int reloadCount = 0; | 
 |   if (args.length > 0) { | 
 |     reloadCount = int.parse(args[0]); | 
 |   } | 
 |   if (reloadCount == 0) return; | 
 |  | 
 |   new Timer.periodic(new Duration(milliseconds: 100), (timer) { | 
 |     var result = f(); | 
 |     if (last != result) { | 
 |       print(result); | 
 |       g(); | 
 |       last = result; | 
 |       if (--reloadCount == 0) timer.cancel(); | 
 |     } | 
 |   }); | 
 | } | 
 |  | 
 | f() => "$line part2"; | 
 | '''; | 
 |  | 
 | const sourceB = r''' | 
 | get line => "part1"; | 
 | '''; | 
 |  | 
 | const sourceC = r''' | 
 | void g() {} | 
 | '''; | 
 |  | 
 | RegExp dartVMServicePortRegExp = new RegExp( | 
 |     "The Dart VM service is listening on http://127.0.0.1:\([0-9]*\)/"); |