Files
sdk/pkg/vm/test/incremental_compiler_test.dart
T
Jens Johansen 73466729f3 [vm] Pass offset and script uri for expression compilation
This CL passes the offset and uri of the file (here called a script uri
as opposed to a library uri, the two will be different if we're in a
part) when doing expression compilation.

This CL only passes the data, but doesn't actually use it.
Future CL(s) will use this data to calculate the static type of
available variables which is needed for an upcomming feature.

TEST=Existing tests.
CoreLibraryReviewExempt: Not changing SDK APIs.
Change-Id: I67ead461ab4bb9341424e693946f3e4afe35ce92
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/329322
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
2023-10-12 10:22:38 +00:00

2004 lines
66 KiB
Dart

// Copyright (c) 2018, 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.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:front_end/src/api_unstable/vm.dart'
show
CompilerOptions,
DiagnosticMessage,
ExperimentalFlag,
IncrementalCompilerResult,
NnbdMode,
computePlatformBinariesLocation;
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:kernel/binary/ast_to_binary.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/target/targets.dart';
import 'package:kernel/text/ast_to_text.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:web_socket_channel/io.dart';
import 'package:vm/incremental_compiler.dart';
import 'package:vm/target/vm.dart';
import 'common_test_utils.dart';
main() {
final platformKernel =
computePlatformBinariesLocation().resolve('vm_platform_strong.dill');
final sdkRoot = computePlatformBinariesLocation();
CompilerOptions getFreshOptions() {
return new CompilerOptions()
..sdkRoot = sdkRoot
..nnbdMode = NnbdMode.Strong
..target = new VmTarget(new TargetFlags())
..additionalDills = <Uri>[platformKernel]
..onDiagnostic = (DiagnosticMessage message) {
fail("Compilation error: ${message.plainTextFormatted.join('\n')}");
}
..environmentDefines = const {};
}
final options = getFreshOptions();
group('basic', () {
late Directory mytest;
late File main;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('incremental');
main = new File('${mytest.path}/main.dart')..createSync();
main.writeAsStringSync("main() {}\n");
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
});
test('compile', () async {
IncrementalCompiler compiler =
new IncrementalCompiler(options, [main.uri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
final StringBuffer buffer = new StringBuffer();
new Printer(buffer, showMetadata: true)
.writeLibraryFile(component.mainMethod!.enclosingLibrary);
expect(
buffer.toString(),
equals('library;\n'
'import self as self;\n'
'\n'
'static method main() → dynamic {}\n'));
});
test('compile exclude sources', () async {
CompilerOptions optionsExcludeSources = getFreshOptions()
..embedSourceText = false;
IncrementalCompiler compiler =
new IncrementalCompiler(optionsExcludeSources, [main.uri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
for (Source source in component.uriToSource.values) {
expect(source.source.length, equals(0));
}
final StringBuffer buffer = new StringBuffer();
new Printer(buffer, showMetadata: true)
.writeLibraryFile(component.mainMethod!.enclosingLibrary);
expect(
buffer.toString(),
equals('library;\n'
'import self as self;\n'
'\n'
'static method main() → dynamic {}\n'));
});
test('compile expressions errors are not re-reported', () async {
var errorsReported = 0;
CompilerOptions optionsAcceptErrors = getFreshOptions()
..onDiagnostic = (DiagnosticMessage message) {
errorsReported++;
message.plainTextFormatted.forEach(print);
};
IncrementalCompiler compiler =
new IncrementalCompiler(optionsAcceptErrors, [main.uri]);
await compiler.compile();
compiler.accept();
{
Procedure? procedure = await compiler.compileExpression(
'main',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
main.uri.toString(),
null,
null,
-1,
null,
true);
expect(procedure, isNotNull);
expect(errorsReported, equals(0));
}
{
Procedure? procedure = await compiler.compileExpression(
'main1',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
main.uri.toString(),
null,
null,
-1,
null,
true);
expect(procedure, isNotNull);
expect(errorsReported, equals(1));
errorsReported = 0;
}
await compiler.compile();
expect(errorsReported, equals(0));
});
});
/// Collects coverage for "main.dart", "lib.dart", "lib1.dart" and "lib2.dart"
/// checks that all tokens can be translated to line and column,
/// return the hit positions for "lib1.dart".
/// If [getAllSources] is false it will ask specifically for report
/// (and thus hits) for "lib1.dart" only.
Future<Set<int>> collectAndCheckCoverageData(int port, bool getAllSources,
{bool resume = true,
bool onGetAllVerifyCount = true,
Set<int>? coverageForLines}) async {
RemoteVm remoteVm = new RemoteVm(port);
// Wait for the script to have finished.
while (true) {
Map isolate = await remoteVm.getIsolate();
Map pauseEvent = isolate["pauseEvent"];
if (pauseEvent["kind"] == "PauseExit") break;
}
// Collect coverage for the two user scripts.
List<Map> sourceReports = <Map>[];
if (getAllSources) {
Map sourceReport = await remoteVm.getSourceReport();
sourceReports.add(sourceReport);
} else {
Map scriptsMap = await remoteVm.getScripts();
List scripts = scriptsMap["scripts"];
Set<String> scriptIds = new Set<String>();
for (int i = 0; i < scripts.length; i++) {
Map script = scripts[i];
String scriptUri = script["uri"];
if (scriptUri.contains("lib1.dart")) {
scriptIds.add(script["id"]);
}
}
for (String scriptId in scriptIds) {
Map sourceReport = await remoteVm.getSourceReport(scriptId);
sourceReports.add(sourceReport);
}
}
List<String> errorMessages = <String>[];
Set<int> hits = new Set<int>();
// Ensure that we can get a line and column number for all reported
// positions in the scripts we care about.
for (Map sourceReport in sourceReports) {
List<Map> scripts = sourceReport["scripts"].cast<Map>();
Map<String, int> scriptIdToIndex = new Map<String, int>();
Set<int> lib1scriptIndices = new Set<int>();
int i = 0;
for (Map script in scripts) {
if (script["uri"].toString().endsWith("main.dart") ||
script["uri"].toString().endsWith("lib.dart") ||
script["uri"].toString().endsWith("lib1.dart") ||
script["uri"].toString().endsWith("lib2.dart")) {
scriptIdToIndex[script["id"]] = i;
if (script["uri"].toString().endsWith("lib1.dart")) {
lib1scriptIndices.add(i);
}
}
i++;
}
if (getAllSources && onGetAllVerifyCount) {
expect(scriptIdToIndex.length >= 2, isTrue);
}
// Ensure the scripts all have a non-null 'tokenPosTable' entry.
Map<int, Map> scriptIndexToScript = new Map<int, Map>();
for (String scriptId in scriptIdToIndex.keys) {
Map script = await remoteVm.getObject(scriptId);
int scriptIdx = scriptIdToIndex[scriptId]!;
scriptIndexToScript[scriptIdx] = script;
List? tokenPosTable = script["tokenPosTable"];
if (tokenPosTable == null) {
errorMessages.add("Script with uri ${script['uri']} "
"and id ${script['id']} "
"has null tokenPosTable.");
} else if (tokenPosTable.isEmpty) {
errorMessages.add("Script with uri ${script['uri']} "
"and id ${script['id']} "
"has empty tokenPosTable.");
}
}
List<Map> ranges = sourceReport["ranges"].cast<Map>();
Set<int> scriptIndexesSet = new Set<int>.from(scriptIndexToScript.keys);
for (Map range in ranges) {
if (scriptIndexesSet.contains(range["scriptIndex"])) {
Set<int> positions = new Set<int>();
positions.add(range["startPos"]);
positions.add(range["endPos"]);
Map coverage = range["coverage"];
for (int pos in coverage["hits"]) {
positions.add(pos);
if (lib1scriptIndices.contains(range["scriptIndex"])) {
hits.add(pos);
}
}
for (int pos in coverage["misses"]) {
positions.add(pos);
}
if (range["possibleBreakpoints"] != null) {
for (int pos in range["possibleBreakpoints"]) {
positions.add(pos);
}
}
Map script = scriptIndexToScript[range["scriptIndex"]]!;
Set<int> knownPositions = new Set<int>();
Map<int, int> tokenPosToLine = {};
if (script["tokenPosTable"] != null) {
for (List tokenPosTableLine in script["tokenPosTable"]) {
for (int i = 1; i < tokenPosTableLine.length; i += 2) {
tokenPosToLine[tokenPosTableLine[i]] = tokenPosTableLine[0];
knownPositions.add(tokenPosTableLine[i]);
}
}
}
for (int pos in positions) {
if (!knownPositions.contains(pos)) {
errorMessages.add("Script with uri ${script['uri']} "
"and id ${script['id']} "
"references position $pos which cannot be translated to "
"line and column.");
}
}
if (coverageForLines != null) {
for (int pos in coverage["hits"]) {
if (lib1scriptIndices.contains(range["scriptIndex"])) {
coverageForLines.add(tokenPosToLine[pos]!);
}
}
}
}
}
}
expect(errorMessages, isEmpty);
if (resume) {
remoteVm.resume();
}
return hits;
}
group('multiple kernels', () {
late Directory mytest;
late File main;
late File lib;
late Process vm;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('incremental');
main = new File('${mytest.path}/main.dart')..createSync();
main.writeAsStringSync("""
import 'lib.dart';
main() => print(foo());
class C1 extends Object with C3, C2 {
c1method() {
print("c1");
}
}
mixin C3 {
c3method() {
print("c3");
}
}
""");
lib = new File('${mytest.path}/lib.dart')..createSync();
lib.writeAsStringSync("""
import 'main.dart';
foo() => 'foo';
main() => print('bar');
mixin C2 on C3 {
c2method() {
print("c2");
}
}
""");
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
});
compileAndSerialize(
File mainDill, File libDill, IncrementalCompiler compiler) async {
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
new BinaryPrinter(new DevNullSink<List<int>>())
.writeComponentFile(component);
IOSink sink = mainDill.openWrite();
BinaryPrinter printer = new BinaryPrinter(sink,
libraryFilter: (lib) => lib.fileUri.path.endsWith("main.dart"));
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
sink = libDill.openWrite();
printer = new BinaryPrinter(sink,
libraryFilter: (lib) => lib.fileUri.path.endsWith("lib.dart"));
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
}
test('main first, lib second', () async {
Directory dir = mytest.createTempSync();
File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill")));
File libDill = File(p.join(dir.path, p.basename(lib.path + ".dill")));
IncrementalCompiler compiler =
new IncrementalCompiler(options, [main.uri]);
await compileAndSerialize(mainDill, libDill, compiler);
var list = new File(p.join(dir.path, 'myMain.dilllist'))..createSync();
list.writeAsStringSync("#@dill\n${mainDill.path}\n${libDill.path}\n");
vm =
await Process.start(Platform.resolvedExecutable, <String>[list.path]);
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stdout: $s");
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete(s);
}
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
expect(await portLineCompleter.future, equals('foo'));
print("Compiler terminated with ${await vm.exitCode} exit code");
});
test('main second, lib first', () async {
Directory dir = mytest.createTempSync();
File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill")));
File libDill = File(p.join(dir.path, p.basename(lib.path + ".dill")));
IncrementalCompiler compiler =
new IncrementalCompiler(options, [lib.uri]);
await compileAndSerialize(mainDill, libDill, compiler);
var list = new File(p.join(dir.path, 'myMain.dilllist'))..createSync();
list.writeAsStringSync("#@dill\n${libDill.path}\n${mainDill.path}\n");
vm =
await Process.start(Platform.resolvedExecutable, <String>[list.path]);
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stdout: $s");
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete(s);
}
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
expect(await portLineCompleter.future, equals('bar'));
print("Compiler terminated with ${await vm.exitCode} exit code");
});
test('empty list', () async {
var list = new File(p.join(mytest.path, 'myMain.dilllist'))..createSync();
list.writeAsStringSync("#@dill\n");
vm =
await Process.start(Platform.resolvedExecutable, <String>[list.path]);
Completer<int> exitCodeCompleter = new Completer<int>();
vm.exitCode.then((exitCode) {
print("Compiler terminated with $exitCode exit code");
exitCodeCompleter.complete(exitCode);
});
expect(await exitCodeCompleter.future, equals(254));
});
test('fallback to source compilation if fail to load', () async {
var list = new File('${mytest.path}/myMain.dilllist')..createSync();
list.writeAsStringSync("main() => print('baz');\n");
vm =
await Process.start(Platform.resolvedExecutable, <String>[list.path]);
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stdout: $s");
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete(s);
}
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
expect(await portLineCompleter.future, equals('baz'));
print("Compiler terminated with ${await vm.exitCode} exit code");
});
test('relative paths', () async {
Directory dir = mytest.createTempSync();
File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill")));
File libDill = File(p.join(dir.path, p.basename(lib.path + ".dill")));
IncrementalCompiler compiler =
new IncrementalCompiler(options, [main.uri]);
await compileAndSerialize(mainDill, libDill, compiler);
var list = new File(p.join(dir.path, 'myMain.dilllist'))..createSync();
list.writeAsStringSync("#@dill\nmain.dart.dill\nlib.dart.dill\n");
Directory runFrom = new Directory(dir.path + "/runFrom")..createSync();
vm = await Process.start(Platform.resolvedExecutable, <String>[list.path],
workingDirectory: runFrom.path);
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stdout: $s");
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete(s);
}
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
expect(await portLineCompleter.future, equals('foo'));
print("Compiler terminated with ${await vm.exitCode} exit code");
});
test('collect coverage', () async {
Directory dir = mytest.createTempSync();
File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill")));
File libDill = File(p.join(dir.path, p.basename(lib.path + ".dill")));
IncrementalCompiler compiler =
new IncrementalCompiler(options, [main.uri]);
await compileAndSerialize(mainDill, libDill, compiler);
var list = new File(p.join(dir.path, 'myMain.dilllist'))..createSync();
list.writeAsStringSync("#@dill\n${mainDill.path}\n${libDill.path}\n");
vm = await Process.start(Platform.resolvedExecutable, <String>[
"--pause-isolates-on-exit",
"--enable-vm-service:0",
"--disable-service-auth-codes",
"--disable-dart-dev",
list.path
]);
const kDartVMServiceListening = 'The Dart VM service is listening on ';
final RegExp dartVMServicePortRegExp = new RegExp(
"The Dart VM service is listening on http://127.0.0.1:\([0-9]*\)");
int port;
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout
.transform(utf8.decoder)
.transform(splitter)
.listen((String s) async {
if (s.startsWith(kDartVMServiceListening)) {
expect(dartVMServicePortRegExp.hasMatch(s), isTrue);
final match = dartVMServicePortRegExp.firstMatch(s)!;
port = int.parse(match.group(1)!);
await collectAndCheckCoverageData(port, true);
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete("done");
}
}
print("vm stdout: $s");
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
await portLineCompleter.future;
print("Compiler terminated with ${await vm.exitCode} exit code");
});
});
group('multiple kernels constant coverage', () {
late Directory mytest;
late File main;
late File lib1;
late int lineForUnnamedConstructor;
late int lineForNamedConstructor;
late Process vm;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('incremental');
main = new File('${mytest.path}/main.dart')..createSync();
main.writeAsStringSync("""
// This file - combined with the lib - should have coverage for both
// constructors of Foo.
import 'lib1.dart' as lib1;
void testFunction() {
const foo = lib1.Foo.named();
const foo2 = lib1.Foo.named();
if (!identical(foo, foo2)) throw "what?";
}
main() {
lib1.testFunction();
testFunction();
print("main");
}
""");
lib1 = new File('${mytest.path}/lib1.dart')..createSync();
lib1.writeAsStringSync("""
// Compiling this file should mark the default constructor - but not the
// named constructor - as having coverage.
class Foo {
final int x;
const Foo([int? x]) : this.x = x ?? 42;
const Foo.named([int? x]) : this.x = x ?? 42;
}
void testFunction() {
const foo = Foo();
const foo2 = Foo();
if (!identical(foo, foo2)) throw "what?";
}
main() {
testFunction();
print("lib1");
}
""");
lineForUnnamedConstructor = 5;
lineForNamedConstructor = 6;
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
});
Future<Set<int>> runAndGetLineCoverage(
File list, String expectStdoutContains) async {
vm = await Process.start(Platform.resolvedExecutable, <String>[
"--pause-isolates-on-exit",
"--enable-vm-service:0",
"--disable-service-auth-codes",
"--disable-dart-dev",
list.path
]);
const kDartVMServiceListening = 'The Dart VM service is listening on ';
final RegExp dartVMServicePortRegExp = new RegExp(
"The Dart VM service is listening on http://127.0.0.1:\([0-9]*\)");
int port;
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
Set<int> coverageLines = {};
bool foundExpectedString = false;
vm.stdout
.transform(utf8.decoder)
.transform(splitter)
.listen((String s) async {
if (s == expectStdoutContains) {
foundExpectedString = true;
}
if (s.startsWith(kDartVMServiceListening)) {
expect(dartVMServicePortRegExp.hasMatch(s), isTrue);
final match = dartVMServicePortRegExp.firstMatch(s)!;
port = int.parse(match.group(1)!);
await collectAndCheckCoverageData(port, true,
onGetAllVerifyCount: false, coverageForLines: coverageLines);
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete("done");
}
}
print("vm stdout: $s");
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
await portLineCompleter.future;
print("Compiler terminated with ${await vm.exitCode} exit code");
expect(foundExpectedString, isTrue);
return coverageLines;
}
test('compile separately, check coverage', () async {
Directory dir = mytest.createTempSync();
// First compile lib, run and verify coverage (un-named constructor
// covered, but not the named constructor).
// Note that it's called 'lib1' to match with expectations from coverage
// collector helper in this file.
File libDill = File(p.join(dir.path, p.basename(lib1.path + ".dill")));
IncrementalCompiler compiler =
new IncrementalCompiler(options, [lib1.uri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
expect(component.libraries.length, equals(1));
expect(component.libraries.single.fileUri, equals(lib1.uri));
IOSink sink = libDill.openWrite();
BinaryPrinter printer = new BinaryPrinter(sink);
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
File list = new File(p.join(dir.path, 'dill.list'))..createSync();
list.writeAsStringSync("#@dill\n${libDill.path}\n");
Set<int> lineCoverage = await runAndGetLineCoverage(list, "lib1");
// Expect coverage for unnamed constructor but not for the named one.
expect(
lineCoverage.intersection(
{lineForUnnamedConstructor, lineForNamedConstructor}),
equals({lineForUnnamedConstructor}));
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
// Accept the compile to not include the lib again.
compiler.accept();
// Then compile lib, run and verify coverage (un-named constructor
// covered, and the named constructor covered too).
File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill")));
compilerResult = await compiler.compile(entryPoints: [main.uri]);
component = compilerResult.component;
expect(component.libraries.length, equals(1));
expect(component.libraries.single.fileUri, equals(main.uri));
sink = mainDill.openWrite();
printer = new BinaryPrinter(sink);
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
list.writeAsStringSync("#@dill\n${mainDill.path}\n${libDill.path}\n");
lineCoverage = await runAndGetLineCoverage(list, "main");
// Expect coverage for both unnamed constructor and for the named one.
expect(
lineCoverage.intersection(
{lineForUnnamedConstructor, lineForNamedConstructor}),
equals({lineForUnnamedConstructor, lineForNamedConstructor}));
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
// Accept the compile to not include the lib again.
compiler.accept();
// Finally, change lib to shift the constructors so the old line numbers
// doesn't match. Compile lib by itself, compile lib, run with the old
// main and verify coverage is still correct (both un-named constructor
// and named constructor (at new line numbers) are covered, and the old
// line numbers are not coverage.
lib1.writeAsStringSync("""
//
// Shift lines down by five
// lines so the original
// lines can't be covered
//
class Foo {
final int x;
const Foo([int? x]) : this.x = x ?? 42;
const Foo.named([int? x]) : this.x = x ?? 42;
}
void testFunction() {
const foo = Foo();
const foo2 = Foo();
if (!identical(foo, foo2)) throw "what?";
}
main() {
testFunction();
print("lib1");
}
""");
int newLineForUnnamedConstructor = 8;
int newLineForNamedConstructor = 9;
compiler.invalidate(lib1.uri);
compilerResult = await compiler.compile(entryPoints: [lib1.uri]);
component = compilerResult.component;
expect(component.libraries.length, equals(1));
expect(component.libraries.single.fileUri, equals(lib1.uri));
sink = libDill.openWrite();
printer = new BinaryPrinter(sink);
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
list.writeAsStringSync("#@dill\n${mainDill.path}\n${libDill.path}\n");
lineCoverage = await runAndGetLineCoverage(list, "main");
// Expect coverage for both unnamed constructor and for the named one on
// the new positions, but no coverage on the old positions.
expect(
lineCoverage.intersection({
lineForUnnamedConstructor,
lineForNamedConstructor,
newLineForUnnamedConstructor,
newLineForNamedConstructor
}),
equals({newLineForUnnamedConstructor, newLineForNamedConstructor}));
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
// Accept the compile to not include the lib again.
compiler.accept();
});
});
group('multiple kernels 2', () {
late Directory mytest;
late File main;
late File lib1;
late File lib2;
late Process vm;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('incremental');
main = new File('${mytest.path}/main.dart')..createSync();
main.writeAsStringSync("""
import 'lib1.dart';
import 'lib2.dart';
void main() {
TestA().foo();
bar();
}
class TestA with A {}
""");
lib1 = new File('${mytest.path}/lib1.dart')..createSync();
lib1.writeAsStringSync("""
mixin A {
void foo() {
print('foo');
}
void bar() {
print('bar');
}
}
""");
lib2 = new File('${mytest.path}/lib2.dart')..createSync();
lib2.writeAsStringSync("""
import 'lib1.dart';
void bar() {
TestB().bar();
}
class TestB with A {}
""");
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
});
compileAndSerialize(File mainDill, File lib1Dill, File lib2Dill,
IncrementalCompiler compiler) async {
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
new BinaryPrinter(new DevNullSink<List<int>>())
.writeComponentFile(component);
IOSink sink = mainDill.openWrite();
BinaryPrinter printer = new BinaryPrinter(sink,
libraryFilter: (lib) => lib.fileUri.path.endsWith("main.dart"));
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
sink = lib1Dill.openWrite();
printer = new BinaryPrinter(sink,
libraryFilter: (lib) => lib.fileUri.path.endsWith("lib1.dart"));
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
sink = lib2Dill.openWrite();
printer = new BinaryPrinter(sink,
libraryFilter: (lib) => lib.fileUri.path.endsWith("lib2.dart"));
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
}
test('collect coverage hits', () async {
Directory dir = mytest.createTempSync();
File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill")));
File lib1Dill = File(p.join(dir.path, p.basename(lib1.path + ".dill")));
File lib2Dill = File(p.join(dir.path, p.basename(lib2.path + ".dill")));
IncrementalCompiler compiler =
new IncrementalCompiler(options, [main.uri]);
await compileAndSerialize(mainDill, lib1Dill, lib2Dill, compiler);
var list = new File(p.join(dir.path, 'myMain.dilllist'))..createSync();
list.writeAsStringSync(
"#@dill\n${mainDill.path}\n${lib1Dill.path}\n${lib2Dill.path}\n");
vm = await Process.start(Platform.resolvedExecutable, <String>[
"--pause-isolates-on-exit",
"--enable-vm-service:0",
"--disable-service-auth-codes",
"--disable-dart-dev",
list.path
]);
const kDartVMServiceListening = 'The Dart VM service is listening on ';
final RegExp dartVMServicePortRegExp = new RegExp(
"The Dart VM service is listening on http://127.0.0.1:\([0-9]*\)");
int port;
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout
.transform(utf8.decoder)
.transform(splitter)
.listen((String s) async {
if (s.startsWith(kDartVMServiceListening)) {
expect(dartVMServicePortRegExp.hasMatch(s), isTrue);
final match = dartVMServicePortRegExp.firstMatch(s)!;
port = int.parse(match.group(1)!);
Set<int> hits1 =
await collectAndCheckCoverageData(port, true, resume: false);
Set<int> hits2 =
await collectAndCheckCoverageData(port, false, resume: true);
expect(hits1.toList()..sort(), equals(hits2.toList()..sort()));
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete("done");
}
}
print("vm stdout: $s");
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
await portLineCompleter.future;
print("Compiler terminated with ${await vm.exitCode} exit code");
});
});
group('reload', () {
late Directory mytest;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('incremental');
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
});
test('picks up after rejected delta', () async {
var file = new File('${mytest.path}/foo.dart')..createSync();
file.writeAsStringSync("import 'bar.dart';\n"
"import 'baz.dart';\n"
"main() {\n"
" new A();\n"
" openReceivePortSoWeWontDie();"
"}\n");
var fileBar = new File('${mytest.path}/bar.dart')..createSync();
fileBar.writeAsStringSync("class A<T> { int _a = 0; }\n");
var fileBaz = new File('${mytest.path}/baz.dart')..createSync();
fileBaz.writeAsStringSync("import 'dart:isolate';\n"
"openReceivePortSoWeWontDie() { new RawReceivePort(); }\n");
IncrementalCompiler compiler =
new IncrementalCompiler(options, [file.uri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
File outputFile = new File('${mytest.path}/foo.dart.dill');
await _writeProgramToFile(component, outputFile);
final List<String> vmArgs = [
'--trace_reload',
'--trace_reload_verbose',
'--enable-vm-service=0', // Note: use 0 to avoid port collisions.
'--pause_isolates_on_start',
'--disable-service-auth-codes',
'--disable-dart-dev',
outputFile.path
];
final vm = await Process.start(Platform.resolvedExecutable, vmArgs);
final splitter = new LineSplitter();
vm.exitCode.then((exitCode) {
print("Compiler terminated with $exitCode exit code");
});
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stdout: $s");
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete(s);
}
});
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
});
String portLine = await portLineCompleter.future;
final RegExp dartVMServicePortRegExp = new RegExp(
"The Dart VM service is listening on http://127.0.0.1:\([0-9]*\)");
expect(dartVMServicePortRegExp.hasMatch(portLine), isTrue);
final match = dartVMServicePortRegExp.firstMatch(portLine)!;
final port = int.parse(match.group(1)!);
var remoteVm = new RemoteVm(port);
await remoteVm.resume();
compiler.accept();
// Confirm that without changes VM reloads nothing.
compilerResult = await compiler.compile();
component = compilerResult.component;
await _writeProgramToFile(component, outputFile);
var reloadResult = await remoteVm.reload(new Uri.file(outputFile.path));
expect(reloadResult['success'], isTrue);
expect(reloadResult['details']['loadedLibraryCount'], equals(0));
// Introduce a change that force VM to reject the change.
fileBar.writeAsStringSync("class A<T,U> { int _a = 0; }\n");
compiler.invalidate(fileBar.uri);
compilerResult = await compiler.compile();
component = compilerResult.component;
await _writeProgramToFile(component, outputFile);
reloadResult = await remoteVm.reload(new Uri.file(outputFile.path));
expect(reloadResult['success'], isFalse);
// Fix a change so VM is happy to accept the change.
fileBar.writeAsStringSync("class A<T> { int _a = 0; hi() => _a; }\n");
compiler.invalidate(fileBar.uri);
compilerResult = await compiler.compile();
component = compilerResult.component;
await _writeProgramToFile(component, outputFile);
reloadResult = await remoteVm.reload(new Uri.file(outputFile.path));
expect(reloadResult['success'], isTrue);
expect(reloadResult['details']['loadedLibraryCount'], equals(2));
compiler.accept();
vm.kill();
});
});
group('reject', () {
late Directory mytest;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('incremental_reject');
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
});
test('compile, reject, compile again', () async {
new Directory(mytest.path + "/.dart_tool").createSync();
var packageUri =
Uri.file('${mytest.path}/.dart_tool/package_config.json');
new File(packageUri.toFilePath()).writeAsStringSync(jsonEncode({
"configVersion": 2,
"packages": [
{
"name": "foo",
"rootUri": "..",
"packageUri": "lib",
"languageVersion": "2.12",
},
],
}));
new Directory(mytest.path + "/lib").createSync();
var fooUri = Uri.file('${mytest.path}/lib/foo.dart');
new File(fooUri.toFilePath())
.writeAsStringSync("import 'package:foo/bar.dart';\n"
"import 'package:foo/baz.dart';\n"
"main() {\n"
" new A();\n"
" openReceivePortSoWeWontDie();"
"}\n");
var barUri = Uri.file('${mytest.path}/lib/bar.dart');
new File(barUri.toFilePath())
.writeAsStringSync("class A { static int a = 0; }\n");
var bazUri = Uri.file('${mytest.path}/lib/baz.dart');
new File(bazUri.toFilePath()).writeAsStringSync("import 'dart:isolate';\n"
"openReceivePortSoWeWontDie() { new RawReceivePort(); }\n");
Uri packageEntry = Uri.parse('package:foo/foo.dart');
CompilerOptions optionsModified = getFreshOptions()
..packagesFileUri = packageUri;
IncrementalCompiler compiler =
new IncrementalCompiler(optionsModified, [packageEntry]);
{
IncrementalCompilerResult compilerResult =
await compiler.compile(entryPoints: [packageEntry]);
Component component = compilerResult.component;
File outputFile = new File('${mytest.path}/foo.dart.dill');
await _writeProgramToFile(component, outputFile);
}
compiler.accept();
{
Procedure? procedure = await compiler.compileExpression(
'a',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
'package:foo/bar.dart',
'A',
null,
-1,
null,
true);
expect(procedure, isNotNull);
}
new File(barUri.toFilePath())
.writeAsStringSync("class A { static int b = 0; }\n");
compiler.invalidate(barUri);
{
IncrementalCompilerResult compilerResult =
await compiler.compile(entryPoints: [packageEntry]);
Component component = compilerResult.component;
File outputFile = new File('${mytest.path}/foo1.dart.dill');
await _writeProgramToFile(component, outputFile);
}
await compiler.reject();
{
Procedure? procedure = await compiler.compileExpression(
'a',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
'package:foo/bar.dart',
'A',
null,
-1,
null,
true);
expect(procedure, isNotNull);
}
});
/// This test basically verifies that components `relink` method is
/// correctly called when rejecting (i.e. logically going back in time to
/// before a rejected compilation).
test('check links after reject', () async {
final Uri fooUri = Uri.file('${mytest.path}/foo.dart');
new File.fromUri(fooUri).writeAsStringSync("""
import 'bar.dart';
main() {
A a = new A();
print(a.b());
print(A.a);
}
""");
final Uri barUri = Uri.file('${mytest.path}/bar.dart');
new File.fromUri(barUri).writeAsStringSync("""
class A {
static int a = 0;
int b() { return 42; }
}
""");
final CompilerOptions optionsModified = getFreshOptions();
optionsModified.explicitExperimentalFlags[
ExperimentalFlag.alternativeInvalidationStrategy] = true;
final IncrementalCompiler compiler =
new IncrementalCompiler(optionsModified, [fooUri]);
Library fooLib;
Library barLib;
{
final IncrementalCompilerResult compilerResult =
await compiler.compile(entryPoints: [fooUri]);
final Component component = compilerResult.component;
expect(component.libraries.length, equals(2));
fooLib = component.libraries.firstWhere((lib) => lib.fileUri == fooUri);
barLib = component.libraries.firstWhere((lib) => lib.fileUri == barUri);
// Verify that foo only has links to this bar.
final LibraryReferenceCollector lrc = new LibraryReferenceCollector();
fooLib.accept(lrc);
expect(lrc.librariesReferenced, equals(<Library>{barLib}));
}
compiler.accept();
{
final Procedure procedure = (await compiler.compileExpression(
'a',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
barUri.toString(),
'A',
null,
-1,
null,
true))!;
// Verify that the expression only has links to the only bar we know
// about.
final LibraryReferenceCollector lrc = new LibraryReferenceCollector();
procedure.accept(lrc);
expect(lrc.librariesReferenced, equals(<Library>{barLib}));
}
new File.fromUri(barUri).writeAsStringSync("""
class A {
static int a = 0;
int b() { return 84; }
}
""");
compiler.invalidate(barUri);
{
final IncrementalCompilerResult compilerResult =
await compiler.compile(entryPoints: [fooUri]);
final Component component = compilerResult.component;
final Library? fooLib2 = component.libraries
.firstWhereOrNull((lib) => lib.fileUri == fooUri);
expect(fooLib2, isNull);
final Library barLib2 =
component.libraries.firstWhere((lib) => lib.fileUri == barUri);
// Verify that the fooLib (we only have the original one) only has
// links to the newly compiled bar.
final LibraryReferenceCollector lrc = new LibraryReferenceCollector();
fooLib.accept(lrc);
expect(lrc.librariesReferenced, equals(<Library>{barLib2}));
}
await compiler.reject();
{
// Verify that the original foo library only has links to the original
// compiled bar.
final LibraryReferenceCollector lrc = new LibraryReferenceCollector();
fooLib.accept(lrc);
expect(lrc.librariesReferenced, equals(<Library>{barLib}));
}
{
// Verify that the saved "last known good" component only contains links
// to the original 'foo' and 'bar' libraries.
final LibraryReferenceCollector lrc = new LibraryReferenceCollector();
compiler.lastKnownGoodResult!.component.accept(lrc);
expect(lrc.librariesReferenced, equals(<Library>{fooLib, barLib}));
}
{
final Procedure procedure = (await compiler.compileExpression(
'a',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
barUri.toString(),
'A',
null,
-1,
null,
true))!;
// Verify that the expression only has links to the original bar.
final LibraryReferenceCollector lrc = new LibraryReferenceCollector();
procedure.accept(lrc);
expect(lrc.librariesReferenced, equals(<Library>{barLib}));
}
});
test('compile, recompile with new entry, reject, expression compilation',
() async {
var v1Uri = Uri.file('${mytest.path}/v1.dart');
new File(v1Uri.toFilePath()).writeAsStringSync("""import 'dart:isolate';
test() => 'apple';
main() {
RawReceivePort keepAlive = new RawReceivePort();
print('spawned isolate running');
}
""");
var v2Uri = Uri.file('${mytest.path}/v2.dart');
new File(v2Uri.toFilePath()).writeAsStringSync("""import 'dart:isolate';
import 'library_isnt_here_man';
test() => 'apple';
main() {
RawReceivePort keepAlive = new RawReceivePort();
print('spawned isolate running');
}
""");
CompilerOptions optionsModified = getFreshOptions()
..onDiagnostic = (DiagnosticMessage message) {
// sometimes expected on this one.
print(message.ansiFormatted);
};
IncrementalCompiler compiler =
new IncrementalCompiler(optionsModified, [v1Uri]);
print("Compiling v1 uri (should be ok)");
await compiler.compile(entryPoints: [v1Uri]);
print("Accepting.");
compiler.accept();
{
print("Compiling expression.");
Procedure? procedure = await compiler.compileExpression(
'test()',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
v1Uri.toString(),
null,
null,
-1,
null,
false);
expect(procedure, isNotNull);
ReturnStatement returnStmt =
procedure!.function.body as ReturnStatement;
expect(returnStmt.expression, isA<StaticInvocation>());
}
print("Compiling v2 uri (with error)");
await compiler.compile(entryPoints: [v2Uri]);
print("Rejecting.");
await compiler.reject();
{
print("Compiling expression.");
Procedure? procedure = await compiler.compileExpression(
'test()',
<String>[],
<String>[],
<String>[],
<String>[],
<String>[],
v1Uri.toString(),
null,
null,
-1,
null,
false);
expect(procedure, isNotNull);
ReturnStatement returnStmt =
procedure!.function.body as ReturnStatement;
expect(returnStmt.expression, isA<StaticInvocation>());
}
});
});
group('expression evaluation', () {
late Directory mytest;
late Process vm;
setUpAll(() {
mytest = Directory.systemTemp.createTempSync('expression_evaluation');
});
tearDownAll(() {
try {
mytest.deleteSync(recursive: true);
} catch (_) {
// Ignore errors;
}
try {
vm.kill();
} catch (_) {
// Ignore errors;
}
});
launchBreakAndEvaluate(File scriptOrDill, String scriptUriToBreakIn,
int lineToBreakAt, List<String> expressionsAndExpectedResults,
{Future Function(RemoteVm remoteVm)? callback}) async {
vm = await Process.start(Platform.resolvedExecutable, <String>[
"--pause-isolates-on-start",
"--enable-vm-service:0",
"--disable-service-auth-codes",
"--disable-dart-dev",
scriptOrDill.path
]);
const kDartVMServiceListening = 'The Dart VM service is listening on ';
final RegExp dartVMServicePortRegExp = new RegExp(
"The Dart VM service is listening on http://127.0.0.1:\([0-9]*\)");
int port;
final splitter = new LineSplitter();
Completer<String> portLineCompleter = new Completer<String>();
vm.stdout
.transform(utf8.decoder)
.transform(splitter)
.listen((String s) async {
print("vm stdout: $s");
if (s.startsWith(kDartVMServiceListening)) {
expect(dartVMServicePortRegExp.hasMatch(s), isTrue);
final match = dartVMServicePortRegExp.firstMatch(s)!;
port = int.parse(match.group(1)!);
RemoteVm remoteVm = new RemoteVm(port);
// Wait for the script to have loaded.
while (true) {
Map isolate = await remoteVm.getIsolate();
Map pauseEvent = isolate["pauseEvent"];
if (pauseEvent["kind"] == "PauseStart") break;
}
var breakpoint = await findScriptAndBreak(
remoteVm, scriptUriToBreakIn, lineToBreakAt);
await remoteVm.resume();
await waitForScriptToHavePaused(remoteVm);
await evaluateExpressions(expressionsAndExpectedResults, remoteVm);
await deletePossibleBreakpoint(remoteVm, breakpoint);
if (callback != null) {
await callback(remoteVm);
}
await remoteVm.resume();
if (!portLineCompleter.isCompleted) {
portLineCompleter.complete("done");
}
}
});
bool gotStdErrOutput = false;
vm.stderr.transform(utf8.decoder).transform(splitter).listen((String s) {
print("vm stderr: $s");
gotStdErrOutput = true;
});
await portLineCompleter.future;
int exitCode = await vm.exitCode;
print("Compiler terminated with ${exitCode} exit code");
expect(exitCode, equals(0));
expect(gotStdErrOutput, isFalse);
}
test('from source', () async {
Directory dir = mytest.createTempSync();
File mainFile = new File.fromUri(dir.uri.resolve("main.dart"));
mainFile.writeAsStringSync(r"""
var hello = "Hello";
main() {
var s = "$hello world!";
print(s);
}
int extra() { return 22; }
""");
await launchBreakAndEvaluate(mainFile, mainFile.uri.toString(), 4, [
// 1st expression
"s.length",
"12",
// 2nd expression
"s",
"Hello world!",
// 3rd expression
"hello",
"Hello",
// 4th expression
"extra()",
"22",
]);
});
test('from dill', () async {
Directory dir = mytest.createTempSync();
File mainFile = new File.fromUri(dir.uri.resolve("main.dart"));
mainFile.writeAsStringSync(r"""
var hello = "Hello";
main() {
var s = "$hello world!";
print(s);
}
int extra() { return 22; }
""");
IncrementalCompiler compiler =
new IncrementalCompiler(options, [mainFile.uri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
File mainDill = new File.fromUri(mainFile.uri.resolve("main.dill"));
IOSink sink = mainDill.openWrite();
new BinaryPrinter(sink).writeComponentFile(component);
await sink.flush();
await sink.close();
mainFile.deleteSync();
await launchBreakAndEvaluate(mainDill, mainFile.uri.toString(), 4, [
// 1st expression
"s.length",
"12",
// 2nd expression
"s",
"Hello world!",
// 3rd expression
"hello",
"Hello",
// 4th expression
"extra()",
"22",
]);
});
test('from dill with reload', () async {
Directory dir = mytest.createTempSync();
File mainFile = new File.fromUri(dir.uri.resolve("main.dart"));
mainFile.writeAsStringSync(r"""
import 'dart:async';
import 'helper.dart';
main() {
int latestReloadTime = -1;
int noChangeCount = 0;
int numChanges = 0;
new Timer.periodic(new Duration(milliseconds: 5), (timer) async {
var result = reloadTime();
if (latestReloadTime != result) {
latestReloadTime = result;
numChanges++;
helperMethod();
} else {
noChangeCount++;
}
if (latestReloadTime == 42) {
timer.cancel();
}
if (numChanges > 20) {
timer.cancel();
}
if (noChangeCount >= 400) {
// ~2 seconds with no change.
throw "Expected to be done but wasn't";
}
});
}
""");
File helperFile = new File.fromUri(dir.uri.resolve("helper.dart"));
helperFile.writeAsStringSync(r"""
int reloadTime() {
return 0;
}
void helperMethod() {
var hello = "Hello";
var s = "$hello world!";
print(s);
}
""");
IncrementalCompiler compiler =
new IncrementalCompiler(options, [mainFile.uri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
File mainDill = new File.fromUri(mainFile.uri.resolve("main.dill"));
IOSink sink = mainDill.openWrite();
new BinaryPrinter(sink).writeComponentFile(component);
await sink.flush();
await sink.close();
print("=> Notice main file has size ${mainDill.lengthSync()}");
helperFile.writeAsStringSync(r"""
int reloadTime() {
return 1;
}
void helperMethod() {
var hello = "Hello";
var s = "$hello world!!!";
print(s);
}
int helperMethod2() {
return 42;
}
""");
compiler.invalidate(helperFile.uri);
compilerResult = await compiler.compile();
component = compilerResult.component;
File partial1Dill =
new File.fromUri(mainFile.uri.resolve("partial1.dill"));
sink = partial1Dill.openWrite();
new BinaryPrinter(sink).writeComponentFile(component);
await sink.flush();
await sink.close();
print("=> Notice partial file #1 has size ${partial1Dill.lengthSync()}");
helperFile.writeAsStringSync(r"""
int reloadTime() {
return 2;
}
void helperMethod() {
var hello = "Hello";
var s = "$hello world!!!!";
print(s);
}
int helperMethod2() {
return 21;
}
int helperMethod3() {
return 84;
}
""");
compiler.invalidate(helperFile.uri);
compilerResult = await compiler.compile();
component = compilerResult.component;
File partial2Dill =
new File.fromUri(mainFile.uri.resolve("partial2.dill"));
sink = partial2Dill.openWrite();
new BinaryPrinter(sink).writeComponentFile(component);
await sink.flush();
await sink.close();
print("=> Notice partial file #2 has size ${partial2Dill.lengthSync()}");
mainFile.deleteSync();
helperFile.deleteSync();
await launchBreakAndEvaluate(mainDill, helperFile.uri.toString(), 7, [
// 1st expression
"s.length",
"12",
// 2nd expression
"s",
"Hello world!",
// 3rd expression
"hello",
"Hello",
// 4th expression
"reloadTime()",
"0",
], callback: (RemoteVm remoteVm) async {
for (int q = 0; q < 10; q++) {
Map reloadResult = await remoteVm.reload(partial1Dill.uri);
expect(reloadResult["success"], equals(true));
await remoteVm.forceGc();
var breakpoint =
await findScriptAndBreak(remoteVm, helperFile.uri.toString(), 7);
await remoteVm.resume();
await waitForScriptToHavePaused(remoteVm);
await evaluateExpressions([
// 1st expression
"s.length",
"14",
// 2nd expression
"s",
"Hello world!!!",
// 3rd expression
"hello",
"Hello",
// 4th expression
"reloadTime()",
"1",
// 5th expression
"helperMethod2()",
"42",
], remoteVm);
await deletePossibleBreakpoint(remoteVm, breakpoint);
reloadResult = await remoteVm.reload(partial2Dill.uri);
expect(reloadResult["success"], equals(true));
await remoteVm.forceGc();
breakpoint =
await findScriptAndBreak(remoteVm, helperFile.uri.toString(), 7);
await remoteVm.resume();
await waitForScriptToHavePaused(remoteVm);
await evaluateExpressions([
// 1st expression
"s.length",
"15",
// 2nd expression
"s",
"Hello world!!!!",
// 3rd expression
"hello",
"Hello",
// 4th expression
"reloadTime()",
"2",
// 5th expression
"helperMethod2()",
"21",
// 6th expression
"helperMethod3()",
"84",
], remoteVm);
await deletePossibleBreakpoint(remoteVm, breakpoint);
}
});
});
test('from dill with package uri', () async {
// 2 iterations: One where the package_config.json file is deleted, and one where
// it is not.
for (int i = 0; i < 2; i++) {
Directory dir = mytest.createTempSync();
File mainFile = new File.fromUri(dir.uri.resolve("main.dart"));
mainFile.writeAsStringSync(r"""
var hello = "Hello";
main() {
var s = "$hello world!";
print(s);
}
int extra() { return 22; }
""");
File packagesFile =
new File.fromUri(dir.uri.resolve("package_config.json"));
packagesFile.writeAsStringSync(jsonEncode({
"configVersion": 2,
"packages": [
{
"name": "foo",
"rootUri": ".",
"languageVersion": "2.12",
},
],
}));
Uri mainUri = Uri.parse("package:foo/main.dart");
CompilerOptions optionsModified = getFreshOptions()
..packagesFileUri = packagesFile.uri;
IncrementalCompiler compiler =
new IncrementalCompiler(optionsModified, [mainUri]);
IncrementalCompilerResult compilerResult = await compiler.compile();
Component component = compilerResult.component;
File mainDill = new File.fromUri(mainFile.uri.resolve("main.dill"));
IOSink sink = mainDill.openWrite();
new BinaryPrinter(sink).writeComponentFile(component);
await sink.flush();
await sink.close();
mainFile.deleteSync();
if (i == 0) {
packagesFile.deleteSync();
}
await launchBreakAndEvaluate(mainDill, mainUri.toString(), 4, [
// 1st expression
"s.length",
"12",
// 2nd expression
"s",
"Hello world!",
// 3rd expression
"hello",
"Hello",
// 4th expression
"extra()",
"22",
]);
try {
dir.deleteSync(recursive: true);
} catch (e) {
// ignore.
}
}
});
});
}
Future evaluateExpressions(
List<String> expressionsAndExpectedResults, RemoteVm remoteVm) async {
for (int i = 0; i < expressionsAndExpectedResults.length; i += 2) {
String expression = expressionsAndExpectedResults[i];
String expectedResult = expressionsAndExpectedResults[i + 1];
print("Evaluating $expression (expecting $expectedResult)");
var result = await remoteVm.evaluateInFrame(expression);
expect(result is Map, isTrue);
expect(result["type"], equals("@Instance"));
expect(result["valueAsString"], equals(expectedResult));
}
}
Future waitForScriptToHavePaused(RemoteVm remoteVm) async {
// Wait for the script to have paused.
while (true) {
Map isolate = await remoteVm.getIsolate();
Map pauseEvent = isolate["pauseEvent"];
if (pauseEvent["kind"] == "PauseBreakpoint") break;
}
}
Future findScriptAndBreak(
RemoteVm remoteVm, String scriptUriToBreakIn, int lineToBreakAt) async {
Map scriptsMap = await remoteVm.getScripts();
List scripts = scriptsMap["scripts"];
String? scriptId;
for (int i = 0; i < scripts.length; i++) {
Map script = scripts[i];
String scriptUri = script["uri"];
if (scriptUri == scriptUriToBreakIn) {
scriptId = script["id"];
break;
}
}
expect(scriptId, isNotNull);
return await remoteVm.addBreakpoint(scriptId!, lineToBreakAt);
}
Future deletePossibleBreakpoint(
RemoteVm remoteVm, dynamic possibleBreakpoint) async {
if (possibleBreakpoint is Map && possibleBreakpoint["id"] is String) {
return await remoteVm.removeBreakpoint(possibleBreakpoint["id"]);
}
}
_writeProgramToFile(Component component, File outputFile) async {
final IOSink sink = outputFile.openWrite();
final BinaryPrinter printer = new BinaryPrinter(sink);
printer.writeComponentFile(component);
await sink.flush();
await sink.close();
}
class LibraryReferenceCollector extends RecursiveVisitor {
Set<Library> librariesReferenced = {};
void defaultMemberReference(Member node) {
Library lib = node.enclosingLibrary;
if (!lib.importUri.isScheme("dart")) {
librariesReferenced.add(lib);
}
return super.defaultMemberReference(node);
}
}
/// APIs to communicate with a remote VM via the VM's service protocol.
///
/// Only supports APIs to resume the program execution (when isolates are paused
/// at startup) and to trigger hot reloads.
class RemoteVm {
/// Port used to connect to the vm service protocol, typically 8181.
final int port;
/// An peer point used to send service protocol messages. The service
/// protocol uses JSON rpc on top of web-sockets.
json_rpc.Peer get rpc => _rpc ??= _createPeer();
json_rpc.Peer? _rpc;
/// The main isolate ID of the running VM. Needed to indicate to the VM which
/// isolate to reload.
FutureOr<String> get mainId async => _mainId ??= await _computeMainId();
String? _mainId;
RemoteVm([this.port = 8181]);
/// Establishes the JSON rpc connection.
json_rpc.Peer _createPeer() {
var socket = new IOWebSocketChannel.connect('ws://127.0.0.1:$port/ws');
var peer = new json_rpc.Peer(socket.cast<String>());
peer.listen().then((_) {
print('connection to vm-service closed');
return disconnect();
}).catchError((e) {
print('error connecting to the vm-service');
return disconnect();
});
return peer;
}
/// Retrieves the ID of the main isolate using the service protocol.
Future<String> _computeMainId() async {
var completer = new Completer<String>();
rpc.registerMethod('streamNotify', (response) {
if (response['streamId'] == 'Isolate') return;
var event = response['event'];
if (event['kind'] != 'IsolateStart') return;
var isolate = event['isolate'];
completer.complete(isolate['id']);
});
await rpc.sendRequest('streamListen', {'streamId': 'Isolate'});
var vm = await rpc.sendRequest('getVM', {});
var isolates = vm['isolates'];
for (var isolate in isolates) {
if (isolate['name'].contains(r'$main')) {
return isolate['id'];
}
}
for (var isolate in isolates) {
return isolate['id'];
}
return completer.future;
}
/// Send a request to the VM to reload sources from [entryUri].
///
/// This will establish a connection with the VM assuming it is running on the
/// local machine and listening on [port] for service protocol requests.
///
/// The result is the JSON map received from the reload request.
Future<Map> reload(Uri entryUri) async {
print("reload($entryUri)");
var id = await mainId;
print("got $id, sending reloadSources rpc request");
var result = await rpc.sendRequest('reloadSources', {
'isolateId': id,
'rootLibUri': entryUri.toString(),
});
print("got rpc result $result");
return result;
}
Future resume() async {
var id = await mainId;
await rpc.sendRequest('resume', {'isolateId': id});
}
Future<Map> getIsolate() async {
var id = await mainId;
return (await rpc.sendRequest('getIsolate', {'isolateId': id})) as Map;
}
Future<Map> getScripts() async {
var id = await mainId;
return (await rpc.sendRequest('getScripts', {
'isolateId': id,
})) as Map;
}
Future<Map> getSourceReport([String? scriptId]) async {
var id = await mainId;
return (await rpc.sendRequest('getSourceReport', {
'isolateId': id,
if (scriptId != null) 'scriptId': scriptId,
'reports': ['Coverage', 'PossibleBreakpoints'],
'forceCompile': true,
})) as Map;
}
Future<Map> getObject(String objectId) async {
var id = await mainId;
return (await rpc.sendRequest('getObject', {
'isolateId': id,
'objectId': objectId,
})) as Map;
}
Future addBreakpoint(String scriptId, int line) async {
var id = await mainId;
return await rpc.sendRequest('addBreakpoint', {
'isolateId': id,
'scriptId': scriptId,
'line': line,
});
}
Future removeBreakpoint(String breakpointId) async {
var id = await mainId;
return await rpc.sendRequest('removeBreakpoint', {
'isolateId': id,
'breakpointId': breakpointId,
});
}
Future evaluateInFrame(String expression) async {
var id = await mainId;
var frameIndex = 0;
return await rpc.sendRequest('evaluateInFrame', {
'isolateId': id,
"frameIndex": frameIndex,
'expression': expression,
});
}
Future forceGc() async {
int expectGcAfter = new DateTime.now().millisecondsSinceEpoch;
while (true) {
var id = await mainId;
Map result = (await rpc.sendRequest('getAllocationProfile', {
'isolateId': id,
"gc": true,
})) as Map;
String? lastGc = result["dateLastServiceGC"];
if (lastGc != null && int.parse(lastGc) >= expectGcAfter) return;
}
}
/// Close any connections used to communicate with the VM.
Future disconnect() async {
final rpc = this._rpc;
if (rpc == null) return null;
this._mainId = null;
if (!rpc.isClosed) {
var future = rpc.close();
_rpc = null;
return future;
}
return null;
}
}