| // 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. |
| |
| // Helper methods and definitions used in the use_dwarf_stack_traces_flag tests. |
| |
| import "dart:async"; |
| import "dart:convert"; |
| import "dart:io"; |
| |
| import 'package:native_stack_traces/native_stack_traces.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| |
| import 'use_flag_test_helper.dart'; |
| |
| /// Returns false if tests involving assembly snapshots should be run |
| /// and a String describing why the tests should be skipped otherwise. |
| Object get skipAssembly { |
| // Currently there are no appropriate buildtools on the simulator trybots as |
| // normally they compile to ELF and don't need them for compiling assembly |
| // snapshots. |
| if (isSimulator) { |
| return "running on a simulated architecture"; |
| } |
| return (Platform.isLinux || Platform.isMacOS) |
| ? false |
| : "no process for assembling snapshots on this platform"; |
| } |
| |
| /// Returns false if tests involving MacOS universal binaries should be run |
| /// and a String describing why the tests should be skipped otherwise. |
| Object get skipUniversalBinary { |
| final assemblySkipped = skipAssembly; |
| if (assemblySkipped != false) return assemblySkipped; |
| return Platform.isMacOS ? false : "only valid for MacOS"; |
| } |
| |
| typedef Trace = List<String>; |
| |
| class DwarfTestOutput { |
| final Trace trace; |
| final int allocateObjectStart; |
| final int allocateObjectEnd; |
| |
| DwarfTestOutput(this.trace, this.allocateObjectStart, this.allocateObjectEnd); |
| } |
| |
| abstract class State { |
| final DwarfTestOutput output; |
| final DwarfTestOutput outputWithOppositeFlag; |
| |
| State(this.output, this.outputWithOppositeFlag); |
| } |
| |
| class NonDwarfState extends State { |
| NonDwarfState(super.output, super.outputWithOppositeFlag); |
| |
| void check() => expect(outputWithOppositeFlag.trace, equals(output.trace)); |
| } |
| |
| abstract class DwarfState<T> extends State { |
| final T snapshot; |
| final T debugInfo; |
| DwarfState( |
| super.output, |
| super.outputWithOppositeFlag, |
| this.snapshot, |
| this.debugInfo, |
| ); |
| |
| String get description; |
| |
| Future<void> check(Trace trace, T t); |
| |
| Future<void> makeTests(Trace nonDwarfTrace) async { |
| test( |
| 'Testing $description traces with separate debugging info', |
| () async => await check(nonDwarfTrace, debugInfo), |
| ); |
| |
| test( |
| 'Testing $description traces with original snapshot', |
| () async => await check(nonDwarfTrace, snapshot), |
| ); |
| } |
| } |
| |
| abstract class ElfState<T> extends DwarfState<T> { |
| ElfState( |
| super.output, |
| super.outputWithOppositeFlag, |
| super.snapshot, |
| super.debugInfo, |
| ); |
| |
| @override |
| String get description => 'ELF'; |
| } |
| |
| abstract class MultiArchDwarfState<T> extends DwarfState<T> { |
| final T? singleArch; |
| final T? multiArch; |
| |
| MultiArchDwarfState( |
| super.output, |
| super.outputWithOppositeFlag, |
| super.snapshot, |
| super.debugInfo, [ |
| this.singleArch, |
| this.multiArch, |
| ]); |
| |
| @override |
| Future<void> makeTests(Trace nonDwarfTrace) async { |
| await super.makeTests(nonDwarfTrace); |
| |
| test( |
| 'Testing $description single-architecture universal binary', |
| () async { |
| expect(singleArch, isNotNull); |
| await check(nonDwarfTrace, singleArch!); |
| }, |
| skip: skipUniversalBinary, |
| ); |
| |
| test( |
| 'Testing $description multi-architecture universal binary', |
| () async { |
| expect(multiArch, isNotNull); |
| await check(nonDwarfTrace, multiArch!); |
| }, |
| skip: skipUniversalBinary, |
| ); |
| } |
| } |
| |
| abstract class AssemblyState<T> extends MultiArchDwarfState<T> { |
| AssemblyState( |
| super.output, |
| super.outputWithOppositeFlag, |
| super.snapshot, |
| super.debugInfo, [ |
| super.singleArch, |
| super.multiArch, |
| ]); |
| |
| @override |
| String get description => 'assembly'; |
| } |
| |
| abstract class MachOState<T> extends MultiArchDwarfState<T> { |
| MachOState( |
| super.output, |
| super.outputWithOppositeFlag, |
| super.snapshot, |
| super.debugInfo, [ |
| super.singleArch, |
| super.multiArch, |
| ]); |
| |
| @override |
| String get description => 'Mach-O'; |
| } |
| |
| Future<void> runTests<T>( |
| String tempPrefix, |
| String scriptPath, |
| Future<NonDwarfState> Function(String, String) runNonDwarf, |
| Iterable<Future<DwarfState?> Function(String, String)> runDwarfs, |
| ) async { |
| if (!isAOTRuntime) { |
| return; // Running in JIT: AOT binaries not available. |
| } |
| |
| if (Platform.isAndroid) { |
| return; // SDK tree and dart_bootstrap not available on the test device. |
| } |
| |
| // These are the tools we need to be available to run on a given platform: |
| if (!await testExecutable(genSnapshot)) { |
| throw "Cannot run test as $genSnapshot not available"; |
| } |
| if (!await testExecutable(dartPrecompiledRuntime)) { |
| throw "Cannot run test as $dartPrecompiledRuntime not available"; |
| } |
| if (!File(platformDill).existsSync()) { |
| throw "Cannot run test as $platformDill does not exist"; |
| } |
| |
| await withTempDir(tempPrefix, (String tempDir) async { |
| // We have to use the program in its original location so it can use |
| // the dart:_internal library (as opposed to adding it as an OtherResources |
| // option to the test). |
| final scriptDill = path.join(tempDir, 'flag_program.dill'); |
| |
| // Compile script to Kernel IR. |
| await run(genKernel, <String>[ |
| '--aot', |
| '--platform=$platformDill', |
| '-o', |
| scriptDill, |
| scriptPath, |
| ]); |
| |
| final nonDwarfState = await runNonDwarf(tempDir, scriptDill); |
| final dwarfStates = [ |
| for (final f in runDwarfs) await f(tempDir, scriptDill), |
| ]; |
| |
| test('Testing symbolic traces', nonDwarfState.check); |
| |
| final nonDwarfTrace = nonDwarfState.output.trace; |
| |
| for (final dwarfState in dwarfStates.whereType<DwarfState>()) { |
| dwarfState.makeTests(nonDwarfTrace); |
| } |
| }); |
| } |
| |
| void checkHeader(StackTraceHeader header) { |
| // These should be all available. |
| expect(header.vmStart, isNotNull); |
| expect(header.isolateStart, isNotNull); |
| expect(header.isolateDsoBase, isNotNull); |
| expect(header.buildId, isNotNull); |
| expect(header.os, isNotNull); |
| expect(header.architecture, isNotNull); |
| expect(header.usingSimulator, isNotNull); |
| expect(header.compressedPointers, isNotNull); |
| } |
| |
| void checkRootUnitAssumptions( |
| DwarfTestOutput output1, |
| DwarfTestOutput output2, |
| Dwarf rootDwarf, { |
| required PCOffset sampleOffset, |
| bool matchingBuildIds = true, |
| }) { |
| // We run the test program on the same host OS as the test, so any |
| // PCOffset from the trace should have this information. |
| expect(sampleOffset.os, isNotNull); |
| expect(sampleOffset.architecture, isNotNull); |
| expect(sampleOffset.usingSimulator, isNotNull); |
| expect(sampleOffset.compressedPointers, isNotNull); |
| |
| expect(sampleOffset.os, equals(Platform.operatingSystem)); |
| final archString = |
| '${sampleOffset.usingSimulator! ? 'SIM' : ''}' |
| '${sampleOffset.architecture!.toUpperCase()}' |
| '${sampleOffset.compressedPointers! ? 'C' : ''}'; |
| final baseBuildDir = path.basename(buildDir); |
| expect(baseBuildDir, endsWith(archString)); |
| |
| // Check that the build IDs exist in the traces and are the same. |
| final buildId1 = buildId(output1.trace); |
| expect(buildId1, isNotEmpty); |
| print('Trace 1 build ID: "${buildId1}"'); |
| final buildId2 = buildId(output2.trace); |
| expect(buildId2, isNotEmpty); |
| print('Trace 2 build ID: "${buildId2}"'); |
| expect(buildId2, equals(buildId1)); |
| |
| if (matchingBuildIds) { |
| // The build ID in the traces should be the same as the DWARF build ID |
| // when the ELF was generated by gen_snapshot. |
| final dwarfBuildId = rootDwarf.buildId(); |
| expect(dwarfBuildId, isNotNull); |
| print('Dwarf build ID: "${dwarfBuildId!}"'); |
| // We should never generate an all-zero build ID. |
| expect(dwarfBuildId, isNot("00000000000000000000000000000000")); |
| // This is a common failure case as well, when HashBitsContainer ends up |
| // hashing over seemingly empty sections. |
| expect(dwarfBuildId, isNot("01000000010000000100000001000000")); |
| expect(buildId1, equals(dwarfBuildId)); |
| expect(buildId2, equals(dwarfBuildId)); |
| } |
| |
| final allocateObjectStart = output1.allocateObjectStart; |
| final allocateObjectEnd = output1.allocateObjectEnd; |
| expect(output2.allocateObjectStart, equals(allocateObjectStart)); |
| expect(output2.allocateObjectEnd, equals(allocateObjectEnd)); |
| |
| checkAllocateObjectOffset(rootDwarf, allocateObjectStart); |
| // The end of the bare instructions payload may be padded up to word size, |
| // so check the maximum possible word size (64 bits) before the end. |
| checkAllocateObjectOffset(rootDwarf, allocateObjectEnd - 8); |
| // The end should be either in a different stub or not a stub altogether. |
| checkAllocateObjectOffset(rootDwarf, allocateObjectEnd, expectedValue: false); |
| // The byte before the start should also be in either a different stub or |
| // not in a stub altogether. |
| checkAllocateObjectOffset( |
| rootDwarf, |
| allocateObjectStart - 1, |
| expectedValue: false, |
| ); |
| // Check the midpoint of the stub, as the stub should be large enough that the |
| // midpoint won't be in any possible padding. |
| expect( |
| allocateObjectEnd - allocateObjectStart, |
| greaterThanOrEqualTo(16), |
| reason: 'midpoint of stub may be in bare payload padding', |
| ); |
| checkAllocateObjectOffset( |
| rootDwarf, |
| (allocateObjectStart + allocateObjectEnd) ~/ 2, |
| ); |
| |
| print("Successfully matched AllocateObject stub addresses"); |
| } |
| |
| void checkAllocateObjectOffset( |
| Dwarf dwarf, |
| int offset, { |
| bool expectedValue = true, |
| }) { |
| final pcOffset = PCOffset(offset, InstructionsSection.isolate); |
| print('Offset of tested stub address is $pcOffset'); |
| final callInfo = dwarf.callInfoForPCOffset( |
| pcOffset, |
| includeInternalFrames: true, |
| ); |
| print('Call info for tested stub address is $callInfo'); |
| final got = |
| callInfo != null && |
| callInfo.length == 1 && |
| callInfo.single is StubCallInfo && |
| (callInfo.single as StubCallInfo).name.endsWith('AllocateObjectStub'); |
| expect( |
| got, |
| equals(expectedValue), |
| reason: |
| 'address is ${expectedValue ? 'not within' : 'within'} ' |
| 'the AllocateObject stub', |
| ); |
| } |
| |
| void checkTranslatedTrace(List<String> nonDwarfTrace, List<String> dwarfTrace) { |
| final translatedStackFrames = onlySymbolicFrameLines(dwarfTrace); |
| final originalStackFrames = onlySymbolicFrameLines(nonDwarfTrace); |
| |
| print('Stack frames from translated non-symbolic stack trace:'); |
| print(translatedStackFrames.join('\n')); |
| |
| print('Stack frames from original symbolic stack trace:'); |
| print(originalStackFrames.join('\n')); |
| |
| expect(translatedStackFrames, isNotEmpty); |
| expect(originalStackFrames, isNotEmpty); |
| |
| // In symbolic mode, we don't store column information to avoid an increase |
| // in size of CodeStackMaps. Thus, we need to strip any columns from the |
| // translated non-symbolic stack to compare them via equality. |
| final columnStrippedTranslated = removeColumns(translatedStackFrames); |
| |
| print('Stack frames from translated non-symbolic stack trace, no columns:'); |
| print(columnStrippedTranslated.join('\n')); |
| |
| expect(columnStrippedTranslated, equals(originalStackFrames)); |
| } |
| |
| Future<DwarfTestOutput> runTestProgram( |
| String executable, |
| List<String> args, |
| ) async { |
| final result = await runHelper(executable, args); |
| |
| if (result.exitCode == 0) { |
| throw 'Command did not fail with non-zero exit code'; |
| } |
| if (result.stdout.isEmpty) { |
| throw 'Command did not print a stacktrace'; |
| } |
| |
| final stdoutLines = LineSplitter.split(result.stdout).toList(); |
| if (result.stdout.length < 2) { |
| throw 'Command did not print both absolute addresses for stub range'; |
| } |
| final start = int.parse(stdoutLines[0]); |
| final end = int.parse(stdoutLines[1]); |
| |
| return DwarfTestOutput( |
| LineSplitter.split(result.stderr).toList(), |
| start, |
| end, |
| ); |
| } |
| |
| final _buildIdRE = RegExp(r"build_id: '([a-f\d]+)'"); |
| String buildId(Iterable<String> lines) { |
| for (final line in lines) { |
| final match = _buildIdRE.firstMatch(line); |
| if (match != null) { |
| return match.group(1)!; |
| } |
| } |
| return ''; |
| } |
| |
| final _symbolicFrameRE = RegExp(r'^#\d+\s+'); |
| |
| Iterable<String> onlySymbolicFrameLines(Iterable<String> lines) { |
| return lines.where((line) => _symbolicFrameRE.hasMatch(line)); |
| } |
| |
| final _columnsRE = RegExp(r'[(](.*:\d+):\d+[)]'); |
| |
| Iterable<String> removeColumns(Iterable<String> lines) sync* { |
| for (final line in lines) { |
| final match = _columnsRE.firstMatch(line); |
| if (match != null) { |
| yield line.replaceRange(match.start, match.end, '(${match.group(1)!})'); |
| } else { |
| yield line; |
| } |
| } |
| } |
| |
| Iterable<int> parseUsingAddressRegExp(RegExp re, Iterable<String> lines) sync* { |
| for (final line in lines) { |
| final match = re.firstMatch(line); |
| if (match != null) { |
| yield int.parse(match.group(1)!, 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); |
| |
| // We only list architectures supported by the current CpuType enum in |
| // pkg:native_stack_traces/src/macho.dart. |
| const machOArchNames = <String, String>{ |
| "ARM": "arm", |
| "ARM64": "arm64", |
| "IA32": "ia32", |
| "X64": "x64", |
| }; |
| |
| String? get dartNameForCurrentArchitecture { |
| for (final entry in machOArchNames.entries) { |
| if (buildDir.endsWith(entry.key)) { |
| return entry.value; |
| } |
| } |
| return null; |
| } |