blob: 71c48ee8137726f19de6763f07ac422d9474bcd1 [file] [log] [blame]
// Copyright (c) 2024, 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:js_interop';
import 'dart:typed_data';
import 'dart:convert';
import 'package:source_maps/parser.dart';
void f() {
g();
}
void g() {
throw 'hi';
}
runtimeFalse() => int.parse('1') == 0;
// `expectedFrames` is (file, line, column, name) of the frames we check.
//
// Information we don't check are "null": we don't want to check line/column
// of standard library functions to avoid breaking the test with unrelated
// changes to the standard library.
void testMain(
String testName,
List<(String?, int?, int?, String?)?> expectedFrames,
) {
// Use `f` and `g` in a few places to make sure wasm-opt won't inline them
// in the test.
final fTearOff = f;
final gTearOff = g;
if (runtimeFalse()) f();
if (runtimeFalse()) g();
// Get some simple stack trace.
String? stackTraceString;
try {
f();
} catch (e, st) {
stackTraceString = st.toString();
}
// Print the stack trace to make it easy to update the test.
print("-----");
print(stackTraceString);
print("-----");
final actualFrames = parseStack(
testName,
getSourceMapping(testName),
stackTraceString!,
);
print('Got stack trace:');
for (final frame in actualFrames) {
print(' $frame');
}
print('Matching against:');
for (final frame in expectedFrames) {
print(' $frame');
}
if (actualFrames.length < expectedFrames.length) {
throw 'Less actual frames than expected';
}
for (int i = 0; i < expectedFrames.length; i++) {
final expected = expectedFrames[i];
final actual = actualFrames[i];
if (expected == null) continue;
if (actual == null) {
throw 'Mismatch:\n Expected: $expected\n Actual: <no mapping>';
}
if ((expected.$1 != null && actual.$1 != expected.$1) ||
(expected.$2 != null && actual.$2 != expected.$2) ||
(expected.$3 != null && actual.$3 != expected.$3) ||
(expected.$4 != null && actual.$4 != expected.$4)) {
throw 'Mismatch:\n Expected: $expected\n Actual: $actual';
}
}
}
String getFilename(String testName, int moduleId) {
final compilationDir = const String.fromEnvironment("TEST_COMPILATION_DIR");
if (moduleId == 0) {
return '$compilationDir/${testName}_test.wasm.map';
} else {
return '$compilationDir/${testName}_test_module$moduleId.wasm.map';
}
}
Mapping getSourceMapping(String testName) {
final allMappings = MappingBundle();
// Read source map of the current program.
final compilationDir = const String.fromEnvironment("TEST_COMPILATION_DIR");
final mainFilename = getFilename(testName, 0);
final mainSourceMapFile = readfile(mainFilename);
final mainSourceMap = parse(utf8.decode(mainSourceMapFile)) as SingleMapping;
mainSourceMap.targetUrl = mainFilename;
allMappings.addMapping(mainSourceMap);
int i = 1;
while (true) {
// All the modules will have consecutive names. Keep reading them until we
// fail to find one.
try {
final filename = getFilename(testName, i);
final fileContents = readfile(filename);
final sourceMap = parse(utf8.decode(fileContents)) as SingleMapping;
sourceMap.targetUrl = filename;
allMappings.addMapping(sourceMap);
i++;
} catch (e) {
break;
}
}
return allMappings;
}
List<(String?, int?, int?, String?)?> parseStack(
String testName,
Mapping mapping,
String stackTraceString,
) {
final parsed = <(String?, int?, int?, String?)?>[];
for (final line in stackTraceString.split('\n')) {
if (line.contains('.mjs') || line.contains('.js')) {
parsed.add(null);
continue;
}
final hexOffsetMatch = stackTraceHexOffsetRegExp.firstMatch(line);
if (hexOffsetMatch == null) {
throw 'Unable to parse hex offset in frame "$line"';
}
final hexOffsetStr = hexOffsetMatch.group(1)!; // includes '0x'
final offset = int.tryParse(hexOffsetStr);
if (offset == null) {
throw 'Unable to parse hex number in frame "$line"';
}
final moduleIdMatch = stackTraceModuleNameRegExp.firstMatch(line);
if (moduleIdMatch == null) {
throw 'Unable to parse module name in frame "$line"';
}
final moduleId = int.parse(moduleIdMatch.group(1)!);
final uri = getFilename(testName, moduleId);
final span = mapping.spanFor(0, offset, uri: uri);
if (span == null) {
print('Stack frame "$line" does not have a source mapping');
parsed.add(null);
continue;
}
final filename = span.sourceUrl!.pathSegments.last;
final lineNumber = span.start.line;
final columnNumber = span.start.column;
final symbolName = span.text;
parsed.add((filename, 1 + lineNumber, 1 + columnNumber, symbolName));
}
return parsed;
}
/// Read the file at the given [path].
///
/// This relies on the `readbuffer` function provided by d8.
@JS()
external JSArrayBuffer readbuffer(JSString path);
/// Read the file at the given [path].
Uint8List readfile(String path) => Uint8List.view(readbuffer(path.toJS).toDart);
final stackTraceHexOffsetRegExp = RegExp(r'wasm-function.*(0x[0-9a-fA-F]+)\)$');
final stackTraceModuleNameRegExp = RegExp(r'wasm/module([0-9]+)');