9297d718a1
This reverts commit b5294b9753.
Reason for revert: many failures on appjit, reload and fuchsia bots
Failure Link: <LINK TO FAILURE>
Original change's description:
> [test_runner] Invoke tests with relative paths.
>
> This makes tests more reproducible, and makes it easier to copy commands between workspaces, or between a failing bot and a local workspace.
>
> Change-Id: Ic8dd10a3540f314a406e5c5b0a23d97032e5d01d
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/508364
> Reviewed-by: Alexander Aprelev <aam@google.com>
> Commit-Queue: Ryan Macnak <rmacnak@google.com>
Change-Id: Id7aa668c0609df65a5f622ae73013a15e7f9200e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511400
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
Reviewed-by: Nate Biggs <natebiggs@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
1626 lines
48 KiB
Dart
1626 lines
48 KiB
Dart
// 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.
|
|
|
|
import 'dart:convert';
|
|
// We need to use the 'io' prefix here, otherwise io.exitCode will shadow
|
|
// CommandOutput.exitCode in subclasses of CommandOutput.
|
|
import 'dart:io' as io;
|
|
|
|
import 'package:dart2js_tools/deobfuscate_stack_trace.dart';
|
|
import 'package:status_file/expectation.dart';
|
|
import 'package:test_runner/src/static_error.dart';
|
|
|
|
import 'browser_controller.dart';
|
|
import 'command.dart';
|
|
import 'configuration.dart';
|
|
import 'path.dart';
|
|
import 'process_queue.dart';
|
|
import 'terminal.dart';
|
|
import 'test_case.dart';
|
|
import 'test_progress.dart';
|
|
import 'utils.dart';
|
|
|
|
/// CommandOutput records the output of a completed command: the process's exit
|
|
/// code, the standard output and standard error, whether the process timed out,
|
|
/// and the time the process took to run. It does not contain a pointer to the
|
|
/// [TestCase] this is the output of, so some functions require the test case
|
|
/// to be passed as an argument.
|
|
class CommandOutput {
|
|
final Command command;
|
|
|
|
final bool hasTimedOut;
|
|
|
|
final Duration time;
|
|
|
|
final int exitCode;
|
|
|
|
final int pid;
|
|
|
|
final List<int> stdout;
|
|
final List<int> stderr;
|
|
|
|
final bool compilationSkipped;
|
|
|
|
final List<String> diagnostics = [];
|
|
|
|
CommandOutput(
|
|
this.command,
|
|
this.exitCode,
|
|
this.hasTimedOut,
|
|
this.stdout,
|
|
this.stderr,
|
|
this.time,
|
|
this.compilationSkipped,
|
|
this.pid,
|
|
);
|
|
|
|
Expectation result(TestCase testCase) {
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (_didFail(testCase)) return Expectation.fail;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
return Expectation.pass;
|
|
}
|
|
|
|
bool get hasCrashed {
|
|
// dart2js exits with code 253 in case of unhandled exceptions.
|
|
// The dart binary exits with code 253 in case of an API error such
|
|
// as an invalid snapshot file.
|
|
// The batch mode can also exit 253 (unhandledCompilerExceptionExitCode).
|
|
// In either case an exit code of 253 is considered a crash.
|
|
if (exitCode == unhandledCompilerExceptionExitCode) return true;
|
|
if (exitCode == parseFailExitCode) return false;
|
|
if (hasTimedOut) return false;
|
|
if (io.Platform.isWindows) {
|
|
// The VM uses std::abort to terminate on asserts.
|
|
// std::abort terminates with exit code 3 on Windows.
|
|
if (exitCode == 3) return true;
|
|
|
|
// When VM is built with Crashpad support we get STATUS_FATAL_APP_EXIT
|
|
// for all crashes that Crashpad has intercepted.
|
|
if (exitCode == 0x40000015) return true;
|
|
|
|
// If a program receives an uncaught system exception, the program
|
|
// terminates with the exception code as exit code.
|
|
// https://msdn.microsoft.com/en-us/library/cc704588.aspx lists status
|
|
// codes basically saying that codes starting with 0xC0, 0x80 or 0x40
|
|
// are crashes, so look at the 4 most significant bits in 32-bit-space
|
|
// make sure its either 0b1100, 0b1000 or 0b0100.
|
|
var masked = (exitCode & 0xF0000000) >> 28;
|
|
return (exitCode < 0) && (masked >= 4) && ((masked & 3) == 0);
|
|
}
|
|
return exitCode < 0;
|
|
}
|
|
|
|
bool get hasCoreDump {
|
|
// Unhandled dart exceptions don't produce crashdumps.
|
|
return hasCrashed && exitCode != 253;
|
|
}
|
|
|
|
bool _didFail(TestCase testCase) => exitCode != 0 && !hasCrashed;
|
|
|
|
bool get canRunDependentCommands {
|
|
// TODO(kustermann): We may need to change this
|
|
return !hasTimedOut && exitCode == 0;
|
|
}
|
|
|
|
bool get successful {
|
|
// TODO(kustermann): We may need to change this
|
|
return !hasTimedOut && exitCode == 0;
|
|
}
|
|
|
|
bool get hasNonUtf8 => exitCode == nonUtfFakeExitCode;
|
|
|
|
/// Whether the command's output was too long and was truncated.
|
|
bool get truncatedOutput => exitCode == truncatedFakeExitCode;
|
|
|
|
/// Called when producing output for a test failure to describe this output.
|
|
void describe(TestCase testCase, Progress progress, OutputWriter output) {
|
|
output.subsection("exit code");
|
|
output.write(exitCode.toString());
|
|
|
|
if (diagnostics.isNotEmpty) {
|
|
output.subsection("diagnostics");
|
|
output.writeAll(diagnostics);
|
|
}
|
|
|
|
if (stdout.isNotEmpty) {
|
|
output.subsection("stdout");
|
|
output.writeAll(decodeLines(stdout));
|
|
}
|
|
|
|
if (stderr.isNotEmpty) {
|
|
output.subsection("stderr");
|
|
output.writeAll(decodeLines(stderr));
|
|
}
|
|
}
|
|
}
|
|
|
|
class BrowserTestJsonResult {
|
|
static const _allowedTypes = [
|
|
'sync_exception',
|
|
'window_onerror',
|
|
'script_onerror',
|
|
'window_compilationerror',
|
|
'print',
|
|
'message_received',
|
|
'dom',
|
|
'debug',
|
|
];
|
|
|
|
final Expectation outcome;
|
|
final String htmlDom;
|
|
final List<dynamic> events;
|
|
|
|
BrowserTestJsonResult(this.outcome, this.htmlDom, this.events);
|
|
|
|
static BrowserTestJsonResult? parseFromString(String content) {
|
|
void validate(String message, bool isValid) {
|
|
if (!isValid) {
|
|
throw "InvalidFormat sent from browser driving page: $message:\n\n"
|
|
"$content";
|
|
}
|
|
}
|
|
|
|
try {
|
|
var events = jsonDecode(content);
|
|
if (events != null) {
|
|
validate("Message must be a List", events is List);
|
|
// TODO(srawlins): This will promote `events` in null safety.
|
|
var eventList = events as List<dynamic>;
|
|
|
|
var messagesByType = {
|
|
for (var type in _allowedTypes) type: <String?>[],
|
|
};
|
|
|
|
for (var entry in eventList) {
|
|
validate("Entry must be a Map", entry is Map);
|
|
|
|
var type = entry['type'];
|
|
validate("'type' must be a String", type is String);
|
|
validate(
|
|
"'type' has to be in $_allowedTypes.",
|
|
_allowedTypes.contains(type),
|
|
);
|
|
|
|
var value = entry['value'];
|
|
validate("'value' must be a String", value is String);
|
|
|
|
var timestamp = entry['timestamp'];
|
|
validate("'timestamp' must be a number", timestamp is num);
|
|
|
|
var stackTrace = entry['stack_trace'];
|
|
if (stackTrace != null) {
|
|
validate("'stack_trace' must be a String", stackTrace is String);
|
|
}
|
|
|
|
messagesByType[type]!.add(value as String?);
|
|
}
|
|
validate(
|
|
"The message must have exactly one 'dom' entry.",
|
|
messagesByType['dom']!.length == 1,
|
|
);
|
|
|
|
var dom = messagesByType['dom']![0]!;
|
|
if (dom.endsWith('\n')) {
|
|
dom = '$dom\n';
|
|
}
|
|
|
|
return BrowserTestJsonResult(
|
|
_getOutcome(messagesByType),
|
|
dom,
|
|
eventList,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// If something goes wrong, we know the content was not in the correct
|
|
// JSON format. So we can't parse it.
|
|
// The caller is responsible for falling back to the old way of
|
|
// determining if a test failed.
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static Expectation _getOutcome(Map<String, List<String?>> messagesByType) {
|
|
occurred(String type) => messagesByType[type]!.isNotEmpty;
|
|
|
|
searchForMsg(List<String> types, String message) {
|
|
return types.any((type) => messagesByType[type]!.contains(message));
|
|
}
|
|
|
|
// TODO(kustermann,ricow): I think this functionality doesn't work in
|
|
// test_controller.js: So far I haven't seen anything being reported on
|
|
// "window.compilationerror"
|
|
if (occurred('window_compilationerror')) {
|
|
return Expectation.compileTimeError;
|
|
}
|
|
|
|
if (occurred('sync_exception') ||
|
|
occurred('window_onerror') ||
|
|
occurred('script_onerror')) {
|
|
return Expectation.runtimeError;
|
|
}
|
|
|
|
if (messagesByType['dom']![0]!.contains('FAIL')) {
|
|
return Expectation.runtimeError;
|
|
}
|
|
|
|
// We search for these messages in 'print' and 'message_received' because
|
|
// the unittest implementation posts these messages using
|
|
// "window.postMessage()" instead of the normal "print()" them.
|
|
|
|
var isAsyncTest = searchForMsg([
|
|
'print',
|
|
'message_received',
|
|
], 'unittest-suite-wait-for-done');
|
|
var isAsyncSuccess =
|
|
searchForMsg(['print', 'message_received'], 'unittest-suite-success') ||
|
|
searchForMsg(['print', 'message_received'], 'unittest-suite-done');
|
|
|
|
if (isAsyncTest) {
|
|
if (isAsyncSuccess) {
|
|
return Expectation.pass;
|
|
}
|
|
return Expectation.runtimeError;
|
|
}
|
|
|
|
var mainStarted = searchForMsg([
|
|
'print',
|
|
'message_received',
|
|
], 'dart-calling-main');
|
|
var mainDone = searchForMsg([
|
|
'print',
|
|
'message_received',
|
|
], 'dart-main-done');
|
|
|
|
if (mainStarted && mainDone) {
|
|
return Expectation.pass;
|
|
}
|
|
return Expectation.fail;
|
|
}
|
|
}
|
|
|
|
class BrowserCommandOutput extends CommandOutput
|
|
with _UnittestSuiteMessagesMixin {
|
|
final BrowserTestJsonResult? _jsonResult;
|
|
final BrowserTestOutput _result;
|
|
final Expectation _outcome;
|
|
|
|
/// Directory that is being served under `http:/.../root_build/` to browser
|
|
/// tests.
|
|
final String _buildDirectory;
|
|
|
|
factory BrowserCommandOutput(
|
|
BrowserTestCommand command,
|
|
BrowserTestOutput result,
|
|
) {
|
|
Expectation outcome;
|
|
|
|
var parsedResult = BrowserTestJsonResult.parseFromString(
|
|
result.lastKnownMessage,
|
|
);
|
|
if (parsedResult != null) {
|
|
outcome = parsedResult.outcome;
|
|
} else {
|
|
// Old way of determining whether a test failed or passed.
|
|
if (result.lastKnownMessage.contains("FAIL")) {
|
|
outcome = Expectation.runtimeError;
|
|
} else if (result.lastKnownMessage.contains("PASS")) {
|
|
outcome = Expectation.pass;
|
|
} else {
|
|
outcome = Expectation.runtimeError;
|
|
}
|
|
}
|
|
|
|
var stderr = "";
|
|
if (result.didTimeout) {
|
|
if (result.delayUntilTestStarted != null) {
|
|
stderr =
|
|
"This test timed out. The delay until the test actually "
|
|
"started was: ${result.delayUntilTestStarted}.";
|
|
} else {
|
|
stderr = "This test did not notify test.py that it started running.";
|
|
}
|
|
}
|
|
|
|
return BrowserCommandOutput._internal(
|
|
command,
|
|
result,
|
|
outcome,
|
|
parsedResult,
|
|
command.configuration.buildDirectory,
|
|
utf8.encode(""),
|
|
utf8.encode(stderr),
|
|
);
|
|
}
|
|
|
|
BrowserCommandOutput._internal(
|
|
Command command,
|
|
BrowserTestOutput result,
|
|
this._outcome,
|
|
this._jsonResult,
|
|
this._buildDirectory,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
) : _result = result,
|
|
super(
|
|
command,
|
|
0,
|
|
result.didTimeout,
|
|
stdout,
|
|
stderr,
|
|
result.duration,
|
|
false,
|
|
0,
|
|
);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// Handle timeouts first.
|
|
if (_result.didTimeout) {
|
|
return Expectation.timeout;
|
|
}
|
|
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
return _outcome;
|
|
}
|
|
|
|
@override
|
|
void describe(TestCase testCase, Progress progress, OutputWriter output) {
|
|
if (_jsonResult != null) {
|
|
_describeEvents(progress, output, _jsonResult);
|
|
} else {
|
|
// We couldn't parse the events, so fallback to showing the last message.
|
|
output.section("Last message");
|
|
output.write(_result.lastKnownMessage);
|
|
}
|
|
|
|
super.describe(testCase, progress, output);
|
|
|
|
if (_result.browserOutput.stdout.isNotEmpty) {
|
|
output.subsection("Browser stdout");
|
|
output.write(_result.browserOutput.stdout.toString());
|
|
}
|
|
|
|
if (_result.browserOutput.stderr.isNotEmpty) {
|
|
output.subsection("Browser stderr");
|
|
output.write(_result.browserOutput.stderr.toString());
|
|
}
|
|
}
|
|
|
|
void _describeEvents(
|
|
Progress progress,
|
|
OutputWriter output,
|
|
BrowserTestJsonResult jsonResult,
|
|
) {
|
|
// Always show the error events since those are most useful.
|
|
var errorShown = false;
|
|
|
|
void showError(String header, event) {
|
|
output.subsection(header);
|
|
var value = event["value"] as String?;
|
|
if (event["stack_trace"] != null) {
|
|
value = '$value\n${event["stack_trace"]}';
|
|
}
|
|
errorShown = true;
|
|
output.write(value);
|
|
|
|
// Skip deobfuscation if there is no indication that there is a stack
|
|
// trace in the string value.
|
|
if (!value!.contains('.js:')) return;
|
|
var stringStack = value
|
|
// Convert `http:` URIs to relative `file:` URIs.
|
|
.replaceAll(RegExp(r'http://[^/]*/root_build/'), '$_buildDirectory/')
|
|
.replaceAll(RegExp(r'http://[^/]*/root_dart/'), '')
|
|
// Remove query parameters (seen in .html URIs).
|
|
.replaceAll(RegExp(r'\?[^:\n]*:'), ':');
|
|
// TODO(sigmund): change internal deobfuscation code to avoid spurious
|
|
// error messages when files do not have a corresponding source-map.
|
|
_deobfuscateAndWriteStack(stringStack, output);
|
|
}
|
|
|
|
for (var event in jsonResult.events) {
|
|
if (event["type"] == "sync_exception") {
|
|
showError("Runtime error", event);
|
|
} else if (event["type"] == "window_onerror") {
|
|
showError("Runtime window.onerror", event);
|
|
}
|
|
}
|
|
|
|
// Show the events unless the above error was sufficient.
|
|
// TODO(rnystrom): Let users enable or disable this explicitly?
|
|
if (errorShown && progress != Progress.verbose) {
|
|
return;
|
|
}
|
|
|
|
output.subsection("Events");
|
|
for (var event in jsonResult.events) {
|
|
switch (event["type"] as String?) {
|
|
case "debug":
|
|
output.write('- debug "${event["value"]}"');
|
|
break;
|
|
|
|
case "dom":
|
|
output.write('- dom\n${indent(event["value"] as String, 2)}');
|
|
break;
|
|
|
|
case "print":
|
|
output.write('- print "${event["value"]}"');
|
|
break;
|
|
|
|
case "window_onerror":
|
|
var value = event["value"] as String;
|
|
value = indent(value.trim(), 2);
|
|
value = "- ${value.substring(2)}";
|
|
output.write(value);
|
|
break;
|
|
|
|
default:
|
|
output.write(
|
|
"- ${const JsonEncoder.withIndent(' ').convert(event)}",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A parsed analyzer error diagnostic.
|
|
class AnalyzerError implements Comparable<AnalyzerError> {
|
|
/// The set of static warnings which must be expected in a test. Any warning
|
|
/// not listed here does not need to be expected, and never causes a test to
|
|
/// fail.
|
|
static const Set<String> _validatedWarnings = {
|
|
'dead_null_aware_expression',
|
|
'invalid_null_aware_operator',
|
|
'invalid_null_aware_element_or_map_entry',
|
|
'missing_enum_constant_in_switch',
|
|
'unnecessary_non_null_assertion',
|
|
'unnecessary_null_assert_pattern',
|
|
'unnecessary_null_check_pattern',
|
|
'unreachable_switch_case',
|
|
'unreachable_switch_default',
|
|
};
|
|
|
|
/// Whether [code] is a warning that should be expected by a test.
|
|
///
|
|
/// Any warning whose code is not covered here is implicitly ignored by the
|
|
/// test runner. Tests often have unused local variables, dead code, etc. and
|
|
/// we don't want to clutter up the test expectations with those.
|
|
static bool isValidatedWarning(String code) =>
|
|
_validatedWarnings.contains(code);
|
|
|
|
/// The set of hints which must be expected in a test. Any hint not specified
|
|
/// here which is reported by the analyzer does not need to be expected, and
|
|
/// never causes a test to fail.
|
|
static const Set<String> _specifiedHints = {};
|
|
|
|
/// Parses all errors from analyzer [stdout] output.
|
|
static List<AnalyzerError> parseStdout(String stdout) {
|
|
var result = <AnalyzerError>[];
|
|
|
|
var jsonData = json.decode(stdout) as Map<String, dynamic>;
|
|
var version = jsonData['version'];
|
|
if (version != 1) {
|
|
DebugLogger.error('Unexpected analyzer JSON data version: $version');
|
|
throw UnimplementedError();
|
|
}
|
|
|
|
for (var diagnostic in jsonData['diagnostics'] as List<dynamic>) {
|
|
var diagnosticMap = diagnostic as Map<String, dynamic>;
|
|
|
|
var code = diagnosticMap['code'] as String;
|
|
var type = diagnosticMap['type'] as String?;
|
|
|
|
if (type == 'HINT' && !_specifiedHints.contains(code)) {
|
|
// The analyzer can report hints which do not need to be expected in
|
|
// the test source. These can be ignored.
|
|
// TODO(srawlins): Hints will start to change to be warnings. There are
|
|
// some warnings produced now which must be expected. See
|
|
// [StaticError._analyzerWarningCodes]. When we change hints to
|
|
// warnings, we will need to ignore them here.
|
|
continue;
|
|
}
|
|
|
|
if (type == 'STATIC_WARNING' && !_validatedWarnings.contains(code)) {
|
|
continue;
|
|
}
|
|
|
|
if (type == 'LINT') continue;
|
|
|
|
var errorCode = '$type.${code.toUpperCase()}';
|
|
|
|
var error = _parse(
|
|
diagnosticMap,
|
|
diagnosticMap['problemMessage'] as String,
|
|
errorCode,
|
|
);
|
|
result.add(error);
|
|
|
|
var contextMessages = diagnosticMap['contextMessages'] as List<dynamic>?;
|
|
for (var contextMessage in contextMessages ?? const []) {
|
|
var contextMessageMap = contextMessage as Map<String, dynamic>;
|
|
error.contextMessages.add(
|
|
_parse(contextMessageMap, contextMessageMap['message'] as String),
|
|
);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static AnalyzerError _parse(
|
|
Map<String, dynamic> diagnostic,
|
|
String message, [
|
|
String? errorCode,
|
|
]) {
|
|
var location = diagnostic['location'] as Map<String, dynamic>;
|
|
|
|
var range = location['range'] as Map<String, dynamic>;
|
|
var start = range['start'] as Map<String, dynamic>;
|
|
var end = range['end'] as Map<String, dynamic>;
|
|
return AnalyzerError._(
|
|
severity: diagnostic['severity'] as String? ?? '',
|
|
errorCode: errorCode ?? '',
|
|
file: location['file'] as String,
|
|
message: message,
|
|
line: start['line'] as int,
|
|
column: start['column'] as int,
|
|
length: (end['offset'] as int) - (start['offset'] as int),
|
|
);
|
|
}
|
|
|
|
final String severity;
|
|
final String errorCode;
|
|
final String file;
|
|
final String message;
|
|
final int line;
|
|
final int column;
|
|
final int length;
|
|
|
|
final List<AnalyzerError> contextMessages = [];
|
|
|
|
AnalyzerError._({
|
|
this.severity = '',
|
|
this.errorCode = '',
|
|
required this.file,
|
|
required this.message,
|
|
required this.line,
|
|
required this.column,
|
|
required this.length,
|
|
});
|
|
|
|
@override
|
|
int compareTo(AnalyzerError other) {
|
|
if (file != other.file) return file.compareTo(other.file);
|
|
if (line != other.line) return line.compareTo(other.line);
|
|
if (column != other.column) return column.compareTo(other.column);
|
|
if (length != other.length) return length.compareTo(other.length);
|
|
if (severity != other.severity) return severity.compareTo(other.severity);
|
|
if (errorCode != other.errorCode) {
|
|
return errorCode.compareTo(other.errorCode);
|
|
}
|
|
return message.compareTo(other.message);
|
|
}
|
|
}
|
|
|
|
class AnalysisCommandOutput extends CommandOutput with _StaticErrorOutput {
|
|
static void parseErrors(
|
|
String stdout,
|
|
List<StaticError> errors, [
|
|
List<StaticError>? warnings,
|
|
]) {
|
|
StaticError convert(AnalyzerError error) {
|
|
var staticError = StaticError(
|
|
ErrorSource.analyzer,
|
|
error.errorCode,
|
|
path: error.file,
|
|
line: error.line,
|
|
column: error.column,
|
|
length: error.length,
|
|
);
|
|
|
|
for (var context in error.contextMessages) {
|
|
staticError.contextMessages.add(
|
|
StaticError(
|
|
ErrorSource.context,
|
|
context.message,
|
|
path: context.file,
|
|
line: context.line,
|
|
column: context.column,
|
|
length: context.length,
|
|
),
|
|
);
|
|
}
|
|
|
|
return staticError;
|
|
}
|
|
|
|
// Parse as Analyzer errors and then convert them to the StaticError objects
|
|
// the static error tests expect.
|
|
for (var diagnostic in AnalyzerError.parseStdout(stdout)) {
|
|
if (diagnostic.severity == 'ERROR') {
|
|
errors.add(convert(diagnostic));
|
|
} else if (warnings != null &&
|
|
(diagnostic.severity == 'WARNING' || diagnostic.severity == 'INFO')) {
|
|
warnings.add(convert(diagnostic));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If the stdout of analyzer could not be parsed as valid JSON, this will be
|
|
/// the stdout as a string instead. Otherwise it will be null.
|
|
String? get invalidJsonStdout {
|
|
if (!_parsedErrors) {
|
|
_parseErrors();
|
|
_parsedErrors = true;
|
|
}
|
|
|
|
return _invalidJsonStdout;
|
|
}
|
|
|
|
String? _invalidJsonStdout;
|
|
|
|
AnalysisCommandOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
bool compilationSkipped,
|
|
) : super(
|
|
command,
|
|
exitCode,
|
|
timedOut,
|
|
stdout,
|
|
stderr,
|
|
time,
|
|
compilationSkipped,
|
|
0,
|
|
);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// TODO(kustermann): If we run the analyzer not in batch mode, make sure
|
|
// that command.exitCodes matches 2 (errors), 1 (warnings), 0 (no warnings,
|
|
// no errors)
|
|
|
|
// Handle crashes and timeouts first.
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
if (invalidJsonStdout != null) return Expectation.fail;
|
|
|
|
// If it's a static error test, validate the exact errors.
|
|
if (testCase.testFile.isStaticErrorTest) {
|
|
return _validateExpectedErrors(testCase);
|
|
}
|
|
|
|
if (errors.isNotEmpty) {
|
|
return Expectation.compileTimeError;
|
|
}
|
|
if (warnings.isNotEmpty) {
|
|
return Expectation.staticWarning;
|
|
}
|
|
return Expectation.pass;
|
|
}
|
|
|
|
@override
|
|
void describe(TestCase testCase, Progress progress, OutputWriter output) {
|
|
if (invalidJsonStdout != null) {
|
|
output.subsection("analyzer json parse result");
|
|
output.write("- parse failed");
|
|
output.subsection("invalid analyzer json");
|
|
super.describe(testCase, progress, output);
|
|
return;
|
|
}
|
|
|
|
// Handle static error test output specially. We don't want to show the raw
|
|
// stdout if we can give the user the parsed expectations instead.
|
|
if (testCase.testFile.isStaticErrorTest || hasCrashed || hasTimedOut) {
|
|
super.describe(testCase, progress, output);
|
|
} else {
|
|
// Parse and sort the errors.
|
|
var errorsByFile = <String?, List<AnalyzerError>>{};
|
|
for (var error in AnalyzerError.parseStdout(decodeUtf8(stdout))) {
|
|
errorsByFile.putIfAbsent(error.file, () => []).add(error);
|
|
}
|
|
|
|
var files = errorsByFile.keys.toList();
|
|
files.sort();
|
|
|
|
for (var file in files) {
|
|
var path = Path(
|
|
file!,
|
|
).relativeTo(testCase.testFile.path.directoryPath).toString();
|
|
output.subsection("unexpected analysis errors in $path");
|
|
|
|
var errors = errorsByFile[file]!;
|
|
errors.sort();
|
|
|
|
for (var error in errors) {
|
|
var line = error.line.toString();
|
|
var column = error.column.toString();
|
|
var message = wordWrap(error.message.trim(), prefix: " ");
|
|
output.write("- Line $line, column $column: ${error.errorCode}");
|
|
output.write(" $message");
|
|
output.separator();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses the JSON output of the analyzer.
|
|
@override
|
|
void _parseErrors() {
|
|
var stdoutString = decodeUtf8(stdout);
|
|
try {
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
parseErrors(stdoutString, errors, warnings);
|
|
errors.forEach(addError);
|
|
warnings.forEach(addWarning);
|
|
} on FormatException {
|
|
// It wasn't JSON. This can happen if analyzer instead prints:
|
|
// "No dart files found at: ..."
|
|
_invalidJsonStdout = stdoutString;
|
|
}
|
|
}
|
|
}
|
|
|
|
class CompareAnalyzerCfeCommandOutput extends CommandOutput {
|
|
CompareAnalyzerCfeCommandOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
bool compilationSkipped,
|
|
) : super(
|
|
command,
|
|
exitCode,
|
|
timedOut,
|
|
stdout,
|
|
stderr,
|
|
time,
|
|
compilationSkipped,
|
|
0,
|
|
);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// Handle crashes and timeouts first
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
if (exitCode != 0) return Expectation.fail;
|
|
for (var line in decodeUtf8(stdout).split('\n')) {
|
|
if (line.contains('No differences found')) return Expectation.pass;
|
|
if (line.contains('Differences found')) return Expectation.fail;
|
|
}
|
|
return Expectation.fail;
|
|
}
|
|
}
|
|
|
|
class SpecParseCommandOutput extends CommandOutput with _StaticErrorOutput {
|
|
SpecParseCommandOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
bool compilationSkipped,
|
|
) : super(
|
|
command,
|
|
exitCode,
|
|
timedOut,
|
|
stdout,
|
|
stderr,
|
|
time,
|
|
compilationSkipped,
|
|
0,
|
|
);
|
|
|
|
bool get hasSyntaxError => exitCode == parseFailExitCode;
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
if (testCase.testFile.isStaticErrorTest) {
|
|
return _validateExpectedErrors(testCase);
|
|
}
|
|
if (hasSyntaxError || exitCode != 0) return Expectation.syntaxError;
|
|
return Expectation.pass;
|
|
}
|
|
|
|
/// Parses the ANTLR parser's diagnostic output.
|
|
///
|
|
/// For example: `void main() {(}` in 'foo.dart' yields
|
|
/// Syntax error in foo.dart:
|
|
/// line 1:14 no viable alternative at input '(}'
|
|
/// Parsing failed
|
|
///
|
|
/// The 'Syntax error' line is emitted once for each file with
|
|
/// a syntax error, and each error gets a 'line ...' message.
|
|
@override
|
|
void _parseErrors() {
|
|
var output = decodeUtf8(stderr);
|
|
var lines = output.split('\n');
|
|
String? currentPath;
|
|
var processCommand = command as ProcessCommand;
|
|
var fallbackPath = processCommand.arguments.firstWhere(
|
|
(arg) => arg.endsWith('.dart'),
|
|
orElse: () => '',
|
|
);
|
|
for (var line in lines) {
|
|
if (_pathRegExp.firstMatch(line) case var match?) {
|
|
currentPath = match[1];
|
|
} else if (_errorRegExp.firstMatch(line) case var match?) {
|
|
var lineNum = int.parse(match[1]!);
|
|
var column = int.parse(match[2]!) + 1; // ANTLR is 0-based.
|
|
var message = match[3]!;
|
|
addError(
|
|
StaticError(
|
|
ErrorSource.specParser,
|
|
message,
|
|
path: currentPath ?? fallbackPath,
|
|
line: lineNum,
|
|
column: column,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
static final RegExp _pathRegExp = .new(r"^Syntax error in (.*):$");
|
|
static final RegExp _errorRegExp = .new(r"^line (\d+):(\d+) (.*)$");
|
|
}
|
|
|
|
class VMCommandOutput extends CommandOutput with _UnittestSuiteMessagesMixin {
|
|
static const _dfeErrorExitCode = 252;
|
|
static const _compileErrorExitCode = 254;
|
|
static const _uncaughtExceptionExitCode = 255;
|
|
static const _adbInfraFailureCodes = [10];
|
|
static const _adbFailureExitCode = 3;
|
|
static const _frontEndTestExitCode = 1;
|
|
|
|
VMCommandOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
int pid,
|
|
) : super(command, exitCode, timedOut, stdout, stderr, time, false, pid);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// `ffx test` isn't preserving exit codes.
|
|
// TODO(38752): Plumb exit codes through something else?
|
|
if (testCase.configuration.system == System.fuchsia) {
|
|
if (utf8.decode(stdout).contains("completed with result: PASSED")) {
|
|
return Expectation.pass;
|
|
}
|
|
return Expectation.fail;
|
|
}
|
|
|
|
// Handle crashes and timeouts first.
|
|
if (exitCode == _dfeErrorExitCode) return Expectation.dartkCrash;
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
// The actual outcome depends on the exitCode.
|
|
if (exitCode == _compileErrorExitCode) return Expectation.compileTimeError;
|
|
if (exitCode == _uncaughtExceptionExitCode) return Expectation.runtimeError;
|
|
if (exitCode == _frontEndTestExitCode &&
|
|
testCase.displayName.startsWith('pkg/front_end/test/')) {
|
|
return Expectation.runtimeError;
|
|
}
|
|
if (testCase.configuration.system == System.android &&
|
|
_adbInfraFailureCodes.contains(exitCode)) {
|
|
print('Android device failed to run test');
|
|
print(command);
|
|
print(decodeUtf8(stdout));
|
|
print(decodeUtf8(stderr));
|
|
io.exit(_adbFailureExitCode);
|
|
}
|
|
if (exitCode != 0) {
|
|
// This is a general fail, in case we get an unknown nonzero exitcode.
|
|
return Expectation.fail;
|
|
}
|
|
var testOutput = decodeUtf8(stdout);
|
|
if (_isAsyncTest(testOutput) && !_isAsyncTestSuccessful(testOutput)) {
|
|
return Expectation.fail;
|
|
}
|
|
return Expectation.pass;
|
|
}
|
|
}
|
|
|
|
class CompilationCommandOutput extends CommandOutput {
|
|
static const _crashExitCode = 253;
|
|
|
|
CompilationCommandOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
bool compilationSkipped,
|
|
) : super(
|
|
command,
|
|
exitCode,
|
|
timedOut,
|
|
stdout,
|
|
stderr,
|
|
time,
|
|
compilationSkipped,
|
|
0,
|
|
);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// Handle general crash/timeout detection.
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) {
|
|
var isWindows = io.Platform.operatingSystem == 'windows';
|
|
var isBrowserTestCase = testCase.commands.any(
|
|
(command) => command is BrowserTestCommand,
|
|
);
|
|
// TODO(26060) Dart2js batch mode hangs on Windows under heavy load.
|
|
return (isWindows && isBrowserTestCase)
|
|
? Expectation.ignore
|
|
: Expectation.timeout;
|
|
}
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
// Handle dart2js specific crash detection
|
|
if (exitCode == _crashExitCode ||
|
|
exitCode == VMCommandOutput._compileErrorExitCode ||
|
|
exitCode == VMCommandOutput._uncaughtExceptionExitCode) {
|
|
return Expectation.crash;
|
|
}
|
|
if (exitCode != 0) return Expectation.compileTimeError;
|
|
return Expectation.pass;
|
|
}
|
|
}
|
|
|
|
class Dart2jsCompilerCommandOutput extends CompilationCommandOutput
|
|
with _StaticErrorOutput {
|
|
static void parseErrors(
|
|
String stdout,
|
|
List<StaticError> errors, [
|
|
List<StaticError>? warnings,
|
|
]) {
|
|
_StaticErrorOutput._parseCfeErrors(
|
|
ErrorSource.web,
|
|
_errorRegexp,
|
|
stdout,
|
|
errors,
|
|
warnings,
|
|
);
|
|
}
|
|
|
|
/// Matches the location and message of a dart2js error message, which looks
|
|
/// like:
|
|
///
|
|
/// tests/language/some_test.dart:9:3:
|
|
/// Error: Some message.
|
|
/// BadThing();
|
|
/// ^
|
|
///
|
|
/// The test runner only validates the main error message, and not the
|
|
/// suggested fixes, so we only parse the first line.
|
|
// TODO(rnystrom): Support validating context messages.
|
|
static final _errorRegexp = RegExp(
|
|
r"^([^:\n\r]+):(\d+):(\d+):\n(Error|Warning): (.*)$",
|
|
multiLine: true,
|
|
);
|
|
|
|
Dart2jsCompilerCommandOutput(
|
|
super.command,
|
|
super.exitCode,
|
|
super.timedOut,
|
|
super.stdout,
|
|
super.stderr,
|
|
super.time,
|
|
super.compilationSkipped,
|
|
);
|
|
|
|
@override
|
|
void _parseErrors() {
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
parseErrors(decodeUtf8(stdout), errors, warnings);
|
|
errors.forEach(addError);
|
|
warnings.forEach(addWarning);
|
|
}
|
|
}
|
|
|
|
class Dart2WasmCompilerCommandOutput extends CompilationCommandOutput
|
|
with _StaticErrorOutput {
|
|
static void parseErrors(
|
|
String stdout,
|
|
List<StaticError> errors, [
|
|
List<StaticError>? warnings,
|
|
]) {
|
|
_StaticErrorOutput._parseCfeErrors(
|
|
ErrorSource.web,
|
|
_errorRegexp,
|
|
stdout,
|
|
errors,
|
|
warnings,
|
|
);
|
|
}
|
|
|
|
/// Matches the location and message of a dart2wasm error message, which looks
|
|
/// like:
|
|
///
|
|
/// tests/language/some_test.dart:9:3: Error: Some message.
|
|
/// BadThing();
|
|
/// ^
|
|
///
|
|
/// The test runner only validates the main error message, and not the
|
|
/// suggested fixes, so we only parse the first line.
|
|
// TODO(rnystrom): Support validating context messages.
|
|
static final _errorRegexp = RegExp(
|
|
r"^([^:\n\r]+):(\d+):(\d+): (Error|Warning): (.*)$",
|
|
multiLine: true,
|
|
);
|
|
|
|
Dart2WasmCompilerCommandOutput(
|
|
super.command,
|
|
super.exitCode,
|
|
super.timedOut,
|
|
super.stdout,
|
|
super.stderr,
|
|
super.time,
|
|
super.compilationSkipped,
|
|
);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
switch (exitCode) {
|
|
case VMCommandOutput._dfeErrorExitCode:
|
|
return Expectation.dartkCrash;
|
|
case VMCommandOutput._compileErrorExitCode:
|
|
if (testCase.testFile.isStaticErrorTest) {
|
|
return _validateExpectedErrors(testCase);
|
|
}
|
|
return Expectation.compileTimeError;
|
|
case VMCommandOutput._uncaughtExceptionExitCode:
|
|
return Expectation.crash;
|
|
default:
|
|
return exitCode != 0 ? Expectation.fail : Expectation.pass;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void _parseErrors() {
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
// We expect errors to be printed to `stderr` for dart2wasm.
|
|
parseErrors(decodeUtf8(stderr), errors, warnings);
|
|
errors.forEach(addError);
|
|
warnings.forEach(addWarning);
|
|
}
|
|
}
|
|
|
|
class DevCompilerCommandOutput extends CommandOutput with _StaticErrorOutput {
|
|
static void parseErrors(
|
|
String stdout,
|
|
List<StaticError> errors, [
|
|
List<StaticError>? warnings,
|
|
]) {
|
|
_StaticErrorOutput._parseCfeErrors(
|
|
ErrorSource.web,
|
|
_errorRegexp,
|
|
stdout,
|
|
errors,
|
|
warnings,
|
|
);
|
|
}
|
|
|
|
/// Matches the first line of a DDC error message. DDC prints errors to
|
|
/// stdout that look like:
|
|
///
|
|
/// org-dartlang-app:/tests/language/some_test.dart:7:21: Error: Some message.
|
|
/// Try fixing the code to be less bad.
|
|
/// var _ = <int>[if (1) 2];
|
|
/// ^
|
|
///
|
|
/// The test runner only validates the main error message, and not the
|
|
/// suggested fixes, so we only parse the first line.
|
|
// TODO(rnystrom): Support validating context messages.
|
|
static final _errorRegexp = RegExp(
|
|
r"^org-dartlang-app:/([^\n\r]+):(\d+):(\d+): (Error|Warning): (.*)$",
|
|
multiLine: true,
|
|
);
|
|
|
|
DevCompilerCommandOutput(
|
|
super.command,
|
|
super.exitCode,
|
|
super.timedOut,
|
|
super.stdout,
|
|
super.stderr,
|
|
super.time,
|
|
super.compilationSkipped,
|
|
super.pid,
|
|
);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
// If it's a static error test, validate the exact errors.
|
|
if (testCase.testFile.isStaticErrorTest) {
|
|
return _validateExpectedErrors(testCase);
|
|
}
|
|
|
|
if (exitCode != 0) return Expectation.compileTimeError;
|
|
|
|
return Expectation.pass;
|
|
}
|
|
|
|
@override
|
|
void _parseErrors() {
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
parseErrors(decodeUtf8(stdout), errors, warnings);
|
|
errors.forEach(addError);
|
|
warnings.forEach(addWarning);
|
|
}
|
|
}
|
|
|
|
class VMKernelCompilationCommandOutput extends CompilationCommandOutput {
|
|
VMKernelCompilationCommandOutput(
|
|
super.command,
|
|
super.exitCode,
|
|
super.timedOut,
|
|
super.stdout,
|
|
super.stderr,
|
|
super.time,
|
|
super.compilationSkipped,
|
|
);
|
|
|
|
@override
|
|
bool get canRunDependentCommands {
|
|
// See [BatchRunnerProcess]: 0 means success, 1 means compile-time error.
|
|
// TODO(asgerf): When the frontend supports it, continue running even if
|
|
// there were compile-time errors. See kernel_sdk issue #18.
|
|
return !hasCrashed && !hasTimedOut && exitCode == 0;
|
|
}
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// TODO(kustermann): Currently the batch mode runner (which can be found
|
|
// in `test_runner.dart:BatchRunnerProcess`) does not really distinguish
|
|
// between different kinds of failures and will mark a failed
|
|
// compilation to just an exit code of "1". So we treat all `exitCode ==
|
|
// 1`s as compile-time errors as well.
|
|
const batchModeCompileTimeErrorExit = 1;
|
|
|
|
// Handle crashes and timeouts first.
|
|
if (hasCrashed) return Expectation.dartkCrash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
// If the frontend had an uncaught exception, then we'll consider this a
|
|
// crash.
|
|
if (exitCode == VMCommandOutput._uncaughtExceptionExitCode) {
|
|
return Expectation.dartkCrash;
|
|
}
|
|
|
|
// Multitests are handled specially.
|
|
|
|
if (exitCode == VMCommandOutput._compileErrorExitCode ||
|
|
exitCode == batchModeCompileTimeErrorExit) {
|
|
return Expectation.compileTimeError;
|
|
}
|
|
if (exitCode != 0) {
|
|
// This is a general fail, in case we get an unknown nonzero exitcode.
|
|
return Expectation.fail;
|
|
}
|
|
return Expectation.pass;
|
|
}
|
|
|
|
/// If the compiler was able to produce a Kernel IR file we want to run the
|
|
/// result on the Dart VM. We therefore mark the [VMKernelCompilationCommand]
|
|
/// as successful.
|
|
///
|
|
/// This ensures we test that the DartVM produces correct CompileTime errors
|
|
/// as it is supposed to for our test suites.
|
|
@override
|
|
bool get successful => canRunDependentCommands;
|
|
}
|
|
|
|
class JSCommandLineOutput extends CommandOutput
|
|
with _UnittestSuiteMessagesMixin {
|
|
JSCommandLineOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
) : super(command, exitCode, timedOut, stdout, stderr, time, false, 0);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// Handle crashes and timeouts first.
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
if (exitCode != 0) return Expectation.runtimeError;
|
|
var output = decodeUtf8(stdout);
|
|
if (_isAsyncTest(output) && !_isAsyncTestSuccessful(output)) {
|
|
return Expectation.fail;
|
|
}
|
|
return Expectation.pass;
|
|
}
|
|
|
|
@override
|
|
void describe(TestCase testCase, Progress progress, OutputWriter output) {
|
|
super.describe(testCase, progress, output);
|
|
var decodedOut = decodeUtf8(
|
|
stdout,
|
|
).replaceAll('\r\n', '\n').replaceAll('\r', '\n').trim();
|
|
|
|
_deobfuscateAndWriteStack(decodedOut, output);
|
|
}
|
|
}
|
|
|
|
class Dart2WasmCommandLineOutput extends CommandOutput
|
|
with _UnittestSuiteMessagesMixin {
|
|
Dart2WasmCommandLineOutput(
|
|
Command command,
|
|
int exitCode,
|
|
bool timedOut,
|
|
List<int> stdout,
|
|
List<int> stderr,
|
|
Duration time,
|
|
) : super(command, exitCode, timedOut, stdout, stderr, time, false, 0);
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
// Handle crashes and timeouts first.
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
if (exitCode != 0) return Expectation.runtimeError;
|
|
var output = decodeUtf8(stdout);
|
|
if (_isAsyncTest(output) && !_isAsyncTestSuccessful(output)) {
|
|
return Expectation.fail;
|
|
}
|
|
return Expectation.pass;
|
|
}
|
|
}
|
|
|
|
class ScriptCommandOutput extends CommandOutput {
|
|
final Expectation _result;
|
|
|
|
ScriptCommandOutput(
|
|
ScriptCommand command,
|
|
this._result,
|
|
String scriptExecutionInformation,
|
|
Duration time,
|
|
) : super(command, 0, false, [], [], time, false, 0) {
|
|
var lines = scriptExecutionInformation.split("\n");
|
|
diagnostics.addAll(lines);
|
|
}
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) => _result;
|
|
|
|
@override
|
|
bool get canRunDependentCommands => _result == Expectation.pass;
|
|
|
|
@override
|
|
bool get successful => _result == Expectation.pass;
|
|
}
|
|
|
|
class FastaCommandOutput extends CompilationCommandOutput
|
|
with _StaticErrorOutput {
|
|
static void parseErrors(
|
|
String stdout,
|
|
List<StaticError> errors,
|
|
List<StaticError> warnings,
|
|
) {
|
|
_StaticErrorOutput._parseCfeErrors(
|
|
ErrorSource.cfe,
|
|
_errorRegexp,
|
|
stdout,
|
|
errors,
|
|
warnings,
|
|
);
|
|
}
|
|
|
|
/// Matches the first line of a Fasta error, warning, or context message.
|
|
/// Fasta prints to stdout like:
|
|
///
|
|
/// tests/language/some_test.dart:7:21: Error: Some message.
|
|
/// Try fixing the code to be less bad.
|
|
/// var _ = <int>[if (1) 2];
|
|
/// ^
|
|
///
|
|
/// The test runner only validates the first line of the message, and not the
|
|
/// suggested fixes.
|
|
static final _errorRegexp = RegExp(
|
|
r"^(?:([^:\n\r]+):(\d+):(\d+): )?(Context|Error|Warning): (.*)$",
|
|
multiLine: true,
|
|
);
|
|
|
|
FastaCommandOutput(
|
|
super.command,
|
|
super.exitCode,
|
|
super.hasTimedOut,
|
|
super.stdout,
|
|
super.stderr,
|
|
super.time,
|
|
super.compilationSkipped,
|
|
);
|
|
|
|
@override
|
|
void _parseErrors() {
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
parseErrors(decodeUtf8(stdout), errors, warnings);
|
|
errors.forEach(addError);
|
|
warnings.forEach(addWarning);
|
|
}
|
|
}
|
|
|
|
/// Mixin for outputs from a command that implement a Dart front end which
|
|
/// reports static errors.
|
|
mixin _StaticErrorOutput on CommandOutput {
|
|
/// Parses compile errors reported by CFE using the given [regExp] and adds
|
|
/// them to [errors] as coming from [errorSource].
|
|
static void _parseCfeErrors(
|
|
ErrorSource errorSource,
|
|
RegExp regExp,
|
|
String stdout,
|
|
List<StaticError> errors, [
|
|
List<StaticError>? warnings,
|
|
]) {
|
|
StaticError? previousError;
|
|
for (var match in regExp.allMatches(stdout)) {
|
|
// These three are either all present or all null.
|
|
var path = match[1];
|
|
var line = _parseNullableInt(match[2]);
|
|
var column = _parseNullableInt(match[3]);
|
|
|
|
var severity = match[4];
|
|
var message = match[5];
|
|
|
|
if (path == null) {
|
|
// No location information.
|
|
if (severity == 'Context' && previousError != null) {
|
|
// We can use the location information from the error message
|
|
path = previousError.path;
|
|
line = previousError.line;
|
|
column = previousError.column;
|
|
} else {
|
|
// No good default location information, so disregard the error.
|
|
// TODO(45558): we should do something smarter here.
|
|
continue;
|
|
}
|
|
}
|
|
// Line and column information should have been present or it should have
|
|
// been filled in by the code above.
|
|
assert(line != null);
|
|
assert(column != null);
|
|
|
|
var error = StaticError(
|
|
severity == "Context" ? ErrorSource.context : errorSource,
|
|
message!,
|
|
path: path,
|
|
line: line!,
|
|
column: column!,
|
|
);
|
|
|
|
if (severity == "Context") {
|
|
// Attach context messages to the preceding error/warning.
|
|
if (previousError == null) {
|
|
DebugLogger.error(
|
|
"Got context message in CFE output before "
|
|
"error to attach it to.",
|
|
);
|
|
} else {
|
|
previousError.contextMessages.add(error);
|
|
}
|
|
} else {
|
|
if (severity == "Error") {
|
|
errors.add(error);
|
|
} else {
|
|
warnings?.add(error);
|
|
}
|
|
previousError = error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Same as `int.parse`, but allows nulls to simply pass through.
|
|
static int? _parseNullableInt(String? s) {
|
|
if (s == null) {
|
|
return null;
|
|
}
|
|
return int.parse(s);
|
|
}
|
|
|
|
/// Reported static errors, parsed from [stderr].
|
|
List<StaticError> get errors {
|
|
if (!_parsedErrors) {
|
|
_parseErrors();
|
|
_parsedErrors = true;
|
|
}
|
|
return _errors;
|
|
}
|
|
|
|
/// Don't access this from outside of the mixin. It gets populated lazily by
|
|
/// going through the [errors] getter.
|
|
final List<StaticError> _errors = [];
|
|
|
|
/// Reported static warnings, parsed from [stderr].
|
|
List<StaticError> get warnings {
|
|
if (!_parsedErrors) {
|
|
_parseErrors();
|
|
_parsedErrors = true;
|
|
}
|
|
return _warnings;
|
|
}
|
|
|
|
/// Don't access this from outside of the mixin. It gets populated lazily by
|
|
/// going through the [warnings] getter.
|
|
final List<StaticError> _warnings = [];
|
|
|
|
bool _parsedErrors = false;
|
|
|
|
@override
|
|
void describe(TestCase testCase, Progress progress, OutputWriter output) {
|
|
// Handle static error test output specially. We don't want to show the raw
|
|
// stdout if we can give the user the parsed expectations instead.
|
|
if (testCase.testFile.isStaticErrorTest &&
|
|
!hasCrashed &&
|
|
!hasTimedOut &&
|
|
!hasNonUtf8 &&
|
|
!truncatedOutput) {
|
|
try {
|
|
_validateExpectedErrors(testCase, output);
|
|
} catch (_) {
|
|
// In the event of a crash trying to compute errors, go ahead and give
|
|
// the raw output.
|
|
super.describe(testCase, progress, output);
|
|
return;
|
|
}
|
|
|
|
// Always show the raw output when specifically requested.
|
|
if (progress == Progress.verbose) {
|
|
super.describe(testCase, progress, output);
|
|
}
|
|
} else {
|
|
// Something strange happened, so show the raw output.
|
|
super.describe(testCase, progress, output);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Expectation result(TestCase testCase) {
|
|
if (hasCrashed) return Expectation.crash;
|
|
if (hasTimedOut) return Expectation.timeout;
|
|
if (hasNonUtf8) return Expectation.nonUtf8Error;
|
|
if (truncatedOutput) return Expectation.truncatedOutput;
|
|
|
|
// If it's a static error test, validate the exact errors.
|
|
if (testCase.testFile.isStaticErrorTest) {
|
|
return _validateExpectedErrors(testCase);
|
|
}
|
|
|
|
return super.result(testCase);
|
|
}
|
|
|
|
/// A subclass should override this to parse the command's output for any
|
|
/// reported errors and warnings.
|
|
///
|
|
/// It should read [stderr] and [stdout] and call [addError] and [addWarning].
|
|
void _parseErrors();
|
|
|
|
void addError(StaticError error) {
|
|
_errors.add(error);
|
|
}
|
|
|
|
void addWarning(StaticError error) {
|
|
_warnings.add(error);
|
|
}
|
|
|
|
/// Compare the actual errors produced to the expected static errors parsed
|
|
/// from the test file.
|
|
///
|
|
/// Returns [Expectation.pass] if all expected errors were correctly
|
|
/// reported.
|
|
///
|
|
/// If [writer] is given, outputs a description of any error mismatches.
|
|
Expectation _validateExpectedErrors(
|
|
TestCase testCase, [
|
|
OutputWriter? writer,
|
|
]) {
|
|
// Filter out errors that aren't for this configuration.
|
|
var errorSource = const {
|
|
Compiler.dart2analyzer: ErrorSource.analyzer,
|
|
Compiler.dart2js: ErrorSource.web,
|
|
Compiler.dart2wasm: ErrorSource.web,
|
|
Compiler.ddc: ErrorSource.web,
|
|
Compiler.fasta: ErrorSource.cfe,
|
|
Compiler.specParser: ErrorSource.specParser,
|
|
}[testCase.configuration.compiler]!;
|
|
|
|
var expected = testCase.testFile.expectedErrors.where(
|
|
(error) => error.source == errorSource,
|
|
);
|
|
|
|
var validation = StaticError.validateExpectations(expected, [
|
|
...errors,
|
|
...warnings,
|
|
]);
|
|
if (validation == null) return Expectation.pass;
|
|
|
|
writer?.subsection("static error failures");
|
|
writer?.write(validation);
|
|
|
|
return Expectation.missingCompileTimeError;
|
|
}
|
|
}
|
|
|
|
mixin _UnittestSuiteMessagesMixin {
|
|
bool _isAsyncTest(String testOutput) {
|
|
return testOutput.contains("unittest-suite-wait-for-done");
|
|
}
|
|
|
|
bool _isAsyncTestSuccessful(String testOutput) {
|
|
return testOutput.contains("unittest-suite-success");
|
|
}
|
|
}
|
|
|
|
void _deobfuscateAndWriteStack(String stack, OutputWriter output) {
|
|
try {
|
|
var deobfuscatedStack = deobfuscateStackTrace(stack);
|
|
if (deobfuscatedStack == stack) return;
|
|
output.subsection('Deobfuscated error and stack');
|
|
output.write(deobfuscatedStack);
|
|
} catch (e, st) {
|
|
output.subsection('Warning: not able to deobfuscate stack');
|
|
output.writeAll(['input: $stack', 'error: $e', 'stack trace: $st']);
|
|
return;
|
|
}
|
|
}
|