| 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]*\)/"); | 
|  |