| // 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, |
| 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 = <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 /*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]); |
| 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 /*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', |
| <String>[], |
| <String>[], |
| <String>[], |
| <String>[], |
| <String>[], |
| main.uri.toString(), |
| null, |
| 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, |
| 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"); |
| } |
| } |
| class 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.7", |
| }, |
| ], |
| })); |
| 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]); |
| { |
| 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, |
| true); |
| expect(procedure, isNotNull); |
| } |
| |
| new File(barUri.toFilePath()) |
| .writeAsStringSync("class A { static int b; }\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, |
| 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; |
| 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, |
| 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; |
| 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, |
| 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})); |
| } |
| }); |
| }); |
| |
| 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.7", |
| }, |
| ], |
| })); |
| |
| 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; |
| } |
| } |