// 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: '; } 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]+)');