a6cc772b67
Change-Id: I206a7a8132b5ea79dc019acf5c2e0cd800307bcf Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/212161 Reviewed-by: Jens Johansen <jensj@google.com> Commit-Queue: Johnni Winther <johnniwinther@google.com>
390 lines
11 KiB
Dart
390 lines
11 KiB
Dart
// 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.
|
|
|
|
// @dart = 2.9
|
|
|
|
/// 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 {
|
|
IncrementalKernelGenerator compiler;
|
|
MemoryFileSystem fs;
|
|
Directory outDir;
|
|
Uri outputUri;
|
|
List<Future<String>> lines;
|
|
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 = null;
|
|
}
|
|
|
|
Future<int> computeVmPort() async {
|
|
var portLine = await lines[0];
|
|
Expect.isTrue(observatoryPortRegExp.hasMatch(portLine));
|
|
var match = observatoryPortRegExp.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 component = await compiler.computeDelta();
|
|
if (component != null && !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.scheme != '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 observatoryPortRegExp =
|
|
new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)/");
|