| // 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: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/base/hybrid_file_system.dart' |
| show HybridFileSystem; |
| import 'package:front_end/src/compute_platform_binaries_location.dart' |
| show computePlatformBinariesLocation; |
| import 'package:kernel/ast.dart' show Component; |
| import 'package:kernel/binary/ast_to_binary.dart'; |
| import 'package:kernel/target/targets.dart'; |
| import 'package:vm/modular/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, '.dart_tool/package_config.json', |
| '{"configVersion": 2, "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]*\)/"); |