[DDC/dart2js] Move sourcemap testing to shared place
This moves the logic previously used in DDC for testing stepping via sourcemaps to a shared location so both DDC and dart2js can make use of the same thing. Bug: Change-Id: I83a2b1fc4fe5cefe3c8537e50d03c3e41e0490a8 Reviewed-on: https://dart-review.googlesource.com/24110 Commit-Queue: Jens Johansen <jensj@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
committed by
commit-bot@chromium.org
parent
b0160af36a
commit
03090df3fa
@@ -1,9 +1,9 @@
|
||||
# Testing of source maps
|
||||
|
||||
This folder contains a testing framework for validating the debugging behavior of the generated js.
|
||||
It currently has 2 suits: One for (legacy) DDC and one for DDC with kernel (DDK).
|
||||
In addition it uses a framework for testing stacktraces. This also exists in the same two
|
||||
configurations.
|
||||
This folder contains two types of tests for validating sourcemaps:
|
||||
the debugging behavior and the stacktrace behavior.
|
||||
|
||||
For both there are 2 suits: One for (legacy) DDC and one for DDC with kernel (DDK).
|
||||
|
||||
Running the tests likely requires the compilation of the correct targets. DDK currently also
|
||||
requires `ddc_sdk.dill` inside
|
||||
@@ -16,49 +16,9 @@ All tests are plain Dart files and goes in "testfiles" (debugging tests) or "sta
|
||||
|
||||
## Debugging tests (step tests)
|
||||
|
||||
These tests works in a few steps:
|
||||
1) Create the JS
|
||||
2) Run the JS with D8, setting instructed brakepoints etc.
|
||||
3) Translating JS positions to dart positions
|
||||
4) Validating the stopped at positions.
|
||||
See `README.md` in `pkg/sourcemap_testing`.
|
||||
|
||||
The test files themselves contain information about where to stop, which brakepoints to expect etc.
|
||||
The contain this information in comments inlined in the code as in `/*key*/` where `key` can be on
|
||||
of the following:
|
||||
|
||||
* **fail**: Will fail the test. Useful for debugging in conjunction with
|
||||
`-Ddebug=true -- suite//singletest` (see below).
|
||||
* **Debugger:stepOver**: Will step over breakpoints. Default (i.e. without this) is to step into.
|
||||
* **bl** (break line): insert a breakpoint on this line. This does not add any new expected breaks.
|
||||
* **s:{i}** (stop): adds an expected stop as the `i`th stop (1-indexed).
|
||||
* **sl:{i}** (stop at line): adds an expected stop as the `i`th stop (1-indexed). Only check the
|
||||
line number.
|
||||
* **nb** (no break): The debugger should never break on this line.
|
||||
* **nbc** (no break column): The debugger should never break on this line and column.
|
||||
* **nbb:{i}:{j}** (no break between): The debugger should not break on this line between expectation
|
||||
`i` and `j` (1-indexed). Note that `from` can also be the special value `0` meaning from the very
|
||||
first stop. For example `nbb:0:1` means not before first expected stop.
|
||||
* **nm** (no mapping): There's not allowed to be any mapping to this line.
|
||||
* **bc:{i}** (break column): inserts a breakpoint at this line and column and adds an expected stop
|
||||
as the `i`th stop (1-indexed).
|
||||
|
||||
Note that in an ideal world `bc:{i}` would not be unnecessary: Stopping at a line and stepping
|
||||
should generally be enough. Because of the current behavior of d8 though, for instance
|
||||
```
|
||||
baz(foo(), bar())
|
||||
```
|
||||
will stop at `baz`, go into `foo`, stop at `bar`, go into `bar` and stop at `baz`.
|
||||
From a Dart perspective we would instead expect it to stop at `foo`, go into `foo`, stop at `bar`,
|
||||
go into `bar` and stop a `baz`.
|
||||
Having **bc:{i}** allows us to force this behavior as d8 can actually stop at `foo` too.
|
||||
|
||||
All of these annotations are removed before compiling to js and the expected output thus refers to
|
||||
the unannotated code.
|
||||
|
||||
When the test confirms that the debugger broke at the expected locations it allows for additional
|
||||
breakpoints before, between and after the expected breakpoints.
|
||||
|
||||
## Debugging a test
|
||||
### Debugging a test
|
||||
|
||||
One can filter which tests are run by running (from the sourcemap folder):
|
||||
```
|
||||
@@ -104,17 +64,3 @@ Stop #3
|
||||
|
||||
[...]
|
||||
```
|
||||
|
||||
## Technical details
|
||||
|
||||
Some of the logic comes from https://github.com/ChromeDevTools/devtools-frontend/, for instance see
|
||||
https://github.com/ChromeDevTools/devtools-frontend/blob/fa18d70a995f06cb73365b2e5b8ae974cf60bd3a/
|
||||
front_end/sources/JavaScriptSourceFrame.js#L1520-L1523
|
||||
for how a line breakpoint is resolved:
|
||||
Basically the line asked to break on in user code (e.g. in dart code) is asked for first and last
|
||||
javascript positions; these are then used to get possible breakpoints in that part. If there are
|
||||
none it tries the next line (etc for a number of lines). Once it finds something (in javascript
|
||||
positions) it converts that to user code position (e.g. in dart code), normalizes it by converting
|
||||
to javascript position and back to user code position again, then converts to javascript position
|
||||
and sets the breakpoint.
|
||||
This is to some extend mimicked here when setting a line break (though not a "column break").
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:expect/minitest.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:source_maps/source_maps.dart';
|
||||
import 'package:sourcemap_testing/src/annotated_code_helper.dart';
|
||||
import 'package:sourcemap_testing/src/stepping_helper.dart';
|
||||
import 'package:testing/testing.dart';
|
||||
|
||||
class Data {
|
||||
@@ -17,34 +16,6 @@ class Data {
|
||||
List<String> d8Output;
|
||||
}
|
||||
|
||||
class DartStackTraceDataEntry {
|
||||
final String file;
|
||||
final int line;
|
||||
final int column;
|
||||
final errorString;
|
||||
final int jsLine;
|
||||
final int jsColumn;
|
||||
|
||||
DartStackTraceDataEntry(
|
||||
this.file, this.line, this.column, this.jsLine, this.jsColumn)
|
||||
: errorString = null;
|
||||
DartStackTraceDataEntry.error(this.errorString)
|
||||
: file = null,
|
||||
line = -1,
|
||||
column = -1,
|
||||
jsLine = -1,
|
||||
jsColumn = -1;
|
||||
DartStackTraceDataEntry.errorWithJsPosition(
|
||||
this.errorString, this.jsLine, this.jsColumn)
|
||||
: file = null,
|
||||
line = -1,
|
||||
column = -1;
|
||||
|
||||
get isError => errorString != null;
|
||||
|
||||
String toString() => isError ? errorString : "$file:$line:$column";
|
||||
}
|
||||
|
||||
abstract class ChainContextWithCleanupHelper extends ChainContext {
|
||||
Map<TestDescription, Data> cleanupHelper = {};
|
||||
|
||||
@@ -82,8 +53,7 @@ class SetCwdToSdkRoot extends Step<Data, Data, ChainContext> {
|
||||
|
||||
Future<Result<Data>> run(Data input, ChainContext context) async {
|
||||
// stacktrace_helper assumes CWD is the sdk root dir.
|
||||
var outerDir = getD8File().parent.parent.parent.parent;
|
||||
Directory.current = outerDir;
|
||||
Directory.current = sdkRoot;
|
||||
return pass(input);
|
||||
}
|
||||
}
|
||||
@@ -94,55 +64,10 @@ class StepWithD8 extends Step<Data, Data, ChainContext> {
|
||||
String get name => "step";
|
||||
|
||||
Future<Result<Data>> run(Data data, ChainContext context) async {
|
||||
var outputPath = data.outDir.path;
|
||||
var outputFilename = "js.js";
|
||||
var outputFile = path.join(outputPath, outputFilename);
|
||||
var outWrapperPath = path.join(outputPath, "wrapper.js");
|
||||
var outInspectorPath = path.join(outputPath, "inspector.js");
|
||||
|
||||
SingleMapping sourceMap =
|
||||
parse(new File("${outputFile}.map").readAsStringSync());
|
||||
|
||||
Set<int> mappedToLines = sourceMap.lines
|
||||
.map((entry) => entry.entries.map((entry) => entry.sourceLine).toSet())
|
||||
.fold(new Set<int>(), (prev, e) => prev..addAll(e));
|
||||
|
||||
for (Annotation annotation
|
||||
in data.code.annotations.where((a) => a.text.trim() == "nm")) {
|
||||
if (mappedToLines.contains(annotation.lineNo - 1)) {
|
||||
fail("Was not allowed to have a mapping to line "
|
||||
"${annotation.lineNo}, but did.\n"
|
||||
"Sourcemap looks like this (note 0-indexed):\n"
|
||||
"${sourceMap.debugString}");
|
||||
}
|
||||
}
|
||||
|
||||
List<String> breakpoints = [];
|
||||
// Annotations are 1-based, js breakpoints are 0-based.
|
||||
for (Annotation breakAt
|
||||
in data.code.annotations.where((a) => a.text.trim() == "bl")) {
|
||||
breakpoints.add(getJsBreakpointLine(sourceMap, breakAt.lineNo - 1));
|
||||
}
|
||||
for (Annotation breakAt in data.code.annotations
|
||||
.where((a) => a.text.trim().startsWith("bc:"))) {
|
||||
breakpoints.add(getJsBreakpointLineAndColumn(
|
||||
sourceMap, breakAt.lineNo - 1, breakAt.columnNo - 1));
|
||||
}
|
||||
|
||||
String inspectorPath = new File.fromUri(Platform.script).parent.path +
|
||||
Platform.pathSeparator +
|
||||
"jsHelpers" +
|
||||
Platform.pathSeparator +
|
||||
"inspector.js";
|
||||
new File(inspectorPath).copySync(outInspectorPath);
|
||||
String debugAction = "Debugger.stepInto";
|
||||
if (data.code.annotations
|
||||
.any((a) => a.text.trim() == "Debugger:stepOver")) {
|
||||
debugAction = "Debugger.stepOver";
|
||||
}
|
||||
|
||||
ProcessResult runResult =
|
||||
runD8(outInspectorPath, outWrapperPath, debugAction, breakpoints);
|
||||
var outWrapperPathRelative =
|
||||
path.relative(path.join(data.outDir.path, "wrapper.js"));
|
||||
ProcessResult runResult = runD8AndStep(
|
||||
data.outDir.path, data.code, ['--module', outWrapperPathRelative]);
|
||||
data.d8Output = runResult.stdout.split("\n");
|
||||
return pass(data);
|
||||
}
|
||||
@@ -156,360 +81,13 @@ class CheckSteps extends Step<Data, Data, ChainContext> {
|
||||
String get name => "check";
|
||||
|
||||
Future<Result<Data>> run(Data data, ChainContext context) async {
|
||||
var outputPath = data.outDir.path;
|
||||
var outputFilename = "js.js";
|
||||
var outputFile = path.join(outputPath, outputFilename);
|
||||
|
||||
SingleMapping sourceMap =
|
||||
parse(new File("${outputFile}.map").readAsStringSync());
|
||||
|
||||
List<List<DartStackTraceDataEntry>> result =
|
||||
extractStackTraces(data.d8Output, sourceMap, outputFilename);
|
||||
|
||||
List<DartStackTraceDataEntry> trace =
|
||||
result.map((entry) => entry.first).toList();
|
||||
if (debug) debugPrint(trace, outputPath);
|
||||
|
||||
List<String> recordStops = trace
|
||||
.where((entry) => !entry.isError)
|
||||
.map((entry) => "$entry")
|
||||
.toList();
|
||||
|
||||
Set<int> recordStopLines = trace
|
||||
.where((entry) => !entry.isError)
|
||||
.map((entry) => entry.line)
|
||||
.toSet();
|
||||
Set<String> recordStopLineColumns = trace
|
||||
.where((entry) => !entry.isError)
|
||||
.map((entry) => "${entry.line}:${entry.column}")
|
||||
.toSet();
|
||||
|
||||
List<String> expectedStops = [];
|
||||
for (Annotation annotation in data.code.annotations.where((annotation) =>
|
||||
annotation.text.trim().startsWith("s:") ||
|
||||
annotation.text.trim().startsWith("sl:") ||
|
||||
annotation.text.trim().startsWith("bc:"))) {
|
||||
String text = annotation.text.trim();
|
||||
int stopNum = int.parse(text.substring(text.indexOf(":") + 1));
|
||||
if (expectedStops.length < stopNum) expectedStops.length = stopNum;
|
||||
if (text.startsWith("sl:")) {
|
||||
expectedStops[stopNum - 1] = "test.dart:${annotation.lineNo}:";
|
||||
} else {
|
||||
expectedStops[stopNum - 1] =
|
||||
"test.dart:${annotation.lineNo}:${annotation.columnNo}:";
|
||||
}
|
||||
}
|
||||
|
||||
List<List<String>> noBreaksStart = [];
|
||||
List<List<String>> noBreaksEnd = [];
|
||||
for (Annotation annotation in data.code.annotations
|
||||
.where((annotation) => annotation.text.trim().startsWith("nbb:"))) {
|
||||
String text = annotation.text.trim();
|
||||
var split = text.split(":");
|
||||
int stopNum1 = int.parse(split[1]);
|
||||
int stopNum2 = int.parse(split[2]);
|
||||
if (noBreaksStart.length <= stopNum1) noBreaksStart.length = stopNum1 + 1;
|
||||
noBreaksStart[stopNum1] ??= [];
|
||||
if (noBreaksEnd.length <= stopNum2) noBreaksEnd.length = stopNum2 + 1;
|
||||
noBreaksEnd[stopNum2] ??= [];
|
||||
|
||||
noBreaksStart[stopNum1].add("test.dart:${annotation.lineNo}:");
|
||||
noBreaksEnd[stopNum2].add("test.dart:${annotation.lineNo}:");
|
||||
}
|
||||
|
||||
checkRecordedStops(recordStops, expectedStops, noBreaksStart, noBreaksEnd);
|
||||
|
||||
for (Annotation annotation in data.code.annotations
|
||||
.where((annotation) => annotation.text.trim() == "nb")) {
|
||||
// Check that we didn't break where we're not allowed to.
|
||||
if (recordStopLines.contains(annotation.lineNo)) {
|
||||
fail("Was not allowed to stop on line ${annotation.lineNo}, but did!");
|
||||
}
|
||||
}
|
||||
for (Annotation annotation in data.code.annotations
|
||||
.where((annotation) => annotation.text.trim() == "nbc")) {
|
||||
// Check that we didn't break where we're not allowed to.
|
||||
if (recordStopLineColumns
|
||||
.contains("${annotation.lineNo}:${annotation.columnNo}")) {
|
||||
fail(
|
||||
"Was not allowed to stop on line ${annotation.lineNo} column ${annotation.columnNo}, but did!");
|
||||
}
|
||||
}
|
||||
|
||||
if (data.code.annotations.any((a) => a.text.trim() == "fail")) {
|
||||
fail("Test contains 'fail' annotation.");
|
||||
}
|
||||
|
||||
checkD8Steps(data.outDir.path, data.d8Output, data.code, debug: debug);
|
||||
return pass(data);
|
||||
}
|
||||
|
||||
void debugPrint(List<DartStackTraceDataEntry> trace, String outputPath) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
var jsFile =
|
||||
new File(path.join(outputPath, "js.js")).readAsStringSync().split("\n");
|
||||
var dartFile = new File(path.join(outputPath, "test.dart"))
|
||||
.readAsStringSync()
|
||||
.split("\n");
|
||||
|
||||
List<String> getSnippet(List<String> data, int line, int column) {
|
||||
List<String> result = new List<String>.filled(5, "");
|
||||
if (line < 0 || column < 0) return result;
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
int j = line - 2 + i;
|
||||
if (j < 0 || j >= data.length) continue;
|
||||
result[i] = data[j];
|
||||
}
|
||||
if (result[2].length >= column) {
|
||||
result[2] = result[2].substring(0, column) +
|
||||
"/*STOP*/" +
|
||||
result[2].substring(column);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<String> sideBySide(List<String> a, List<String> b, int columns) {
|
||||
List<String> result = new List<String>(a.length);
|
||||
for (int i = 0; i < a.length; ++i) {
|
||||
String left = a[i].padRight(columns).substring(0, columns);
|
||||
String right = b[i].padRight(columns).substring(0, columns);
|
||||
result[i] = left + " | " + right;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int i = 0; i < trace.length; ++i) {
|
||||
sb.write("\n\nStop #${i + 1}\n\n");
|
||||
if (trace[i].isError && trace[i].jsLine < 0) {
|
||||
sb.write("${trace[i].errorString}\n");
|
||||
continue;
|
||||
}
|
||||
var jsSnippet = getSnippet(jsFile, trace[i].jsLine, trace[i].jsColumn);
|
||||
var dartSnippet =
|
||||
getSnippet(dartFile, trace[i].line - 1, trace[i].column - 1);
|
||||
var view = sideBySide(jsSnippet, dartSnippet, 50);
|
||||
sb.writeAll(view, "\n");
|
||||
}
|
||||
|
||||
print(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
class PointMapping {
|
||||
final int fromLine;
|
||||
final int fromColumn;
|
||||
final int toLine;
|
||||
final int toColumn;
|
||||
|
||||
PointMapping(this.fromLine, this.fromColumn, this.toLine, this.toColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Input and output is expected to be 0-based.
|
||||
*
|
||||
* The "magic 4" below is taken from https://github.com/ChromeDevTools/devtools-
|
||||
* frontend/blob/fa18d70a995f06cb73365b2e5b8ae974cf60bd3a/front_end/sources/
|
||||
* JavaScriptSourceFrame.js#L1520-L1523
|
||||
*/
|
||||
String getJsBreakpointLine(SingleMapping sourceMap, int breakOnLine) {
|
||||
List<PointMapping> mappingsOnLines = [];
|
||||
for (var line in sourceMap.lines) {
|
||||
for (var entry in line.entries) {
|
||||
if (entry.sourceLine >= breakOnLine &&
|
||||
entry.sourceLine < breakOnLine + 4) {
|
||||
mappingsOnLines.add(new PointMapping(
|
||||
entry.sourceLine, entry.sourceColumn, line.line, entry.column));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mappingsOnLines.isEmpty) return null;
|
||||
|
||||
mappingsOnLines.sort((a, b) {
|
||||
if (a.fromLine != b.fromLine) return a.fromLine - b.fromLine;
|
||||
if (a.fromColumn != b.fromColumn) return a.fromColumn - b.fromColumn;
|
||||
if (a.toLine != b.toLine) return a.toLine - b.toLine;
|
||||
return a.toColumn - b.toColumn;
|
||||
});
|
||||
PointMapping first = mappingsOnLines.first;
|
||||
mappingsOnLines.retainWhere((p) => p.toLine >= first.toLine);
|
||||
|
||||
PointMapping last = mappingsOnLines.last;
|
||||
return "${first.toLine}:${first.toColumn}:${last.toLine}:${first.toColumn}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Input and output is expected to be 0-based.
|
||||
*/
|
||||
String getJsBreakpointLineAndColumn(
|
||||
SingleMapping sourceMap, int breakOnLine, int breakOnColumn) {
|
||||
for (var line in sourceMap.lines) {
|
||||
for (var entry in line.entries) {
|
||||
if (entry.sourceLine == breakOnLine &&
|
||||
entry.sourceColumn == breakOnColumn)
|
||||
return "${line.line}:${entry.column}";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ProcessResult runD8(String outInspectorPath, String outWrapperPath,
|
||||
String debugAction, List<String> breakpoints) {
|
||||
var outInspectorPathRelative = path.relative(outInspectorPath);
|
||||
var outWrapperPathRelative = path.relative(outWrapperPath);
|
||||
ProcessResult runResult = Process.runSync(
|
||||
d8Executable,
|
||||
[
|
||||
'--enable-inspector',
|
||||
outInspectorPathRelative,
|
||||
'--module',
|
||||
outWrapperPathRelative,
|
||||
"--",
|
||||
debugAction
|
||||
]..addAll(breakpoints.where((s) => s != null)));
|
||||
if (runResult.exitCode != 0) {
|
||||
print(runResult.stderr);
|
||||
print(runResult.stdout);
|
||||
throw "Exit code: ${runResult.exitCode} from d8";
|
||||
}
|
||||
return runResult;
|
||||
}
|
||||
|
||||
List<List<DartStackTraceDataEntry>> extractStackTraces(
|
||||
lines, SingleMapping sourceMap, String outputFilename) {
|
||||
List<List<DartStackTraceDataEntry>> result = [];
|
||||
bool inStackTrace = false;
|
||||
List<String> currentStackTrace = <String>[];
|
||||
for (var line in lines) {
|
||||
if (line.trim() == "--- Debugger stacktrace start ---") {
|
||||
inStackTrace = true;
|
||||
} else if (line.trim() == "--- Debugger stacktrace end ---") {
|
||||
result
|
||||
.add(extractStackTrace(currentStackTrace, sourceMap, outputFilename));
|
||||
currentStackTrace.clear();
|
||||
inStackTrace = false;
|
||||
} else if (inStackTrace) {
|
||||
currentStackTrace.add(line.trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<DartStackTraceDataEntry> extractStackTrace(
|
||||
List<String> js, SingleMapping sourceMap, String wantedFile) {
|
||||
List<DartStackTraceDataEntry> result = [];
|
||||
for (String line in js) {
|
||||
if (!line.contains("$wantedFile:")) {
|
||||
result
|
||||
.add(new DartStackTraceDataEntry.error("Not correct file @ '$line'"));
|
||||
continue;
|
||||
}
|
||||
Iterable<Match> ms = new RegExp(r"(\d+):(\d+)").allMatches(line);
|
||||
if (ms.isEmpty) {
|
||||
result.add(new DartStackTraceDataEntry.error(
|
||||
"Line and column not found for '$line'"));
|
||||
continue;
|
||||
}
|
||||
Match m = ms.first;
|
||||
int l = int.parse(m.group(1));
|
||||
int c = int.parse(m.group(2));
|
||||
SourceMapSpan span = getColumnOrPredecessor(sourceMap, l, c);
|
||||
if (span?.start == null) {
|
||||
result.add(new DartStackTraceDataEntry.errorWithJsPosition(
|
||||
"Source map not found for '$line'", l, c));
|
||||
continue;
|
||||
}
|
||||
var file = span.sourceUrl?.pathSegments?.last ?? "(unknown file)";
|
||||
result.add(new DartStackTraceDataEntry(
|
||||
file, span.start.line + 1, span.start.column + 1, l, c));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SourceMapSpan getColumnOrPredecessor(
|
||||
SingleMapping sourceMap, int line, int column) {
|
||||
SourceMapSpan span = sourceMap.spanFor(line, column);
|
||||
if (span == null && line > 0) {
|
||||
span = sourceMap.spanFor(line - 1, 999999);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
void checkRecordedStops(List<String> recordStops, List<String> expectedStops,
|
||||
List<List<String>> noBreaksStart, List<List<String>> noBreaksEnd) {
|
||||
// We want to find all expected lines in recorded lines in order, but allow
|
||||
// more in between in the recorded lines.
|
||||
// noBreaksStart and noBreaksStart gives instructions on what's *NOT* allowed
|
||||
// to be between those points though.
|
||||
|
||||
int expectedIndex = 0;
|
||||
Set<String> aliveNoBreaks = new Set<String>();
|
||||
if (noBreaksStart.length > 0 && noBreaksStart[0] != null) {
|
||||
aliveNoBreaks.addAll(noBreaksStart[0]);
|
||||
}
|
||||
int stopNumber = 0;
|
||||
for (String recorded in recordStops) {
|
||||
stopNumber++;
|
||||
if (expectedIndex == expectedStops.length) break;
|
||||
if ("$recorded:".contains(expectedStops[expectedIndex])) {
|
||||
++expectedIndex;
|
||||
if (noBreaksStart.length > expectedIndex &&
|
||||
noBreaksStart[expectedIndex] != null) {
|
||||
aliveNoBreaks.addAll(noBreaksStart[expectedIndex]);
|
||||
}
|
||||
if (noBreaksEnd.length > expectedIndex &&
|
||||
noBreaksEnd[expectedIndex] != null) {
|
||||
aliveNoBreaks.removeAll(noBreaksEnd[expectedIndex]);
|
||||
}
|
||||
} else if (aliveNoBreaks
|
||||
.contains("${(recorded.split(":")..removeLast()).join(":")}:")) {
|
||||
fail("Break '$recorded' was found when it wasn't allowed "
|
||||
"(js step $stopNumber, after stop $expectedIndex)");
|
||||
}
|
||||
}
|
||||
if (expectedIndex != expectedStops.length) {
|
||||
// Didn't find everything.
|
||||
fail("Expected to find $expectedStops but found $recordStops");
|
||||
}
|
||||
}
|
||||
|
||||
File _cachedD8File;
|
||||
File getD8File() {
|
||||
File attemptFileFromDir(Directory dir) {
|
||||
if (Platform.isWindows) {
|
||||
return new File(dir.path + Platform.pathSeparator + "d8/windows/d8.exe");
|
||||
} else if (Platform.isLinux) {
|
||||
return new File(dir.path + Platform.pathSeparator + "d8/linux/d8");
|
||||
} else if (Platform.isMacOS) {
|
||||
return new File(dir.path + Platform.pathSeparator + "d8/macos/d8");
|
||||
}
|
||||
throw new UnsupportedError('Unsupported platform.');
|
||||
}
|
||||
|
||||
File search() {
|
||||
Directory dir = new File.fromUri(Platform.script).parent;
|
||||
while (dir.path.length > 1) {
|
||||
for (var entry in dir.listSync()) {
|
||||
if (entry is! Directory) continue;
|
||||
if (entry.path.endsWith("third_party")) {
|
||||
List<String> segments = entry.uri.pathSegments;
|
||||
if (segments[segments.length - 2] == "third_party") {
|
||||
File possibleD8 = attemptFileFromDir(entry);
|
||||
if (possibleD8.existsSync()) return possibleD8;
|
||||
}
|
||||
}
|
||||
}
|
||||
dir = dir.parent;
|
||||
}
|
||||
|
||||
throw "Cannot find D8 directory.";
|
||||
}
|
||||
|
||||
return _cachedD8File ??= search();
|
||||
}
|
||||
|
||||
File findInOutDir(String relative) {
|
||||
var outerDir = getD8File().parent.parent.parent.parent.path;
|
||||
var outerDir = sdkRoot.path;
|
||||
for (var outDir in const ["out/ReleaseX64", "xcodebuild/ReleaseX64"]) {
|
||||
var tryPath = path.join(outerDir, outDir, relative);
|
||||
File file = new File(tryPath);
|
||||
@@ -518,10 +96,6 @@ File findInOutDir(String relative) {
|
||||
throw "Couldn't find $relative. Try building more targets.";
|
||||
}
|
||||
|
||||
String get d8Executable {
|
||||
return getD8File().path;
|
||||
}
|
||||
|
||||
String get dartExecutable {
|
||||
return Platform.resolvedExecutable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Testing of source maps
|
||||
|
||||
Currently this package consists of two "frameworks":
|
||||
1) stacktrace_helper.dart
|
||||
2) stepping_helper.dart
|
||||
|
||||
It is intended to be a shared resource between DDC and dart2js.
|
||||
|
||||
## Stacktraces ("stacktrace_helper.dart")
|
||||
|
||||
TODO
|
||||
|
||||
## Debugging tests (step tests, "stepping_helper.dart")
|
||||
|
||||
This is supposed to work in a few steps:
|
||||
1) Create the JS
|
||||
2) Run the JS with D8, setting instructed breakpoints etc.
|
||||
3) Translating JS positions to Dart positions
|
||||
4) Validating the stepped positions.
|
||||
|
||||
In the above, the helper assumes
|
||||
1) The dart file is called "test.dart" and is placed in the output directory before compiling to
|
||||
"js.js" in the output dir.
|
||||
|
||||
And then performs via
|
||||
2) `runD8AndStep`
|
||||
3) `checkD8Steps`
|
||||
4) (done in above step)
|
||||
|
||||
The test files themselves contain information about where to stop, which breakpoints to expect etc.
|
||||
They contain this information in comments inlined in the code as in `/*key*/` where `key` can be one
|
||||
of the below.
|
||||
|
||||
_Not context sensitive_
|
||||
|
||||
These comments can be anywhere in the file and their position does not matter.
|
||||
|
||||
* **fail**: Will fail the test. Useful for debugging in conjunction with debug being set to true
|
||||
(see below).
|
||||
* **Debugger:stepOver**: Will step over breakpoints. Default (i.e. without this) is to step into.
|
||||
|
||||
_Context sensitive_
|
||||
|
||||
These comments should be placed at the wanted position: The line and possibly column position of the
|
||||
comment matters. They refer to the next non-whitespace position in the source.
|
||||
|
||||
* **bl** (break line): insert a breakpoint on this line. This does not add any new expected breaks.
|
||||
* **s:{i}** (stop): adds an expected stop as the `i`th stop (1-indexed).
|
||||
* **sl:{i}** (stop at line): adds an expected stop as the `i`th stop (1-indexed). Only check the
|
||||
line number.
|
||||
* **nb** (no break): The debugger should never break on this line.
|
||||
* **nbc** (no break column): The debugger should never break on this line and column.
|
||||
* **nbb:{i}:{j}** (no break between): The debugger should not break on this line between expectation
|
||||
`i` and `j` (1-indexed). Note that `from` can also be the special value `0` meaning from the very
|
||||
first stop. For example `nbb:0:1` means not before first expected stop.
|
||||
* **nm** (no mapping): There's not allowed to be any mapping to this line.
|
||||
* **bc:{i}** (break column): inserts a breakpoint at this line and column and adds an expected stop
|
||||
as the `i`th stop (1-indexed).
|
||||
|
||||
Note that in an ideal world `bc:{i}` would not be unnecessary: Stopping at a line and stepping
|
||||
should generally be enough. Because of the current behavior of d8 though, for instance
|
||||
```
|
||||
baz(foo(), bar())
|
||||
```
|
||||
will stop at `baz`, go into `foo`, stop at `bar`, go into `bar` and stop at `baz`.
|
||||
From a Dart perspective we would instead expect it to stop at `foo`, go into `foo`, stop at `bar`,
|
||||
go into `bar` and stop a `baz`.
|
||||
Having **bc:{i}** allows us to force this behavior as d8 can actually stop at `foo` too.
|
||||
|
||||
All of these annotations are removed before compiling to js and the expected output thus refers to
|
||||
the unannotated code.
|
||||
|
||||
When the test confirms that the debugger stopped at the expected locations it allows for additional
|
||||
breakpoints before, between and after the expected breakpoints.
|
||||
|
||||
### Debugging a test
|
||||
|
||||
By calling `checkD8Steps` with the `debug` parameter set to `true` one can get information like this
|
||||
dumped to standard out:
|
||||
|
||||
```
|
||||
Stop #1
|
||||
|
||||
test.main = function() { | main() {
|
||||
try { | try {
|
||||
let value = /*STOP*/"world"; | var value = /*STOP*/"world";
|
||||
dart.throw(dart.str`Hello, ${value}`); | // Comment
|
||||
} catch (e) { | throw "Hello, $value";
|
||||
|
||||
Stop #2
|
||||
|
||||
try { | var value = "world";
|
||||
let value = "world"; | // Comment
|
||||
/*STOP*/dart.throw(dart.str`Hello, ${value}`); | /*STOP*/throw "Hello, $value";
|
||||
} catch (e) { | }
|
||||
let st = dart.stackTrace(e); | // Comment
|
||||
|
||||
Stop #3
|
||||
|
||||
dart.throw(dart.str`Hello, ${value}`); | }
|
||||
} catch (e) { | // Comment
|
||||
let st = /*STOP*/dart.stackTrace(e); | catch (e, /*STOP*/st) {
|
||||
{ | print(e);
|
||||
core.print(e); | print(st);
|
||||
|
||||
[...]
|
||||
```
|
||||
|
||||
This can for instance be useful in combination with `/*fail*/` when adding new tests to see all the
|
||||
places where the debugger stopped.
|
||||
|
||||
### Technical details
|
||||
|
||||
Some of the logic comes from https://github.com/ChromeDevTools/devtools-frontend/, for instance see
|
||||
https://github.com/ChromeDevTools/devtools-frontend/blob/fa18d70a995f06cb73365b2e5b8ae974cf60bd3a/
|
||||
front_end/sources/JavaScriptSourceFrame.js#L1520-L1523
|
||||
for how a line breakpoint is resolved:
|
||||
Basically the line asked to break on in user code (e.g. in dart code) is asked for first and last
|
||||
javascript positions; these are then used to get possible breakpoints in that part. If there are
|
||||
none it tries the next line (etc for a number of lines). Once it finds something (in javascript
|
||||
positions) it converts that to user code position (e.g. in dart code), normalizes it by converting
|
||||
to javascript position and back to user code position again, then converts to javascript position
|
||||
and sets the breakpoint.
|
||||
This is to some extend mimicked here when setting a line break (though not a "column break").
|
||||
@@ -0,0 +1,448 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:expect/minitest.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:source_maps/source_maps.dart';
|
||||
|
||||
import 'annotated_code_helper.dart';
|
||||
|
||||
/**
|
||||
* Runs D8 and steps as the AnnotatedCode dictates.
|
||||
*
|
||||
* Note that the compiled javascript is expected to be called "js.js" inside the
|
||||
* outputPath directory. It is also expected that there is a "js.js.map" file.
|
||||
* It is furthermore expected that the js has been compiled from a file in the
|
||||
* same folder called test.dart.
|
||||
*/
|
||||
ProcessResult runD8AndStep(
|
||||
String outputPath, AnnotatedCode code, List<String> scriptD8Command) {
|
||||
var outputFile = path.join(outputPath, "js.js");
|
||||
SingleMapping sourceMap =
|
||||
parse(new File("${outputFile}.map").readAsStringSync());
|
||||
|
||||
Set<int> mappedToLines = sourceMap.lines
|
||||
.map((entry) => entry.entries.map((entry) => entry.sourceLine).toSet())
|
||||
.fold(new Set<int>(), (prev, e) => prev..addAll(e));
|
||||
|
||||
for (Annotation annotation
|
||||
in code.annotations.where((a) => a.text.trim() == "nm")) {
|
||||
if (mappedToLines.contains(annotation.lineNo - 1)) {
|
||||
fail("Was not allowed to have a mapping to line "
|
||||
"${annotation.lineNo}, but did.\n"
|
||||
"Sourcemap looks like this (note 0-indexed):\n"
|
||||
"${sourceMap.debugString}");
|
||||
}
|
||||
}
|
||||
|
||||
List<String> breakpoints = [];
|
||||
// Annotations are 1-based, js breakpoints are 0-based.
|
||||
for (Annotation breakAt
|
||||
in code.annotations.where((a) => a.text.trim() == "bl")) {
|
||||
breakpoints.add(_getJsBreakpointLine(sourceMap, breakAt.lineNo - 1));
|
||||
}
|
||||
for (Annotation breakAt
|
||||
in code.annotations.where((a) => a.text.trim().startsWith("bc:"))) {
|
||||
breakpoints.add(_getJsBreakpointLineAndColumn(
|
||||
sourceMap, breakAt.lineNo - 1, breakAt.columnNo - 1));
|
||||
}
|
||||
|
||||
File inspectorFile = new File.fromUri(
|
||||
sdkRoot.uri.resolve("pkg/sourcemap_testing/lib/src/js/inspector.js"));
|
||||
if (!inspectorFile.existsSync()) throw "Couldn't find 'inspector.js'";
|
||||
var outInspectorPath = path.join(outputPath, "inspector.js");
|
||||
inspectorFile.copySync(outInspectorPath);
|
||||
String debugAction = "Debugger.stepInto";
|
||||
if (code.annotations.any((a) => a.text.trim() == "Debugger:stepOver")) {
|
||||
debugAction = "Debugger.stepOver";
|
||||
}
|
||||
return _runD8(outInspectorPath, scriptD8Command, debugAction, breakpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the D8 js steps and checks against expectations.
|
||||
*
|
||||
* Note that the compiled javascript is expected to be called "js.js" inside the
|
||||
* outputPath directory. It is also expected that there is a "js.js.map" file.
|
||||
* It is furthermore expected that the js has been compiled from a file in the
|
||||
* same folder called test.dart.
|
||||
*/
|
||||
void checkD8Steps(String outputPath, List<String> d8Output, AnnotatedCode code,
|
||||
{bool debug: false}) {
|
||||
var outputFilename = "js.js";
|
||||
var outputFile = path.join(outputPath, outputFilename);
|
||||
SingleMapping sourceMap =
|
||||
parse(new File("${outputFile}.map").readAsStringSync());
|
||||
|
||||
List<List<_DartStackTraceDataEntry>> result =
|
||||
_extractStackTraces(d8Output, sourceMap, outputFilename);
|
||||
|
||||
List<_DartStackTraceDataEntry> trace =
|
||||
result.map((entry) => entry.first).toList();
|
||||
if (debug) _debugPrint(trace, outputPath);
|
||||
|
||||
List<String> recordStops =
|
||||
trace.where((entry) => !entry.isError).map((entry) => "$entry").toList();
|
||||
|
||||
Set<int> recordStopLines =
|
||||
trace.where((entry) => !entry.isError).map((entry) => entry.line).toSet();
|
||||
Set<String> recordStopLineColumns = trace
|
||||
.where((entry) => !entry.isError)
|
||||
.map((entry) => "${entry.line}:${entry.column}")
|
||||
.toSet();
|
||||
|
||||
List<String> expectedStops = [];
|
||||
for (Annotation annotation in code.annotations.where((annotation) =>
|
||||
annotation.text.trim().startsWith("s:") ||
|
||||
annotation.text.trim().startsWith("sl:") ||
|
||||
annotation.text.trim().startsWith("bc:"))) {
|
||||
String text = annotation.text.trim();
|
||||
int stopNum = int.parse(text.substring(text.indexOf(":") + 1));
|
||||
if (expectedStops.length < stopNum) expectedStops.length = stopNum;
|
||||
if (text.startsWith("sl:")) {
|
||||
expectedStops[stopNum - 1] = "test.dart:${annotation.lineNo}:";
|
||||
} else {
|
||||
expectedStops[stopNum - 1] =
|
||||
"test.dart:${annotation.lineNo}:${annotation.columnNo}:";
|
||||
}
|
||||
}
|
||||
|
||||
List<List<String>> noBreaksStart = [];
|
||||
List<List<String>> noBreaksEnd = [];
|
||||
for (Annotation annotation in code.annotations
|
||||
.where((annotation) => annotation.text.trim().startsWith("nbb:"))) {
|
||||
String text = annotation.text.trim();
|
||||
var split = text.split(":");
|
||||
int stopNum1 = int.parse(split[1]);
|
||||
int stopNum2 = int.parse(split[2]);
|
||||
if (noBreaksStart.length <= stopNum1) noBreaksStart.length = stopNum1 + 1;
|
||||
noBreaksStart[stopNum1] ??= [];
|
||||
if (noBreaksEnd.length <= stopNum2) noBreaksEnd.length = stopNum2 + 1;
|
||||
noBreaksEnd[stopNum2] ??= [];
|
||||
|
||||
noBreaksStart[stopNum1].add("test.dart:${annotation.lineNo}:");
|
||||
noBreaksEnd[stopNum2].add("test.dart:${annotation.lineNo}:");
|
||||
}
|
||||
|
||||
_checkRecordedStops(recordStops, expectedStops, noBreaksStart, noBreaksEnd);
|
||||
|
||||
for (Annotation annotation in code.annotations
|
||||
.where((annotation) => annotation.text.trim() == "nb")) {
|
||||
// Check that we didn't break where we're not allowed to.
|
||||
if (recordStopLines.contains(annotation.lineNo)) {
|
||||
fail("Was not allowed to stop on line ${annotation.lineNo}, but did!");
|
||||
}
|
||||
}
|
||||
for (Annotation annotation in code.annotations
|
||||
.where((annotation) => annotation.text.trim() == "nbc")) {
|
||||
// Check that we didn't break where we're not allowed to.
|
||||
if (recordStopLineColumns
|
||||
.contains("${annotation.lineNo}:${annotation.columnNo}")) {
|
||||
fail("Was not allowed to stop on line ${annotation.lineNo} "
|
||||
"column ${annotation.columnNo}, but did!");
|
||||
}
|
||||
}
|
||||
|
||||
if (code.annotations.any((a) => a.text.trim() == "fail")) {
|
||||
fail("Test contains 'fail' annotation.");
|
||||
}
|
||||
}
|
||||
|
||||
void _checkRecordedStops(List<String> recordStops, List<String> expectedStops,
|
||||
List<List<String>> noBreaksStart, List<List<String>> noBreaksEnd) {
|
||||
// We want to find all expected lines in recorded lines in order, but allow
|
||||
// more in between in the recorded lines.
|
||||
// noBreaksStart and noBreaksStart gives instructions on what's *NOT* allowed
|
||||
// to be between those points though.
|
||||
|
||||
int expectedIndex = 0;
|
||||
Set<String> aliveNoBreaks = new Set<String>();
|
||||
if (noBreaksStart.length > 0 && noBreaksStart[0] != null) {
|
||||
aliveNoBreaks.addAll(noBreaksStart[0]);
|
||||
}
|
||||
int stopNumber = 0;
|
||||
for (String recorded in recordStops) {
|
||||
stopNumber++;
|
||||
if (expectedIndex == expectedStops.length) break;
|
||||
if ("$recorded:".contains(expectedStops[expectedIndex])) {
|
||||
++expectedIndex;
|
||||
if (noBreaksStart.length > expectedIndex &&
|
||||
noBreaksStart[expectedIndex] != null) {
|
||||
aliveNoBreaks.addAll(noBreaksStart[expectedIndex]);
|
||||
}
|
||||
if (noBreaksEnd.length > expectedIndex &&
|
||||
noBreaksEnd[expectedIndex] != null) {
|
||||
aliveNoBreaks.removeAll(noBreaksEnd[expectedIndex]);
|
||||
}
|
||||
} else if (aliveNoBreaks
|
||||
.contains("${(recorded.split(":")..removeLast()).join(":")}:")) {
|
||||
fail("Break '$recorded' was found when it wasn't allowed "
|
||||
"(js step $stopNumber, after stop $expectedIndex)");
|
||||
}
|
||||
}
|
||||
if (expectedIndex != expectedStops.length) {
|
||||
// Didn't find everything.
|
||||
fail("Expected to find $expectedStops but found $recordStops");
|
||||
}
|
||||
}
|
||||
|
||||
void _debugPrint(List<_DartStackTraceDataEntry> trace, String outputPath) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
var jsFile =
|
||||
new File(path.join(outputPath, "js.js")).readAsStringSync().split("\n");
|
||||
var dartFile = new File(path.join(outputPath, "test.dart"))
|
||||
.readAsStringSync()
|
||||
.split("\n");
|
||||
|
||||
List<String> getSnippet(List<String> data, int line, int column) {
|
||||
List<String> result = new List<String>.filled(5, "");
|
||||
if (line < 0 || column < 0) return result;
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
int j = line - 2 + i;
|
||||
if (j < 0 || j >= data.length) continue;
|
||||
result[i] = data[j];
|
||||
}
|
||||
if (result[2].length >= column) {
|
||||
result[2] = result[2].substring(0, column) +
|
||||
"/*STOP*/" +
|
||||
result[2].substring(column);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<String> sideBySide(List<String> a, List<String> b, int columns) {
|
||||
List<String> result = new List<String>(a.length);
|
||||
for (int i = 0; i < a.length; ++i) {
|
||||
String left = a[i].padRight(columns).substring(0, columns);
|
||||
String right = b[i].padRight(columns).substring(0, columns);
|
||||
result[i] = left + " | " + right;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int i = 0; i < trace.length; ++i) {
|
||||
sb.write("\n\nStop #${i + 1}\n\n");
|
||||
if (trace[i].isError && trace[i].jsLine < 0) {
|
||||
sb.write("${trace[i].errorString}\n");
|
||||
continue;
|
||||
}
|
||||
var jsSnippet = getSnippet(jsFile, trace[i].jsLine, trace[i].jsColumn);
|
||||
var dartSnippet =
|
||||
getSnippet(dartFile, trace[i].line - 1, trace[i].column - 1);
|
||||
var view = sideBySide(jsSnippet, dartSnippet, 50);
|
||||
sb.writeAll(view, "\n");
|
||||
}
|
||||
|
||||
print(sb.toString());
|
||||
}
|
||||
|
||||
List<List<_DartStackTraceDataEntry>> _extractStackTraces(
|
||||
lines, SingleMapping sourceMap, String outputFilename) {
|
||||
List<List<_DartStackTraceDataEntry>> result = [];
|
||||
bool inStackTrace = false;
|
||||
List<String> currentStackTrace = <String>[];
|
||||
for (var line in lines) {
|
||||
if (line.trim() == "--- Debugger stacktrace start ---") {
|
||||
inStackTrace = true;
|
||||
} else if (line.trim() == "--- Debugger stacktrace end ---") {
|
||||
result.add(
|
||||
_extractStackTrace(currentStackTrace, sourceMap, outputFilename));
|
||||
currentStackTrace.clear();
|
||||
inStackTrace = false;
|
||||
} else if (inStackTrace) {
|
||||
currentStackTrace.add(line.trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<_DartStackTraceDataEntry> _extractStackTrace(
|
||||
List<String> js, SingleMapping sourceMap, String wantedFile) {
|
||||
List<_DartStackTraceDataEntry> result = [];
|
||||
for (String line in js) {
|
||||
if (!line.contains("$wantedFile:")) {
|
||||
result.add(
|
||||
new _DartStackTraceDataEntry.error("Not correct file @ '$line'"));
|
||||
continue;
|
||||
}
|
||||
Iterable<Match> ms = new RegExp(r"(\d+):(\d+)").allMatches(line);
|
||||
if (ms.isEmpty) {
|
||||
result.add(new _DartStackTraceDataEntry.error(
|
||||
"Line and column not found for '$line'"));
|
||||
continue;
|
||||
}
|
||||
Match m = ms.first;
|
||||
int l = int.parse(m.group(1));
|
||||
int c = int.parse(m.group(2));
|
||||
SourceMapSpan span = _getColumnOrPredecessor(sourceMap, l, c);
|
||||
if (span?.start == null) {
|
||||
result.add(new _DartStackTraceDataEntry.errorWithJsPosition(
|
||||
"Source map not found for '$line'", l, c));
|
||||
continue;
|
||||
}
|
||||
var file = span.sourceUrl?.pathSegments?.last ?? "(unknown file)";
|
||||
result.add(new _DartStackTraceDataEntry(
|
||||
file, span.start.line + 1, span.start.column + 1, l, c));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SourceMapSpan _getColumnOrPredecessor(
|
||||
SingleMapping sourceMap, int line, int column) {
|
||||
SourceMapSpan span = sourceMap.spanFor(line, column);
|
||||
if (span == null && line > 0) {
|
||||
span = sourceMap.spanFor(line - 1, 999999);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
class _DartStackTraceDataEntry {
|
||||
final String file;
|
||||
final int line;
|
||||
final int column;
|
||||
final errorString;
|
||||
final int jsLine;
|
||||
final int jsColumn;
|
||||
|
||||
_DartStackTraceDataEntry(
|
||||
this.file, this.line, this.column, this.jsLine, this.jsColumn)
|
||||
: errorString = null;
|
||||
_DartStackTraceDataEntry.error(this.errorString)
|
||||
: file = null,
|
||||
line = -1,
|
||||
column = -1,
|
||||
jsLine = -1,
|
||||
jsColumn = -1;
|
||||
_DartStackTraceDataEntry.errorWithJsPosition(
|
||||
this.errorString, this.jsLine, this.jsColumn)
|
||||
: file = null,
|
||||
line = -1,
|
||||
column = -1;
|
||||
|
||||
get isError => errorString != null;
|
||||
|
||||
String toString() => isError ? errorString : "$file:$line:$column";
|
||||
}
|
||||
|
||||
class _PointMapping {
|
||||
final int fromLine;
|
||||
final int fromColumn;
|
||||
final int toLine;
|
||||
final int toColumn;
|
||||
|
||||
_PointMapping(this.fromLine, this.fromColumn, this.toLine, this.toColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Input and output is expected to be 0-based.
|
||||
*
|
||||
* The "magic 4" below is taken from https://github.com/ChromeDevTools/devtools-
|
||||
* frontend/blob/fa18d70a995f06cb73365b2e5b8ae974cf60bd3a/front_end/sources/
|
||||
* JavaScriptSourceFrame.js#L1520-L1523
|
||||
*/
|
||||
String _getJsBreakpointLine(SingleMapping sourceMap, int breakOnLine) {
|
||||
List<_PointMapping> mappingsOnLines = [];
|
||||
for (var line in sourceMap.lines) {
|
||||
for (var entry in line.entries) {
|
||||
if (entry.sourceLine >= breakOnLine &&
|
||||
entry.sourceLine < breakOnLine + 4) {
|
||||
mappingsOnLines.add(new _PointMapping(
|
||||
entry.sourceLine, entry.sourceColumn, line.line, entry.column));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mappingsOnLines.isEmpty) return null;
|
||||
|
||||
mappingsOnLines.sort((a, b) {
|
||||
if (a.fromLine != b.fromLine) return a.fromLine - b.fromLine;
|
||||
if (a.fromColumn != b.fromColumn) return a.fromColumn - b.fromColumn;
|
||||
if (a.toLine != b.toLine) return a.toLine - b.toLine;
|
||||
return a.toColumn - b.toColumn;
|
||||
});
|
||||
_PointMapping first = mappingsOnLines.first;
|
||||
mappingsOnLines.retainWhere((p) => p.toLine >= first.toLine);
|
||||
|
||||
_PointMapping last = mappingsOnLines.last;
|
||||
return "${first.toLine}:${first.toColumn}:${last.toLine}:${first.toColumn}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Input and output is expected to be 0-based.
|
||||
*/
|
||||
String _getJsBreakpointLineAndColumn(
|
||||
SingleMapping sourceMap, int breakOnLine, int breakOnColumn) {
|
||||
for (var line in sourceMap.lines) {
|
||||
for (var entry in line.entries) {
|
||||
if (entry.sourceLine == breakOnLine &&
|
||||
entry.sourceColumn == breakOnColumn)
|
||||
return "${line.line}:${entry.column}";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
ProcessResult _runD8(String outInspectorPath, List<String> scriptD8Command,
|
||||
String debugAction, List<String> breakpoints) {
|
||||
var outInspectorPathRelative = path.relative(outInspectorPath);
|
||||
ProcessResult runResult = Process.runSync(
|
||||
d8Executable,
|
||||
['--enable-inspector', outInspectorPathRelative]
|
||||
..addAll(scriptD8Command)
|
||||
..addAll(["--", debugAction])
|
||||
..addAll(breakpoints.where((s) => s != null)));
|
||||
if (runResult.exitCode != 0) {
|
||||
print(runResult.stderr);
|
||||
print(runResult.stdout);
|
||||
throw "Exit code: ${runResult.exitCode} from d8";
|
||||
}
|
||||
return runResult;
|
||||
}
|
||||
|
||||
File _cachedD8File;
|
||||
Directory _cachedSdkRoot;
|
||||
File getD8File() {
|
||||
File attemptFileFromDir(Directory dir) {
|
||||
if (Platform.isWindows) {
|
||||
return new File(dir.path + Platform.pathSeparator + "d8/windows/d8.exe");
|
||||
} else if (Platform.isLinux) {
|
||||
return new File(dir.path + Platform.pathSeparator + "d8/linux/d8");
|
||||
} else if (Platform.isMacOS) {
|
||||
return new File(dir.path + Platform.pathSeparator + "d8/macos/d8");
|
||||
}
|
||||
throw new UnsupportedError('Unsupported platform.');
|
||||
}
|
||||
|
||||
File search() {
|
||||
Directory dir = new File.fromUri(Platform.script).parent;
|
||||
while (dir.path.length > 1) {
|
||||
for (var entry in dir.listSync()) {
|
||||
if (entry is! Directory) continue;
|
||||
if (entry.path.endsWith("third_party")) {
|
||||
List<String> segments = entry.uri.pathSegments;
|
||||
if (segments[segments.length - 2] == "third_party") {
|
||||
File possibleD8 = attemptFileFromDir(entry);
|
||||
if (possibleD8.existsSync()) {
|
||||
_cachedSdkRoot = dir;
|
||||
return possibleD8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dir = dir.parent;
|
||||
}
|
||||
|
||||
throw "Cannot find D8 directory.";
|
||||
}
|
||||
|
||||
return _cachedD8File ??= search();
|
||||
}
|
||||
|
||||
Directory get sdkRoot {
|
||||
getD8File();
|
||||
return _cachedSdkRoot;
|
||||
}
|
||||
|
||||
String get d8Executable {
|
||||
return getD8File().path;
|
||||
}
|
||||
Reference in New Issue
Block a user