OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Integration test that runs the incremental compiler, runs the compiled |
| 6 /// program, incrementally rebuild portions of the app, and triggers a hot |
| 7 /// reload on the running program. |
| 8 library front_end.incremental.hot_reload_e2e_test; |
| 9 |
| 10 import 'dart:async'; |
| 11 import 'dart:convert'; |
| 12 import 'dart:io'; |
| 13 |
| 14 import 'package:front_end/compiler_options.dart'; |
| 15 import 'package:front_end/file_system.dart'; |
| 16 import 'package:front_end/incremental_kernel_generator.dart'; |
| 17 import 'package:front_end/memory_file_system.dart'; |
| 18 import 'package:front_end/src/incremental/byte_store.dart'; |
| 19 import 'package:front_end/src/testing/hybrid_file_system.dart'; |
| 20 import 'package:front_end/src/vm/reload.dart'; |
| 21 import 'package:kernel/ast.dart'; |
| 22 import 'package:kernel/binary/limited_ast_to_binary.dart'; |
| 23 import 'package:test/test.dart'; |
| 24 |
| 25 main() { |
| 26 IncrementalKernelGenerator compiler; |
| 27 MemoryFileSystem fs; |
| 28 Directory outDir; |
| 29 Uri outputUri; |
| 30 List<Future<String>> lines; |
| 31 Future programIsDone; |
| 32 |
| 33 setUp(() async { |
| 34 outDir = Directory.systemTemp.createTempSync('hotreload_test'); |
| 35 outputUri = outDir.uri.resolve('test.dill'); |
| 36 fs = new MemoryFileSystem(Uri.parse('file:///')); |
| 37 writeFile(fs, 'a.dart', sourceA); |
| 38 writeFile(fs, 'b.dart', sourceB); |
| 39 writeFile(fs, '.packages', ''); |
| 40 compiler = await createIncrementalCompiler( |
| 41 'file:///a.dart', new HybridFileSystem(fs)); |
| 42 await rebuild(compiler, outputUri); // this is a full compile. |
| 43 }); |
| 44 |
| 45 tearDown(() async { |
| 46 outDir.deleteSync(recursive: true); |
| 47 lines = null; |
| 48 }); |
| 49 |
| 50 /// Start the VM with the first version of the program compiled by the |
| 51 /// incremental compiler. |
| 52 startProgram(int reloadCount) async { |
| 53 var vmArgs = [ |
| 54 '--enable-vm-service=0', // Note: use 0 to avoid port collisions. |
| 55 '--platform=${platformFile.toFilePath()}', |
| 56 outputUri.toFilePath() |
| 57 ]; |
| 58 vmArgs.add('$reloadCount'); |
| 59 var vm = await Process.start(Platform.executable, vmArgs); |
| 60 var splitter = new LineSplitter(); |
| 61 |
| 62 /// The program prints at most 2 + reloadCount lines: |
| 63 /// - a line displaying the observatory port |
| 64 /// - a line before waiting for a reload |
| 65 /// - a line after each hot-reload |
| 66 int i = 0; |
| 67 int expectedLines = 2 + reloadCount; |
| 68 var completers = |
| 69 new List.generate(expectedLines, (_) => new Completer<String>()); |
| 70 lines = completers.map((c) => c.future).toList(); |
| 71 vm.stdout.transform(UTF8.decoder).transform(splitter).listen((line) { |
| 72 expect(i, lessThan(expectedLines)); |
| 73 completers[i++].complete(line); |
| 74 }, onDone: () { |
| 75 expect(i, expectedLines); |
| 76 }); |
| 77 |
| 78 vm.stderr.transform(UTF8.decoder).transform(splitter).toList().then((err) { |
| 79 expect(err, isEmpty, reason: err.join('\n')); |
| 80 }); |
| 81 |
| 82 programIsDone = vm.exitCode; |
| 83 } |
| 84 |
| 85 /// Request a hot reload on the running program. |
| 86 Future hotReload() async { |
| 87 var portLine = await lines[0]; |
| 88 expect(observatoryPortRegExp.hasMatch(portLine), isTrue); |
| 89 var match = observatoryPortRegExp.firstMatch(portLine); |
| 90 var port = int.parse(match.group(1)); |
| 91 var reloader = new VmReloader(port); |
| 92 var reloadResult = await reloader.reload(outputUri); |
| 93 expect(reloadResult['success'], isTrue); |
| 94 await reloader.disconnect(); |
| 95 } |
| 96 |
| 97 test('initial program is valid', () async { |
| 98 await startProgram(0); |
| 99 await programIsDone; |
| 100 expect(await lines.skip(1).first, "part1 part2"); |
| 101 }); |
| 102 |
| 103 test('reload after leaf library modification', () async { |
| 104 await startProgram(1); |
| 105 expect(await lines[1], "part1 part2"); |
| 106 |
| 107 writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part3")); |
| 108 await rebuild(compiler, outputUri); |
| 109 await hotReload(); |
| 110 await programIsDone; |
| 111 expect(await lines[2], "part3 part2"); |
| 112 }); |
| 113 |
| 114 test('reload after non-leaf library modification', () async { |
| 115 await startProgram(1); |
| 116 expect(await lines[1], "part1 part2"); |
| 117 |
| 118 writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part4")); |
| 119 await rebuild(compiler, outputUri); |
| 120 await hotReload(); |
| 121 await programIsDone; |
| 122 expect(await lines[2], "part1 part4"); |
| 123 }, skip: true /* VM crashes on reload */); |
| 124 |
| 125 test('reload after whole program modification', () async { |
| 126 await startProgram(1); |
| 127 expect(await lines[1], "part1 part2"); |
| 128 |
| 129 writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part5")); |
| 130 writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part6")); |
| 131 await rebuild(compiler, outputUri); |
| 132 await hotReload(); |
| 133 await programIsDone; |
| 134 expect(await lines[2], "part5 part6"); |
| 135 }); |
| 136 |
| 137 test('reload twice', () async { |
| 138 await startProgram(2); |
| 139 expect(await lines[1], "part1 part2"); |
| 140 |
| 141 writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part5")); |
| 142 writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part6")); |
| 143 await rebuild(compiler, outputUri); |
| 144 await hotReload(); |
| 145 expect(await lines[2], "part5 part6"); |
| 146 |
| 147 writeFile(fs, 'b.dart', sourceB.replaceAll("part1", "part7")); |
| 148 writeFile(fs, 'a.dart', sourceA.replaceAll("part2", "part8")); |
| 149 await rebuild(compiler, outputUri); |
| 150 await hotReload(); |
| 151 await programIsDone; |
| 152 expect(await lines[3], "part7 part8"); |
| 153 }); |
| 154 } |
| 155 |
| 156 var dartVm = Uri.base.resolve(Platform.resolvedExecutable); |
| 157 var sdkRoot = dartVm.resolve("patched_sdk/"); |
| 158 var platformFile = sdkRoot.resolve('platform.dill'); |
| 159 |
| 160 Future<IncrementalKernelGenerator> createIncrementalCompiler( |
| 161 String entry, FileSystem fs) { |
| 162 var entryUri = Uri.base.resolve(entry); |
| 163 var options = new CompilerOptions() |
| 164 ..sdkRoot = sdkRoot |
| 165 ..sdkSummary = sdkRoot.resolve('outline.dill') |
| 166 ..packagesFileUri = Uri.parse('file:///.packages') |
| 167 ..strongMode = false |
| 168 ..dartLibraries = loadDartLibraries() |
| 169 ..fileSystem = fs |
| 170 ..byteStore = new MemoryByteStore(); |
| 171 return IncrementalKernelGenerator.newInstance(options, entryUri); |
| 172 } |
| 173 |
| 174 Map<String, Uri> loadDartLibraries() { |
| 175 var libraries = sdkRoot.resolve('lib/libraries.json'); |
| 176 var map = |
| 177 JSON.decode(new File.fromUri(libraries).readAsStringSync())['libraries']; |
| 178 var dartLibraries = <String, Uri>{}; |
| 179 map.forEach((k, v) => dartLibraries[k] = libraries.resolve(v)); |
| 180 return dartLibraries; |
| 181 } |
| 182 |
| 183 Future<bool> rebuild(IncrementalKernelGenerator compiler, Uri outputUri) async { |
| 184 compiler.invalidate(Uri.parse("file:///a.dart")); |
| 185 compiler.invalidate(Uri.parse("file:///b.dart")); |
| 186 var program = (await compiler.computeDelta()).newProgram; |
| 187 if (program != null && !program.libraries.isEmpty) { |
| 188 await writeProgram(program, outputUri); |
| 189 return true; |
| 190 } |
| 191 return false; |
| 192 } |
| 193 |
| 194 Future<Null> writeProgram(Program program, Uri outputUri) async { |
| 195 var sink = new File.fromUri(outputUri).openWrite(); |
| 196 // TODO(sigmund): the incremental generator should always filter these |
| 197 // libraries instead. |
| 198 new LimitedBinaryPrinter( |
| 199 sink, (library) => library.importUri.scheme != 'dart') |
| 200 .writeProgramFile(program); |
| 201 await sink.close(); |
| 202 } |
| 203 |
| 204 void writeFile(MemoryFileSystem fs, String fileName, String contents) { |
| 205 fs.entityForUri(Uri.parse('file:///$fileName')).writeAsStringSync(contents); |
| 206 } |
| 207 |
| 208 /// This program calls a function periodically and tracks when the function |
| 209 /// returns a different value than before (which only happens after a |
| 210 /// hot-reload). The program exits after certain number of reloads, specified as |
| 211 /// an argument to main. |
| 212 const sourceA = r''' |
| 213 import 'dart:async'; |
| 214 import 'b.dart'; |
| 215 |
| 216 void main(List<String> args) { |
| 217 var last = f(); |
| 218 print(last); |
| 219 |
| 220 // The argument indicates how many "visible" hot-reloads to run |
| 221 int reloadCount = 0; |
| 222 if (args.length > 0) { |
| 223 reloadCount = int.parse(args[0]); |
| 224 } |
| 225 if (reloadCount == 0) return; |
| 226 |
| 227 new Timer.periodic(new Duration(milliseconds: 100), (timer) { |
| 228 var result = f(); |
| 229 if (last != result) { |
| 230 print(result); |
| 231 last = result; |
| 232 if (--reloadCount == 0) timer.cancel(); |
| 233 } |
| 234 }); |
| 235 } |
| 236 |
| 237 f() => "$line part2"; |
| 238 '''; |
| 239 |
| 240 const sourceB = r''' |
| 241 get line => "part1"; |
| 242 '''; |
| 243 |
| 244 RegExp observatoryPortRegExp = |
| 245 new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)/"); |
OLD | NEW |