| // 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=$TEST_COMPILATION_DIR/dwarf.so | 
 |  | 
 | import 'dart:convert'; | 
 | import 'dart:io'; | 
 |  | 
 | import 'package:expect/config.dart'; | 
 | import 'package:expect/expect.dart'; | 
 | import 'package:native_stack_traces/native_stack_traces.dart'; | 
 | import 'package:path/path.dart' as path; | 
 |  | 
 | @pragma("vm:prefer-inline") | 
 | bar() { | 
 |   // Keep the 'throw' and its argument on separate lines. | 
 |   throw // force linebreak with dart format | 
 |   "Hello, Dwarf!"; | 
 | } | 
 |  | 
 | @pragma("vm:never-inline") | 
 | foo() { | 
 |   bar(); | 
 | } | 
 |  | 
 | Future<void> main() async { | 
 |   String rawStack = ""; | 
 |   try { | 
 |     foo(); | 
 |   } catch (e, st) { | 
 |     rawStack = st.toString(); | 
 |   } | 
 |  | 
 |   if (!isVmAotConfiguration) { | 
 |     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( | 
 |     path.join(Platform.environment["TEST_COMPILATION_DIR"]!, "dwarf.so"), | 
 |   )!; | 
 |  | 
 |   await checkStackTrace(rawStack, dwarf, expectedCallsInfo); | 
 | } | 
 |  | 
 | Future<void> checkStackTrace( | 
 |   String rawStack, | 
 |   Dwarf dwarf, | 
 |   List<List<DartCallInfo>> expectedCallsInfo, | 
 | ) async { | 
 |   print(""); | 
 |   print("Raw stack trace:"); | 
 |   print(rawStack); | 
 |  | 
 |   final rawLines = await Stream.value( | 
 |     rawStack, | 
 |   ).transform(const LineSplitter()).toList(); | 
 |  | 
 |   final pcOffsets = collectPCOffsets(rawLines).toList(); | 
 |   Expect.isNotEmpty(pcOffsets); | 
 |  | 
 |   print('PCOffsets:'); | 
 |   for (final offset in pcOffsets) { | 
 |     print('* $offset'); | 
 |   } | 
 |   print(''); | 
 |  | 
 |   // We should have at least enough PC addresses to cover the frames we'll be | 
 |   // checking. | 
 |   Expect.isTrue(pcOffsets.length >= expectedCallsInfo.length); | 
 |  | 
 |   final isolateStart = dwarf.isolateStartAddress(pcOffsets.first.architecture); | 
 |   Expect.isNotNull(isolateStart); | 
 |   print('Isolate start offset: 0x${isolateStart!.toRadixString(16)}'); | 
 |  | 
 |   // The addresses of the stack frames in the separate DWARF debugging info. | 
 |   final virtualAddresses = pcOffsets | 
 |       .map((o) => dwarf.virtualAddressOf(o)) | 
 |       .toList(); | 
 |  | 
 |   print('Virtual addresses from PCOffsets:'); | 
 |   for (final address in virtualAddresses) { | 
 |     print('* 0x${address.toRadixString(16)}'); | 
 |   } | 
 |   print(''); | 
 |  | 
 |   // Some double-checks using other information in the non-symbolic stack trace. | 
 |   final dsoBase = dsoBaseAddresses(rawLines).single; | 
 |   print('DSO base address: 0x${dsoBase.toRadixString(16)}'); | 
 |  | 
 |   final absoluteIsolateStart = isolateStartAddresses(rawLines).single; | 
 |   print( | 
 |     'Absolute isolate start address: ' | 
 |     '0x${absoluteIsolateStart.toRadixString(16)}', | 
 |   ); | 
 |  | 
 |   final absolutes = absoluteAddresses(rawLines); | 
 |   // The relocated addresses of the stack frames in the loaded DSO. These is | 
 |   // only guaranteed to be the same as virtualAddresses if the built-in ELF | 
 |   // generator was used to create the snapshot. | 
 |   final relocatedAddresses = absolutes.map((a) => a - dsoBase); | 
 |  | 
 |   print('Relocated absolute addresses:'); | 
 |   for (final address in relocatedAddresses) { | 
 |     print('* 0x${address.toRadixString(16)}'); | 
 |   } | 
 |   print(''); | 
 |  | 
 |   // 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. | 
 |   final explicits = explicitVirtualAddresses(rawLines); | 
 |   if (explicits.isNotEmpty) { | 
 |     print('Explicit virtual addresses:'); | 
 |     for (final address in explicits) { | 
 |       print('* 0x${address.toRadixString(16)}'); | 
 |     } | 
 |     print(''); | 
 |     // Direct-to-ELF snapshots should have a build ID. | 
 |     Expect.isNotNull(dwarf.buildId); | 
 |     Expect.deepEquals(explicits, virtualAddresses); | 
 |  | 
 |     // This is an ELF snapshot, so check that these two are the same. | 
 |     Expect.deepEquals(virtualAddresses, relocatedAddresses); | 
 |   } | 
 |  | 
 |   final gotCallsInfo = <List<DartCallInfo>>[]; | 
 |  | 
 |   for (final offset in pcOffsets) { | 
 |     final externalCallInfo = dwarf.callInfoForPCOffset(offset); | 
 |     Expect.isNotNull(externalCallInfo); | 
 |     final allCallInfo = dwarf.callInfoForPCOffset( | 
 |       offset, | 
 |       includeInternalFrames: true, | 
 |     ); | 
 |     Expect.isNotNull(allCallInfo); | 
 |     for (final call in externalCallInfo!) { | 
 |       Expect.isTrue(call is DartCallInfo, "got non-Dart call info ${call}"); | 
 |       Expect.isFalse(call.isInternal); | 
 |       Expect.isTrue( | 
 |         allCallInfo!.contains(call), | 
 |         "External call info ${call} is not among all calls", | 
 |       ); | 
 |     } | 
 |     for (final call in allCallInfo!) { | 
 |       if (!call.isInternal) { | 
 |         Expect.isTrue( | 
 |           externalCallInfo.contains(call), | 
 |           "External call info ${call} is not among external calls", | 
 |         ); | 
 |       } | 
 |     } | 
 |     gotCallsInfo.add(externalCallInfo.cast<DartCallInfo>().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}")); | 
 |   } | 
 |  | 
 |   // Remove empty entries which correspond to skipped internal frames. | 
 |   gotCallsInfo.removeWhere((calls) => calls.isEmpty); | 
 |  | 
 |   checkFrames(gotCallsInfo, expectedCallsInfo); | 
 |  | 
 |   final gotSymbolizedLines = await Stream.fromIterable(rawLines) | 
 |       .transform(DwarfStackTraceDecoder(dwarf, includeInternalFrames: false)) | 
 |       .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.containsInOrder(expectedStrings, gotCallsTrace); | 
 | } | 
 |  | 
 | final expectedCallsInfo = <List<DartCallInfo>>[ | 
 |   // 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<List<DartCallInfo>> gotInfo, | 
 |   List<List<DartCallInfo>> 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<String> extractCallStrings(List<List<CallInfo>> expectedCalls) { | 
 |   var ret = <String>[]; | 
 |   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<int> parseUsingAddressRegExp(RegExp re, Iterable<String> 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<int> absoluteAddresses(Iterable<String> lines) => | 
 |     parseUsingAddressRegExp(_absRE, lines); | 
 |  | 
 | final _virtRE = RegExp(r'virt ([a-f\d]+)'); | 
 |  | 
 | Iterable<int> explicitVirtualAddresses(Iterable<String> lines) => | 
 |     parseUsingAddressRegExp(_virtRE, lines); | 
 |  | 
 | final _dsoBaseRE = RegExp(r'isolate_dso_base: ([a-f\d]+)'); | 
 |  | 
 | Iterable<int> dsoBaseAddresses(Iterable<String> lines) => | 
 |     parseUsingAddressRegExp(_dsoBaseRE, lines); | 
 |  | 
 | final _isolateStartRE = RegExp(r'isolate_instructions: ([a-f\d]+)'); | 
 |  | 
 | Iterable<int> isolateStartAddresses(Iterable<String> lines) => | 
 |     parseUsingAddressRegExp(_isolateStartRE, lines); |