[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:
Jens Johansen
2017-11-30 11:10:51 +00:00
committed by commit-bot@chromium.org
parent b0160af36a
commit 03090df3fa
5 changed files with 586 additions and 494 deletions
+6 -60
View File
@@ -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").
+8 -434
View File
@@ -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;
}
+124
View File
@@ -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;
}