// 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:front_end/src/api_unstable/vm.dart' show CompilerOptions, DiagnosticMessage, ExperimentalFlag, 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 ..target = new VmTarget(new TargetFlags()) ..additionalDills = [platformKernel] ..onDiagnostic = (DiagnosticMessage message) { fail("Compilation error: ${message.plainTextFormatted.join('\n')}"); } ..environmentDefines = const {}; } final options = getFreshOptions(); group('basic', () { Directory mytest; 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); Component component = await compiler.compile(); final StringBuffer buffer = new StringBuffer(); new Printer(buffer, showMetadata: true) .writeLibraryFile(component.mainMethod.enclosingLibrary); expect( buffer.toString(), equals('library /*isNonNullableByDefault*/;\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); Component component = await compiler.compile(); 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 /*isNonNullableByDefault*/;\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', [], [], main.uri.toString(), null, true); expect(procedure, isNotNull); expect(errorsReported, equals(0)); } { Procedure procedure = await compiler.compileExpression( 'main1', [], [], main.uri.toString(), 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> collectAndCheckCoverageData(int port, bool getAllSources, {bool resume: true, bool onGetAllVerifyCount: true, Set 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 sourceReports = []; if (getAllSources) { Map sourceReport = await remoteVm.getSourceReport(); sourceReports.add(sourceReport); } else { Map scriptsMap = await remoteVm.getScripts(); List scripts = scriptsMap["scripts"]; Set scriptIds = new Set(); 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 errorMessages = []; Set hits = new Set(); // 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 scripts = sourceReport["scripts"]; Map scriptIdToIndex = new Map(); Set lib1scriptIndices = new Set(); 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 scriptIndexToScript = new 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 ranges = sourceReport["ranges"]; Set scriptIndexesSet = new Set.from(scriptIndexToScript.keys); for (Map range in ranges) { if (scriptIndexesSet.contains(range["scriptIndex"])) { Set positions = new Set(); 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 knownPositions = new Set(); Map 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', () { Directory mytest; File main; File lib; 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 C2, C3 { c1method() { print("c1"); } } class C3 { c3method() { print("c3"); } } """); lib = new File('${mytest.path}/lib.dart')..createSync(); lib.writeAsStringSync(""" import 'main.dart'; foo() => 'foo'; main() => print('bar'); class C2 extends Object with 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 { Component component = await compiler.compile(); new BinaryPrinter(new DevNullSink>()) .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, [list.path]); final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); 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, [list.path]); final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); 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, [list.path]); Completer exitCodeCompleter = new Completer(); 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, [list.path]); final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); 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, [list.path], workingDirectory: runFrom.path); final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); 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, [ "--pause-isolates-on-exit", "--enable-vm-service:0", "--disable-service-auth-codes", "--disable-dart-dev", list.path ]); const kObservatoryListening = 'Observatory listening on '; final RegExp observatoryPortRegExp = new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)"); int port; final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); vm.stdout .transform(utf8.decoder) .transform(splitter) .listen((String s) async { if (s.startsWith(kObservatoryListening)) { expect(observatoryPortRegExp.hasMatch(s), isTrue); final match = observatoryPortRegExp.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', () { Directory mytest; File main; File lib1; int lineForUnnamedConstructor; int lineForNamedConstructor; 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> runAndGetLineCoverage( File list, String expectStdoutContains) async { vm = await Process.start(Platform.resolvedExecutable, [ "--pause-isolates-on-exit", "--enable-vm-service:0", "--disable-service-auth-codes", "--disable-dart-dev", list.path ]); const kObservatoryListening = 'Observatory listening on '; final RegExp observatoryPortRegExp = new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)"); int port; final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); Set coverageLines = {}; bool foundExpectedString = false; vm.stdout .transform(utf8.decoder) .transform(splitter) .listen((String s) async { if (s == expectStdoutContains) { foundExpectedString = true; } if (s.startsWith(kObservatoryListening)) { expect(observatoryPortRegExp.hasMatch(s), isTrue); final match = observatoryPortRegExp.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 seperatly, 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); Component component = await compiler.compile(); 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 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 coveraged too). File mainDill = File(p.join(dir.path, p.basename(main.path + ".dill"))); component = await compiler.compile(entryPoint: main.uri); 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 coverred // 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); component = await compiler.compile(entryPoint: lib1.uri); 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', () { Directory mytest; File main; File lib1; File lib2; 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 { Component component = await compiler.compile(); new BinaryPrinter(new DevNullSink>()) .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, [ "--pause-isolates-on-exit", "--enable-vm-service:0", "--disable-service-auth-codes", "--disable-dart-dev", list.path ]); const kObservatoryListening = 'Observatory listening on '; final RegExp observatoryPortRegExp = new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)"); int port; final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); vm.stdout .transform(utf8.decoder) .transform(splitter) .listen((String s) async { if (s.startsWith(kObservatoryListening)) { expect(observatoryPortRegExp.hasMatch(s), isTrue); final match = observatoryPortRegExp.firstMatch(s); port = int.parse(match.group(1)); Set hits1 = await collectAndCheckCoverageData(port, true, resume: false); Set 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', () { 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 { 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); Component component = await compiler.compile(); File outputFile = new File('${mytest.path}/foo.dart.dill'); await _writeProgramToFile(component, outputFile); final List 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 portLineCompleter = new Completer(); 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 observatoryPortRegExp = new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)"); expect(observatoryPortRegExp.hasMatch(portLine), isTrue); final match = observatoryPortRegExp.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. component = await compiler.compile(); 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 { int _a = 0; }\n"); compiler.invalidate(fileBar.uri); component = await compiler.compile(); 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 { int _a = 0; hi() => _a; }\n"); compiler.invalidate(fileBar.uri); component = await compiler.compile(); 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', () { Directory mytest; setUpAll(() { mytest = Directory.systemTemp.createTempSync('incremental_reject'); }); tearDownAll(() { try { mytest.deleteSync(recursive: true); } catch (_) { // Ignore errors; } }); test('compile, reject, compile again', () async { var packageUri = Uri.file('${mytest.path}/.packages'); new File(packageUri.toFilePath()).writeAsStringSync('foo:lib/\n'); 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; }\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); { Component component = await compiler.compile(entryPoint: packageEntry); File outputFile = new File('${mytest.path}/foo.dart.dill'); await _writeProgramToFile(component, outputFile); } compiler.accept(); { Procedure procedure = await compiler.compileExpression( 'a', [], [], 'package:foo/bar.dart', 'A', true); expect(procedure, isNotNull); } new File(barUri.toFilePath()) .writeAsStringSync("class A { static int b; }\n"); compiler.invalidate(barUri); { Component component = await compiler.compile(entryPoint: packageEntry); File outputFile = new File('${mytest.path}/foo1.dart.dill'); await _writeProgramToFile(component, outputFile); } await compiler.reject(); { Procedure procedure = await compiler.compileExpression( 'a', [], [], 'package:foo/bar.dart', 'A', true); expect(procedure, isNotNull); } }); /// This test basicaly 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; 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 Component component = await compiler.compile(entryPoint: fooUri); 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({barLib})); } compiler.accept(); { final Procedure procedure = await compiler.compileExpression( 'a', [], [], barUri.toString(), 'A', true); expect(procedure, isNotNull); // 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({barLib})); } new File.fromUri(barUri).writeAsStringSync(""" class A { static int a; int b() { return 84; } } """); compiler.invalidate(barUri); { final Component component = await compiler.compile(entryPoint: fooUri); final Library fooLib2 = component.libraries .firstWhere((lib) => lib.fileUri == fooUri, orElse: () => null); 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({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({barLib})); } { // Verify that the saved "last known good" compnent only contains links // to the original 'foo' and 'bar' libraries. final LibraryReferenceCollector lrc = new LibraryReferenceCollector(); compiler.lastKnownGoodComponent.accept(lrc); expect(lrc.librariesReferenced, equals({fooLib, barLib})); } { final Procedure procedure = await compiler.compileExpression( 'a', [], [], barUri.toString(), 'A', true); expect(procedure, isNotNull); // Verify that the expression only has links to the original bar. final LibraryReferenceCollector lrc = new LibraryReferenceCollector(); procedure.accept(lrc); expect(lrc.librariesReferenced, equals({barLib})); } }); }); group('expression evaluation', () { Directory mytest; 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 expressionsAndExpectedResults, {Future Function(RemoteVm remoteVm) callback}) async { vm = await Process.start(Platform.resolvedExecutable, [ "--pause-isolates-on-start", "--enable-vm-service:0", "--disable-service-auth-codes", "--disable-dart-dev", scriptOrDill.path ]); const kObservatoryListening = 'Observatory listening on '; final RegExp observatoryPortRegExp = new RegExp("Observatory listening on http://127.0.0.1:\([0-9]*\)"); int port; final splitter = new LineSplitter(); Completer portLineCompleter = new Completer(); vm.stdout .transform(utf8.decoder) .transform(splitter) .listen((String s) async { print("vm stdout: $s"); if (s.startsWith(kObservatoryListening)) { expect(observatoryPortRegExp.hasMatch(s), isTrue); final match = observatoryPortRegExp.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); Component component = await compiler.compile(); 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); Component component = await compiler.compile(); 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); component = await compiler.compile(); 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); component = await compiler.compile(); 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++) { var reloadResult = await remoteVm.reload(partial1Dill.uri); expect(reloadResult is Map, isTrue); 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 is Map, isTrue); 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 .packages 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(".packages")); packagesFile.writeAsStringSync("foo:."); Uri mainUri = Uri.parse("package:foo/main.dart"); CompilerOptions optionsModified = getFreshOptions() ..packagesFileUri = packagesFile.uri; IncrementalCompiler compiler = new IncrementalCompiler(optionsModified, mainUri); Component component = await compiler.compile(); 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 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 librariesReferenced = {}; void defaultMemberReference(Member node) { Library lib = node.enclosingLibrary; if (lib.importUri.scheme != "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 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()); 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 _computeMainId() async { var vm = await rpc.sendRequest('getVM', {}); var isolates = vm['isolates']; for (var isolate in isolates) { if (isolate['name'].contains(r'$main')) { return isolate['id']; } } return isolates.first['id']; } /// 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 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 getIsolate() async { var id = await mainId; return await rpc.sendRequest('getIsolate', {'isolateId': id}); } Future getScripts() async { var id = await mainId; return await rpc.sendRequest('getScripts', { 'isolateId': id, }); } Future getSourceReport([String scriptId]) async { var id = await mainId; if (scriptId != null) { return await rpc.sendRequest('getSourceReport', { 'isolateId': id, 'scriptId': scriptId, 'reports': ['Coverage', 'PossibleBreakpoints'], 'forceCompile': true }); } return await rpc.sendRequest('getSourceReport', { 'isolateId': id, 'reports': ['Coverage', 'PossibleBreakpoints'], 'forceCompile': true }); } Future getObject(String objectId) async { var id = await mainId; return await rpc.sendRequest('getObject', { 'isolateId': id, 'objectId': objectId, }); } 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, }); String lastGc = result["dateLastServiceGC"]; if (lastGc != null && int.parse(lastGc) >= expectGcAfter) return; } } /// Close any connections used to communicate with the VM. Future disconnect() async { if (_rpc == null) return null; this._mainId = null; if (!_rpc.isClosed) { var future = _rpc.close(); _rpc = null; return future; } return null; } }