blob: a673278670a067dae49dce63dd00a5d20153cc24 [file] [log] [blame]
// Copyright (c) 2023, 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:async';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:expect/expect.dart';
import 'package:native_stack_traces/native_stack_traces.dart';
//
// Test framework
//
class _ParsedFrame {
const _ParsedFrame();
static _ParsedFrame parse(String frame) {
if (frame == '<asynchronous suspension>') {
return const _AsynchronousGap();
} else {
return _DartFrame.parse(frame);
}
}
}
class _DartFrame extends _ParsedFrame {
final int no;
final String symbol;
final String location;
final int? lineNo;
_DartFrame({
required this.no,
required this.symbol,
required this.location,
required this.lineNo,
});
static final _pattern = RegExp(
r'^#(?<no>\d+)\s+(?<symbol>[^(]+)(\((?<location>((\w+://)?[/\w]+:)?[^:]+)(:(?<line>\d+)(:(?<column>\d+))?)?\))?$');
static _DartFrame parse(String frame) {
final match = _pattern.firstMatch(frame);
if (match == null) {
throw 'Failed to parse: $frame';
}
final no = int.parse(match.namedGroup('no')!);
final symbol = match.namedGroup('symbol')!.trim();
var location = match.namedGroup('location')!;
if (location.endsWith('_test.dart')) {
location = '%test%';
}
final lineNo =
location.endsWith('utils.dart') || location.endsWith('tests.dart')
? match.namedGroup('line')
: null;
return _DartFrame(
no: no,
symbol: symbol,
location: location.split('/').last,
lineNo: lineNo != null ? int.parse(lineNo) : null,
);
}
@override
String toString() =>
'#$no $symbol ($location${lineNo != null ? ':$lineNo' : ''})';
@override
bool operator ==(Object other) {
if (other is! _DartFrame) {
return false;
}
return no == other.no &&
symbol == other.symbol &&
location == other.location &&
lineNo == other.lineNo;
}
}
class _AsynchronousGap extends _ParsedFrame {
const _AsynchronousGap();
@override
String toString() => '<asynchronous suspension>';
}
final _lineRE = RegExp(r'^(?:#(?<number>\d+)|<asynchronous suspension>)');
Future<List<_ParsedFrame>> _parseStack(String text) async {
if (text.contains('*** *** ***')) {
// Looks like DWARF stack traces mode.
text = await Stream.fromIterable(text.split('\n'))
.transform(DwarfStackTraceDecoder(_dwarf!))
.where(_lineRE.hasMatch)
.join('\n');
}
return text
.split('\n')
.map((l) => l.trim())
.where((l) => l.isNotEmpty)
.map(_ParsedFrame.parse)
.toList();
}
const _updatingExpectations = bool.fromEnvironment('update.expectations');
final _updatedExpectations = <String>[];
late final List<String> _currentExpectations;
var _testIndex = 0;
late final Dwarf? _dwarf;
void configure(List<String> currentExpectations,
{String debugInfoFilename = 'debug.so'}) {
try {
final testCompilationDir = Platform.environment['TEST_COMPILATION_DIR'];
if (testCompilationDir != null) {
debugInfoFilename = path.join(testCompilationDir, debugInfoFilename);
}
_dwarf = Dwarf.fromFile(debugInfoFilename)!;
} on FileSystemException {
// We're not running in precompiled mode, so the file doesn't exist and
// we can continue normally.
}
_currentExpectations = currentExpectations;
}
Future<void> runTest(Future<void> Function() body) async {
try {
await body();
} catch (e, st) {
await checkExpectedStack(st);
}
}
Future<void> checkExpectedStack(StackTrace st) async {
final expectedFramesString = _testIndex < _currentExpectations.length
? _currentExpectations[_testIndex]
: '';
final stackTraceString = st.toString();
final gotFrames = await _parseStack(stackTraceString);
final normalizedStack = gotFrames.join('\n');
if (_updatingExpectations) {
_updatedExpectations.add(normalizedStack);
} else {
if (normalizedStack != expectedFramesString) {
final expectedFrames = await _parseStack(expectedFramesString);
final isDwarfMode = stackTraceString.contains('*** *** ***');
print('''
STACK TRACE MISMATCH -----------------
GOT:
$normalizedStack
EXPECTED:
$expectedFramesString
--------------------------------------
To regenate expectations run:
\$ ${Platform.executable} -Dupdate.expectations=true ${Platform.script}
--------------------------------------
''');
if (isDwarfMode) {
print('''
--------------------------------------
RAW STACK:
$st
--------------------------------------
''');
}
Expect.equals(
expectedFrames.length, gotFrames.length, 'wrong number of frames');
for (var i = 0; i < expectedFrames.length; i++) {
final expectedFrame = expectedFrames[i];
final gotFrame = gotFrames[i];
if (expectedFrame == gotFrame) {
continue;
}
if (expectedFrame is _DartFrame && gotFrame is _DartFrame) {
Expect.equals(expectedFrame.symbol, gotFrame.symbol,
'at frame #$i mismatched function name');
Expect.equals(expectedFrame.location, gotFrame.location,
'at frame #$i mismatched location');
Expect.equals(expectedFrame.lineNo, gotFrame.lineNo,
'at frame #$i mismatched line location');
}
Expect.equals(expectedFrame, gotFrame);
}
}
}
_testIndex++;
}
void updateExpectations([String? expectationsFile]) {
if (!_updatingExpectations) {
return;
}
final sourceFilePath = expectationsFile != null
? path.join(path.dirname(Platform.script.toFilePath()), expectationsFile)
: Platform.script.toFilePath();
final sourceFile = File(sourceFilePath);
final source = sourceFile.readAsStringSync();
final expectationsStart = source.lastIndexOf('// CURRENT EXPECTATIONS BEGIN');
final updatedExpectationsString =
[for (var s in _updatedExpectations) '"""\n$s"""'].join(",\n");
final newSource = source.substring(0, expectationsStart) +
"""
// CURRENT EXPECTATIONS BEGIN
final currentExpectations = [${updatedExpectationsString}];
// CURRENT EXPECTATIONS END
""";
sourceFile.writeAsStringSync(newSource);
print('updated expectations in ${sourceFile}!');
}
// Check if we are running with obfuscation but without DWARF stack traces
// then we don't have a way to deobfuscate the stack trace.
bool shouldSkip() {
final stack = StackTrace.current.toString();
final isObfuscateMode = !stack.contains('shouldSkip');
final isDwarfStackTracesMode = stack.contains('*** ***');
// We should skip the test if we are running without DWARF stack
// traces enabled but with obfuscation.
return !isDwarfStackTracesMode && isObfuscateMode;
}