// Copyright (c) 2017, 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. /// VMOptions=--dwarf-stack-traces --save-debugging-info=dwarf.so import 'dart:convert'; import 'dart:io'; import 'package:native_stack_traces/native_stack_traces.dart'; import 'package:path/path.dart' as path; import 'package:expect/expect.dart'; @pragma("vm:prefer-inline") bar() { // Keep the 'throw' and its argument on separate lines. throw // force linebreak with dartfmt "Hello, Dwarf!"; } @pragma("vm:never-inline") foo() { bar(); } Future main() async { String rawStack = ""; try { foo(); } catch (e, st) { rawStack = st.toString(); } if (path.basenameWithoutExtension(Platform.executable) != "dart_precompiled_runtime") { return; // Not running from an AOT compiled snapshot. } if (Platform.isAndroid) { return; // Generated dwarf.so not available on the test device. } final dwarf = Dwarf.fromFile("dwarf.so"); await checkStackTrace(rawStack, dwarf, expectedCallsInfo); } Future checkStackTrace(String rawStack, Dwarf dwarf, List> expectedCallsInfo) async { print(""); print("Raw stack trace:"); print(rawStack); final rawLines = await Stream.value(rawStack).transform(const LineSplitter()).toList(); final pcOffsets = collectPCOffsets(rawLines).toList(); // We should have at least enough PC addresses to cover the frames we'll be // checking. Expect.isTrue(pcOffsets.length >= expectedCallsInfo.length); final virtualAddresses = pcOffsets.map((o) => dwarf.virtualAddressOf(o)).toList(); // Some double-checks using other information in the non-symbolic stack trace. final dsoBase = dsoBaseAddresses(rawLines).single; final absolutes = absoluteAddresses(rawLines); final relocatedAddresses = absolutes.map((a) => a - dsoBase); final explicits = explicitVirtualAddresses(rawLines); // Explicits will be empty if not generating ELF snapshots directly, which // means we can't depend on virtual addresses in the snapshot lining up with // those in the separate debugging information. if (explicits.isNotEmpty) { // Direct-to-ELF snapshots should have a build ID. Expect.isNotNull(dwarf.buildId); Expect.deepEquals(relocatedAddresses, virtualAddresses); Expect.deepEquals(explicits, virtualAddresses); } final gotCallsInfo = >[]; for (final addr in virtualAddresses) { final externalCallInfo = dwarf.callInfoFor(addr); Expect.isNotNull(externalCallInfo); final allCallInfo = dwarf.callInfoFor(addr, includeInternalFrames: true); Expect.isNotNull(allCallInfo); for (final call in allCallInfo) { Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}"); } Expect.deepEquals(externalCallInfo, allCallInfo); gotCallsInfo.add(allCallInfo.cast().toList()); } print(""); print("Call information for PC addresses:"); for (var i = 0; i < virtualAddresses.length; i++) { print("For PC 0x${virtualAddresses[i].toRadixString(16)}:"); print(" Calls corresponding to user or library code:"); gotCallsInfo[i].forEach((frame) => print(" ${frame}")); } checkFrames(gotCallsInfo, expectedCallsInfo); final gotSymbolizedLines = await Stream.fromIterable(rawLines) .transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: true)) .toList(); final gotSymbolizedCalls = gotSymbolizedLines.where((s) => s.startsWith('#')).toList(); print(""); print("Symbolized stack trace:"); gotSymbolizedLines.forEach(print); print(""); print("Extracted calls:"); gotSymbolizedCalls.forEach(print); final expectedStrings = extractCallStrings(expectedCallsInfo); // There are two strings in the list for each line in the output. final expectedCallCount = expectedStrings.length ~/ 2; Expect.isTrue(gotSymbolizedCalls.length >= expectedCallCount); // Strip off any unexpected lines, so we can also make sure we didn't get // unexpected calls prior to those calls we expect. final gotCallsTrace = gotSymbolizedCalls.sublist(0, expectedCallCount).join('\n'); Expect.stringContainsInOrder(gotCallsTrace, expectedStrings); } final expectedCallsInfo = >[ // The first frame should correspond to the throw in bar, which was inlined // into foo (so we'll get information for two calls for that PC address). [ DartCallInfo( function: "bar", filename: "dwarf_stack_trace_test.dart", line: 17, column: 3, inlined: true), DartCallInfo( function: "foo", filename: "dwarf_stack_trace_test.dart", line: 23, column: 3, inlined: false) ], // The second frame corresponds to call to foo in main. [ DartCallInfo( function: "main", filename: "dwarf_stack_trace_test.dart", line: 29, column: 5, inlined: false) ], // Don't assume anything about any of the frames below the call to foo // in main, as this makes the test too brittle. ]; void checkFrames( List> gotInfo, List> expectedInfo) { // There may be frames below those we check. Expect.isTrue(gotInfo.length >= expectedInfo.length); // We can't just use deep equality, since we only have the filenames in the // expected version, not the whole path, and we don't really care if // non-positive line numbers match, as long as they're both non-positive. for (var i = 0; i < expectedInfo.length; i++) { for (var j = 0; j < expectedInfo[i].length; j++) { final got = gotInfo[i][j]; final expected = expectedInfo[i][j]; Expect.equals(expected.function, got.function); Expect.equals(expected.inlined, got.inlined); Expect.equals(expected.filename, path.basename(got.filename)); if (expected.isInternal) { Expect.isTrue(got.isInternal); } else { Expect.equals(expected.line, got.line); } } } } List extractCallStrings(List> expectedCalls) { var ret = []; for (final frame in expectedCalls) { for (final call in frame) { if (call is DartCallInfo) { ret.add(call.function); if (call.isInternal) { ret.add("${call.filename}:??"); } else { ret.add("${call.filename}:${call.line}"); } } } } return ret; } Iterable parseUsingAddressRegExp(RegExp re, Iterable lines) sync* { for (final line in lines) { final match = re.firstMatch(line); if (match == null) continue; final s = match.group(1); if (s == null) continue; yield int.parse(s, radix: 16); } } final _absRE = RegExp(r'abs ([a-f\d]+)'); Iterable absoluteAddresses(Iterable lines) => parseUsingAddressRegExp(_absRE, lines); final _virtRE = RegExp(r'virt ([a-f\d]+)'); Iterable explicitVirtualAddresses(Iterable lines) => parseUsingAddressRegExp(_virtRE, lines); final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)'); Iterable dsoBaseAddresses(Iterable lines) => parseUsingAddressRegExp(_dsoBaseRE, lines);