Index: pkg/front_end/test/src/incremental/hot_reload_e2e_test.dart |
diff --git a/pkg/front_end/test/src/incremental/hot_reload_e2e_test.dart b/pkg/front_end/test/src/incremental/hot_reload_e2e_test.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..728087c0ab7f976293caf865cac2f46a2af787ce |
--- /dev/null |
+++ b/pkg/front_end/test/src/incremental/hot_reload_e2e_test.dart |
@@ -0,0 +1,245 @@ |
+// 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'; |
+import 'dart:convert'; |
+import 'dart:io'; |
+ |
+import 'package:front_end/compiler_options.dart'; |
+import 'package:front_end/file_system.dart'; |
+import 'package:front_end/incremental_kernel_generator.dart'; |
+import 'package:front_end/memory_file_system.dart'; |
+import 'package:front_end/src/incremental/byte_store.dart'; |
+import 'package:front_end/src/testing/hybrid_file_system.dart'; |
+import 'package:front_end/src/vm/reload.dart'; |
+import 'package:kernel/ast.dart'; |
+import 'package:kernel/binary/limited_ast_to_binary.dart'; |
+import 'package:test/test.dart'; |
+ |
+main() { |
+ IncrementalKernelGenerator compiler; |
+ MemoryFileSystem fs; |
+ Directory outDir; |
+ Uri outputUri; |
+ List<Future<String>> lines; |
+ Future programIsDone; |
+ |
+ setUp(() async { |
+ outDir = Directory.systemTemp.createTempSync('hotreload_test'); |
+ outputUri = outDir.uri.resolve('test.dill'); |
+ fs = new MemoryFileSystem(Uri.parse('file:///')); |
+ writeFile(fs, 'a.dart', sourceA); |
+ writeFile(fs, 'b.dart', sourceB); |
+ writeFile(fs, '.packages', ''); |
+ compiler = await createIncrementalCompiler( |
+ 'file:///a.dart', new HybridFileSystem(fs)); |
+ await rebuild(compiler, outputUri); // this is a full compile. |
+ }); |
+ |
+ tearDown(() async { |
+ outDir.deleteSync(recursive: true); |
+ lines = null; |
+ }); |
+ |
+ /// Start the VM with the first version of the program compiled by the |
+ /// incremental compiler. |
+ startProgram(int reloadCount) async { |
+ var vmArgs = [ |
+ '--enable-vm-service=0', // Note: use 0 to avoid port collisions. |
+ '--platform=${platformFile.toFilePath()}', |
+ 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(i, lessThan(expectedLines)); |
+ completers[i++].complete(line); |
+ }, onDone: () { |
+ expect(i, expectedLines); |
+ }); |
+ |
+ vm.stderr.transform(UTF8.decoder).transform(splitter).toList().then((err) { |
+ expect(err, isEmpty, reason: err.join('\n')); |
+ }); |
+ |
+ programIsDone = vm.exitCode; |
+ } |
+ |
+ /// Request a hot reload on the running program. |
+ Future hotReload() async { |
+ var portLine = await lines[0]; |
+ expect(observatoryPortRegExp.hasMatch(portLine), isTrue); |
+ var match = observatoryPortRegExp.firstMatch(portLine); |
+ var port = int.parse(match.group(1)); |
+ var reloader = new VmReloader(port); |
+ var reloadResult = await reloader.reload(outputUri); |
+ expect(reloadResult['success'], isTrue); |
+ await reloader.disconnect(); |
+ } |
+ |
+ test('initial program is valid', () async { |
+ await startProgram(0); |
+ await programIsDone; |
+ expect(await lines.skip(1).first, "part1 part2"); |
+ }); |
+ |
+ test('reload after leaf library modification', () async { |
+ await startProgram(1); |
+ expect(await lines[1], "part1 part2"); |
+ |
+ writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part3")); |
+ await rebuild(compiler, outputUri); |
+ await hotReload(); |
+ await programIsDone; |
+ expect(await lines[2], "part3 part2"); |
+ }); |
+ |
+ test('reload after non-leaf library modification', () async { |
+ await startProgram(1); |
+ expect(await lines[1], "part1 part2"); |
+ |
+ writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part4")); |
+ await rebuild(compiler, outputUri); |
+ await hotReload(); |
+ await programIsDone; |
+ expect(await lines[2], "part1 part4"); |
+ }, skip: true /* VM crashes on reload */); |
+ |
+ test('reload after whole program modification', () async { |
+ await startProgram(1); |
+ expect(await lines[1], "part1 part2"); |
+ |
+ 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(await lines[2], "part5 part6"); |
+ }); |
+ |
+ test('reload twice', () async { |
+ await startProgram(2); |
+ expect(await lines[1], "part1 part2"); |
+ |
+ writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part5")); |
+ writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part6")); |
+ await rebuild(compiler, outputUri); |
+ await hotReload(); |
+ expect(await lines[2], "part5 part6"); |
+ |
+ 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(await lines[3], "part7 part8"); |
+ }); |
+} |
+ |
+var dartVm = Uri.base.resolve(Platform.resolvedExecutable); |
+var sdkRoot = dartVm.resolve("patched_sdk/"); |
+var platformFile = sdkRoot.resolve('platform.dill'); |
+ |
+Future<IncrementalKernelGenerator> createIncrementalCompiler( |
+ String entry, FileSystem fs) { |
+ var entryUri = Uri.base.resolve(entry); |
+ var options = new CompilerOptions() |
+ ..sdkRoot = sdkRoot |
+ ..sdkSummary = sdkRoot.resolve('outline.dill') |
+ ..packagesFileUri = Uri.parse('file:///.packages') |
+ ..strongMode = false |
+ ..dartLibraries = loadDartLibraries() |
+ ..fileSystem = fs |
+ ..byteStore = new MemoryByteStore(); |
+ return IncrementalKernelGenerator.newInstance(options, entryUri); |
+} |
+ |
+Map<String, Uri> loadDartLibraries() { |
+ var libraries = sdkRoot.resolve('lib/libraries.json'); |
+ var map = |
+ JSON.decode(new File.fromUri(libraries).readAsStringSync())['libraries']; |
+ var dartLibraries = <String, Uri>{}; |
+ map.forEach((k, v) => dartLibraries[k] = libraries.resolve(v)); |
+ return dartLibraries; |
+} |
+ |
+Future<bool> rebuild(IncrementalKernelGenerator compiler, Uri outputUri) async { |
+ compiler.invalidate(Uri.parse("file:///a.dart")); |
+ compiler.invalidate(Uri.parse("file:///b.dart")); |
+ var program = (await compiler.computeDelta()).newProgram; |
+ if (program != null && !program.libraries.isEmpty) { |
+ await writeProgram(program, outputUri); |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+Future<Null> writeProgram(Program program, Uri outputUri) async { |
+ var sink = new File.fromUri(outputUri).openWrite(); |
+ // TODO(sigmund): the incremental generator should always filter these |
+ // libraries instead. |
+ new LimitedBinaryPrinter( |
+ sink, (library) => library.importUri.scheme != 'dart') |
+ .writeProgramFile(program); |
+ await sink.close(); |
+} |
+ |
+void writeFile(MemoryFileSystem fs, String fileName, String contents) { |
+ fs.entityForUri(Uri.parse('file:///$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'; |
+ |
+void main(List<String> args) { |
+ var last = f(); |
+ print(last); |
+ |
+ // 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); |
+ last = result; |
+ if (--reloadCount == 0) timer.cancel(); |
+ } |
+ }); |
+} |
+ |
+f() => "$line part2"; |
+'''; |
+ |
+const sourceB = r''' |
+get line => "part1"; |
+'''; |
+ |
+RegExp observatoryPortRegExp = |
+ new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)/"); |