Files
sdk/pkg/front_end/test/unit_test_suites_impl.dart
T
Jens Johansen 69f6378ed5 [CFE] Make unit_test_suites log better message when test starts to pass
In for instance https://dart-review.googlesource.com/c/sdk/+/193961/5
we had an instance of something good happening (a crashing-ish test
that started to pass) but where the author was told no (red bot) and
not given a path to make the bot happy (bad messaging).

Specifically tests failed and the best log one got was

```
To re-run this test, run:
    dart pkg/front_end/test/unit_test_suites.dart -p pkg/front_end/test/fasta/textual_outline/general/constants/js_semantics/issue45376
```

which isn't very helpful.

If running it locally a little more information is given via stdout,
but having to do that isn't great (and the little extra information
still wasn't great).

This CL updates the messaging to - hopefully - give a path to make the
bot happy. The messaging will now - directly from the log
on the bot - be

```
FAILED: pkg/front_end/test/fasta/textual_outline/general/constants/js_semantics/issue45376: Pass

To re-run this test, run:

   dart pkg/front_end/test/unit_test_suites.dart -p pkg/front_end/test/fasta/textual_outline/general/constants/js_semantics/issue45376

The test passed, but wasn't expected to. You should update the status file for this test.
There's a status entry looking something like

  general/constants/js_semantics/issue45376: FormatterCrash

which should be removed.

The status file is file:///[wherever]/sdk/pkg/front_end/testcases/textual_outline.status.
```

which
a) gives the extra information previously only available via stdout when
   running locally (the first line).
b) Gives an explanation as to why it failed
   ("The test passed, but wasn't expected to.").
c) A path to make the bot green ("You should update the status file for
   this test. [...] should be removed").

Hopefully this will make it easier to deal with in the future when
something good happens.

Change-Id: I838699abce580334c956d8b4753884fd600802ae
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/194242
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
2021-04-07 12:25:27 +00:00

635 lines
21 KiB
Dart

// Copyright (c) 2019, 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.md file.
// ignore_for_file: import_of_legacy_library_into_null_safe
import 'dart:async' show Timer;
import 'dart:convert' show jsonEncode;
import 'dart:io' show File, Platform, exitCode;
import 'dart:isolate' show Isolate, ReceivePort, SendPort;
import 'package:args/args.dart' show ArgParser;
import 'package:testing/src/chain.dart' show CreateContext, Result, Step;
import 'package:testing/src/expectation.dart' show Expectation;
import 'package:testing/src/log.dart' show Logger;
import 'package:testing/src/run.dart' show runMe;
import 'package:testing/src/suite.dart' as testing show Suite;
import 'package:testing/src/test_description.dart' show TestDescription;
import 'fasta/expression_suite.dart' as expression show createContext;
import 'fasta/incremental_dartino_suite.dart' as incremental_dartino
show createContext;
import 'fasta/messages_suite.dart' as messages show createContext;
import 'fasta/outline_suite.dart' as outline show createContext;
import 'fasta/strong_suite.dart' as strong show createContext;
import 'fasta/text_serialization_suite.dart' as text_serialization
show createContext;
import 'fasta/textual_outline_suite.dart' as textual_outline show createContext;
import 'fasta/weak_suite.dart' as weak show createContext;
import 'incremental_bulk_compiler_smoke_suite.dart' as incremental_bulk_compiler
show createContext;
import 'incremental_suite.dart' as incremental show createContext;
import 'lint_suite.dart' as lint show createContext;
import 'parser_suite.dart' as parser show createContext;
import 'parser_all_suite.dart' as parserAll show createContext;
import 'spelling_test_not_src_suite.dart' as spelling_not_src
show createContext;
import 'spelling_test_src_suite.dart' as spelling_src show createContext;
const suiteNamePrefix = "pkg/front_end/test";
int getDefaultThreads() {
int numberOfWorkers = 1;
if (Platform.numberOfProcessors > 2) {
numberOfWorkers = Platform.numberOfProcessors - 1;
}
return numberOfWorkers;
}
class Options {
final String configurationName;
final bool verbose;
final bool printFailureLog;
final Uri outputDirectory;
final String? testFilter;
final List<String> environmentOptions;
final int shardCount;
final int shard;
final bool skipTestsThatRequireGit;
final bool onlyTestsThatRequireGit;
final int numberOfWorkers;
Options(
this.configurationName,
this.verbose,
this.printFailureLog,
this.outputDirectory,
this.testFilter,
this.environmentOptions, {
required this.shardCount,
required this.shard,
required this.skipTestsThatRequireGit,
required this.onlyTestsThatRequireGit,
required this.numberOfWorkers,
});
static Options parse(List<String> args) {
var parser = new ArgParser()
..addOption("named-configuration",
abbr: "n",
help: "configuration name to use for emitting json result files")
..addOption("output-directory",
help: "directory to which results.json and logs.json are written")
..addFlag("verbose",
abbr: "v", help: "print additional information", defaultsTo: false)
..addFlag("print",
abbr: "p", help: "print failure logs", defaultsTo: false)
..addMultiOption('environment',
abbr: 'D', help: "environment options for the test suite")
..addOption("tasks",
abbr: "j",
help: "The number of parallel tasks to run.",
defaultsTo: "${getDefaultThreads()}")
..addOption("shards", help: "Number of shards", defaultsTo: "1")
..addOption("shard", help: "Which shard to run", defaultsTo: "1")
..addFlag("skipTestsThatRequireGit",
help: "Whether to skip tests that require git to run",
defaultsTo: false)
..addFlag("onlyTestsThatRequireGit",
help: "Whether to only run tests that require git",
defaultsTo: false);
var parsedArguments = parser.parse(args);
String outputPath = parsedArguments["output-directory"] ?? ".";
Uri outputDirectory = Uri.base.resolveUri(Uri.directory(outputPath));
bool verbose = parsedArguments["verbose"];
String? filter;
if (parsedArguments.rest.length == 1) {
filter = parsedArguments.rest.single;
if (filter.startsWith("$suiteNamePrefix/")) {
filter = filter.substring(suiteNamePrefix.length + 1);
}
}
String tasksString = parsedArguments["tasks"];
int? tasks = int.tryParse(tasksString);
if (tasks == null || tasks < 1) {
throw "--tasks (-j) has to be an integer >= 1";
}
String shardsString = parsedArguments["shards"];
int? shardCount = int.tryParse(shardsString);
if (shardCount == null || shardCount < 1) {
throw "--shards has to be an integer >= 1";
}
String shardString = parsedArguments["shard"];
int? shard = int.tryParse(shardString);
if (shard == null || shard < 1 || shard > shardCount) {
throw "--shard has to be an integer >= 1 (and <= --shards)";
}
bool skipTestsThatRequireGit = parsedArguments["skipTestsThatRequireGit"];
bool onlyTestsThatRequireGit = parsedArguments["onlyTestsThatRequireGit"];
if (skipTestsThatRequireGit && onlyTestsThatRequireGit) {
throw "Only one of --skipTestsThatRequireGit and "
"--onlyTestsThatRequireGit can be provided.";
}
if (verbose) {
print("NOTE: Created with options\n "
"${parsedArguments["named-configuration"]},\n "
"${verbose},\n "
"${parsedArguments["print"]},\n "
"${outputDirectory},\n "
"${filter},\n "
"${parsedArguments['environment']},\n "
"shardCount: ${shardCount},\n "
"shard: ${shard - 1 /* make it 0-indexed */},\n "
"onlyTestsThatRequireGit: ${onlyTestsThatRequireGit},\n "
"skipTestsThatRequireGit: ${skipTestsThatRequireGit},\n "
"numberOfWorkers: ${tasks}");
}
return Options(
parsedArguments["named-configuration"],
verbose,
parsedArguments["print"],
outputDirectory,
filter,
parsedArguments['environment'],
shardCount: shardCount,
shard: shard - 1 /* make it 0-indexed */,
onlyTestsThatRequireGit: onlyTestsThatRequireGit,
skipTestsThatRequireGit: skipTestsThatRequireGit,
numberOfWorkers: tasks,
);
}
}
class ResultLogger implements Logger {
final String prefix;
final bool verbose;
final bool printFailureLog;
final SendPort resultsPort;
final SendPort logsPort;
final Map<String, Stopwatch> stopwatches = {};
final String configurationName;
final Set<String> seenTests = {};
bool gotFrameworkError = false;
ResultLogger(this.prefix, this.resultsPort, this.logsPort, this.verbose,
this.printFailureLog, this.configurationName);
String getTestName(TestDescription description) {
return "$prefix/${description.shortName}";
}
@override
void logMessage(Object message) {}
@override
void logNumberedLines(String text) {}
@override
void logProgress(String message) {}
@override
void logStepComplete(int completed, int failed, int total,
testing.Suite suite, TestDescription description, Step step) {}
@override
void logStepStart(int completed, int failed, int total, testing.Suite suite,
TestDescription description, Step step) {}
@override
void logSuiteStarted(testing.Suite suite) {}
@override
void logSuiteComplete(testing.Suite suite) {}
handleTestResult(
testing.Suite suite,
TestDescription testDescription,
Result result,
String fullSuiteName,
bool matchedExpectations,
Set<Expectation> expectedOutcomes) {
String testName = getTestName(testDescription);
String suiteName = "pkg";
String shortTestName = testName.substring(suiteName.length + 1);
resultsPort.send(jsonEncode({
"name": testName,
"configuration": configurationName,
"suite": suiteName,
"test_name": shortTestName,
"time_ms": stopwatches[testName]!.elapsedMilliseconds,
"expected": "Pass",
"result": matchedExpectations ? "Pass" : "Fail",
"matches": matchedExpectations,
}));
if (!matchedExpectations) {
StringBuffer sb = new StringBuffer();
if (printFailureLog) {
String outcome = "${result.outcome}";
sb.write("FAILED: $testName: $outcome");
}
sb.write(result.log);
if (result.error != null) {
sb.write("\n\n${result.error}");
}
if (result.trace != null) {
sb.write("\n\n${result.trace}");
}
sb.write("\n\nTo re-run this test, run:");
sb.write("\n\n dart pkg/front_end/test/unit_test_suites.dart -p "
"$testName");
if (result.autoFixCommand != null) {
sb.write("\n\nTo automatically update the test expectations, run:");
sb.write("\n\n dart pkg/front_end/test/unit_test_suites.dart -p "
"$testName -D${result.autoFixCommand}");
if (result.canBeFixWithUpdateExpectations) {
sb.write('\n\nTo update test expectations for all tests at once, '
'run:');
sb.write('\n\n dart pkg/front_end/tool/update_expectations.dart');
sb.write('\n\nNote that this takes a long time and should only be '
'used when many tests need updating.\n');
}
}
if (result.outcome == Expectation.Pass) {
String expectedString =
expectedOutcomes.map((e) => e.toString()).join(", ");
sb.write("\n\nThe test passed, but wasn't expected to. "
"You should update the status file for this test."
"\nThere's a status entry looking something like"
"\n\n ${testDescription.shortName}: ${expectedString}"
"\n\nwhich should be removed."
"\n\nThe status file is ${suite.statusFile}.");
} else if (result.autoFixCommand == null) {
String expectedString =
expectedOutcomes.map((e) => e.toString()).join(", ");
sb.write("\n\nThe test has outcome ${result.outcome}, "
"but was expected to have outcome(s) ${expectedOutcomes}. "
"You might have to update the status file to the new outcome"
"\nThere's a status entry looking something like"
"\n\n ${testDescription.shortName}: ${expectedString}"
"\n\nwhich should be updated."
"\n\nThe status file is ${suite.statusFile}.");
}
String failureLog = sb.toString();
String outcome = "${result.outcome}";
logsPort.send(jsonEncode({
"name": testName,
"configuration": configurationName,
"result": outcome,
"log": failureLog,
}));
if (printFailureLog) {
print(failureLog);
}
}
if (verbose) {
String result = matchedExpectations ? "PASS" : "FAIL";
print("${testName}: ${result}");
}
}
void logTestStart(int completed, int failed, int total, testing.Suite suite,
TestDescription description) {
String name = getTestName(description);
stopwatches[name] = Stopwatch()..start();
}
@override
void logTestComplete(int completed, int failed, int total,
testing.Suite suite, TestDescription description) {}
@override
void logUncaughtError(error, StackTrace stackTrace) {}
void logExpectedResult(testing.Suite suite, TestDescription description,
Result result, Set<Expectation> expectedOutcomes) {
handleTestResult(
suite, description, result, prefix, true, expectedOutcomes);
}
@override
void logUnexpectedResult(testing.Suite suite, TestDescription description,
Result result, Set<Expectation> expectedOutcomes) {
// The test framework (pkg/testing) calls the logger with an unexpected
// results a second time to create a summary. We ignore the second call
// here.
String testName = getTestName(description);
if (seenTests.contains(testName)) return;
seenTests.add(testName);
handleTestResult(
suite, description, result, prefix, false, expectedOutcomes);
}
@override
void noticeFrameworkCatchError(error, StackTrace stackTrace) {
gotFrameworkError = true;
}
}
class Suite {
final CreateContext createContext;
final String testingRootPath;
final String? path;
final int shardCount;
final String prefix;
final bool requiresGit;
const Suite(
this.prefix,
this.createContext,
this.testingRootPath, {
required this.shardCount,
this.path,
this.requiresGit: false,
});
}
const List<Suite> suites = [
const Suite(
"fasta/expression",
expression.createContext,
"../../testing.json",
shardCount: 1,
),
const Suite(
"fasta/outline",
outline.createContext,
"../../testing.json",
shardCount: 2,
),
const Suite(
"fasta/incremental_dartino",
incremental_dartino.createContext,
"../../testing.json",
shardCount: 1,
),
const Suite(
"fasta/messages",
messages.createContext,
"../../testing.json",
shardCount: 1,
requiresGit: true,
),
const Suite(
"fasta/text_serialization",
text_serialization.createContext,
"../../testing.json",
path: "fasta/text_serialization_suite.dart",
shardCount: 10,
),
const Suite(
"fasta/strong",
strong.createContext,
"../../testing.json",
path: "fasta/strong_suite.dart",
shardCount: 2,
),
const Suite(
"incremental_bulk_compiler_smoke",
incremental_bulk_compiler.createContext,
"../testing.json",
shardCount: 1,
),
const Suite(
"incremental",
incremental.createContext,
"../testing.json",
shardCount: 2,
),
const Suite(
"lint",
lint.createContext,
"../testing.json",
shardCount: 1,
requiresGit: true,
),
const Suite(
"parser",
parser.createContext,
"../testing.json",
shardCount: 1,
),
const Suite(
"parser_all",
parserAll.createContext,
"../testing.json",
shardCount: 4,
requiresGit:
true /* technically not true, but tests *many* more files
than in test_matrix.json file set */
,
),
const Suite(
"spelling_test_not_src",
spelling_not_src.createContext,
"../testing.json",
shardCount: 1,
requiresGit: true,
),
const Suite(
"spelling_test_src",
spelling_src.createContext,
"../testing.json",
shardCount: 1,
requiresGit: true,
),
const Suite(
"fasta/weak",
weak.createContext,
"../../testing.json",
path: "fasta/weak_suite.dart",
shardCount: 10,
),
const Suite(
"fasta/textual_outline",
textual_outline.createContext,
"../../testing.json",
shardCount: 1,
),
];
const Duration timeoutDuration = Duration(minutes: 30);
class SuiteConfiguration {
final Suite suite;
final SendPort resultsPort;
final SendPort logsPort;
final bool verbose;
final bool printFailureLog;
final String configurationName;
final String? testFilter;
final List<String> environmentOptions;
final int shard;
const SuiteConfiguration(
this.suite,
this.resultsPort,
this.logsPort,
this.verbose,
this.printFailureLog,
this.configurationName,
this.testFilter,
this.environmentOptions,
this.shard,
);
}
void runSuite(SuiteConfiguration configuration) async {
Suite suite = configuration.suite;
String name = suite.prefix;
String fullSuiteName = "$suiteNamePrefix/$name";
Uri suiteUri = Platform.script.resolve(suite.path ?? "${name}_suite.dart");
if (!new File.fromUri(suiteUri).existsSync()) {
throw "File doesn't exist: $suiteUri";
}
ResultLogger logger = ResultLogger(
fullSuiteName,
configuration.resultsPort,
configuration.logsPort,
configuration.verbose,
configuration.printFailureLog,
configuration.configurationName);
await runMe(
<String>[
if (configuration.testFilter != null) configuration.testFilter!,
for (String option in configuration.environmentOptions) '-D${option}',
],
suite.createContext,
me: suiteUri,
configurationPath: suite.testingRootPath,
logger: logger,
shards: suite.shardCount,
shard: configuration.shard,
);
if (logger.gotFrameworkError) {
throw "Got framework error!";
}
}
Future<void> writeLinesToFile(Uri uri, List<String> lines) async {
await File.fromUri(uri).writeAsString(lines.map((line) => "$line\n").join());
}
main([List<String> arguments = const <String>[]]) async {
Stopwatch totalRuntime = new Stopwatch()..start();
List<String> results = [];
List<String> logs = [];
Options options = Options.parse(arguments);
ReceivePort resultsPort = new ReceivePort()
..listen((resultEntry) => results.add(resultEntry));
ReceivePort logsPort = new ReceivePort()
..listen((logEntry) => logs.add(logEntry));
List<Future<bool>> futures = [];
if (options.verbose) {
print("NOTE: Willing to run with ${options.numberOfWorkers} 'workers'");
print("");
}
int numberOfFreeWorkers = options.numberOfWorkers;
// Run test suites and record the results and possible failure logs.
int chunkNum = 0;
for (Suite suite in suites) {
if (options.onlyTestsThatRequireGit && !suite.requiresGit) continue;
if (options.skipTestsThatRequireGit && suite.requiresGit) continue;
String prefix = suite.prefix;
String? filter = options.testFilter;
if (filter != null) {
// Skip suites that are not hit by the test filter, is there is one.
if (!filter.startsWith(prefix)) {
continue;
}
// Remove the 'fasta/' from filters, if there, because it is not used
// in the name defined in testing.json.
if (filter.startsWith("fasta/")) {
filter = filter.substring("fasta/".length);
}
}
for (int shard = 0; shard < suite.shardCount; shard++) {
if (chunkNum++ % options.shardCount != options.shard) continue;
while (numberOfFreeWorkers <= 0) {
// This might not be great design, but it'll work fine.
await Future.delayed(const Duration(milliseconds: 50));
}
numberOfFreeWorkers--;
// Start the test suite in a new isolate.
ReceivePort exitPort = new ReceivePort();
ReceivePort errorPort = new ReceivePort();
SuiteConfiguration configuration = new SuiteConfiguration(
suite,
resultsPort.sendPort,
logsPort.sendPort,
options.verbose,
options.printFailureLog,
options.configurationName,
filter,
options.environmentOptions,
shard);
Future<bool> future = new Future<bool>(() async {
try {
Stopwatch stopwatch = new Stopwatch()..start();
String naming = "$prefix";
if (suite.shardCount > 1) {
naming += " (${shard + 1} of ${suite.shardCount})";
}
print("Running suite $naming");
Isolate isolate = await Isolate.spawn<SuiteConfiguration>(
runSuite, configuration,
onExit: exitPort.sendPort, onError: errorPort.sendPort);
bool timedOutOrCrash = false;
Timer timer = new Timer(timeoutDuration, () {
timedOutOrCrash = true;
print("Suite $naming timed out after "
"${timeoutDuration.inMilliseconds}ms");
isolate.kill(priority: Isolate.immediate);
});
await exitPort.first;
errorPort.close();
bool gotError = !await errorPort.isEmpty;
if (gotError) {
timedOutOrCrash = true;
}
timer.cancel();
if (!timedOutOrCrash) {
int seconds = stopwatch.elapsedMilliseconds ~/ 1000;
print("Suite $naming finished (took ${seconds} seconds)");
}
return timedOutOrCrash;
} finally {
numberOfFreeWorkers++;
}
});
futures.add(future);
}
}
// Wait for isolates to terminate and clean up.
Iterable<bool> timeouts = await Future.wait(futures);
resultsPort.close();
logsPort.close();
// Write results.json and logs.json.
Uri resultJsonUri = options.outputDirectory.resolve("results.json");
Uri logsJsonUri = options.outputDirectory.resolve("logs.json");
await writeLinesToFile(resultJsonUri, results);
await writeLinesToFile(logsJsonUri, logs);
print("Log files written to ${resultJsonUri.toFilePath()} and"
" ${logsJsonUri.toFilePath()}");
print("Entire run took ${totalRuntime.elapsed}.");
// Return with exit code 1 if at least one suite timed out.
bool timeout = timeouts.any((timeout) => timeout);
if (timeout) {
exitCode = 1;
} else {
// The testing framework (package:testing) sets the exitCode to `1` if any
// test failed, so we reset it here to indicate that the test runner was
// successful.
exitCode = 0;
}
}