980cf725ff
Based on abandoned change #264261. Changes to that PR: compare and show file paths, improve part parsing, include analyzer expectations from other files as well as CFE expectations, update unit tests. Add end to end tests. The simplest case "library_failure_test.dart" passes before this PR, all others require the change in this PR to pick up expectations in other files. Change-Id: Ia71b78a8f0dced83f603309877132f261b47c5a3 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/345541 Reviewed-by: Erik Ernst <eernst@google.com> Reviewed-by: Bob Nystrom <rnystrom@google.com> Commit-Queue: Morgan :) <davidmorgan@google.com>
371 lines
12 KiB
Dart
371 lines
12 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 file.
|
|
|
|
/// Regenerates the static error test markers inside static error tests based
|
|
/// on the actual errors reported by analyzer and CFE.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:glob/glob.dart';
|
|
import 'package:glob/list_local_fs.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:test_runner/src/command_output.dart';
|
|
import 'package:test_runner/src/feature.dart' show Feature;
|
|
import 'package:test_runner/src/path.dart';
|
|
import 'package:test_runner/src/static_error.dart';
|
|
import 'package:test_runner/src/test_file.dart';
|
|
import 'package:test_runner/src/update_errors.dart';
|
|
|
|
const _usage =
|
|
"Usage: dart update_static_error_tests.dart [flags...] <path glob>";
|
|
|
|
final _dartPath = _findBinary("dart", "exe");
|
|
final _analyzerPath = p.join('pkg', 'analyzer_cli', 'bin', 'analyzer.dart');
|
|
|
|
Future<void> main(List<String> args) async {
|
|
var sources = ErrorSource.all.map((e) => e.marker).toList();
|
|
|
|
var parser = ArgParser();
|
|
|
|
parser.addFlag("help", abbr: "h");
|
|
|
|
parser.addFlag("dry-run",
|
|
abbr: "n",
|
|
help: "Print result but do not overwrite any files.",
|
|
negatable: false);
|
|
parser.addFlag("context",
|
|
abbr: "c", help: "Include context messages in output.");
|
|
|
|
parser.addSeparator("What operations to perform:");
|
|
parser.addFlag("remove-all",
|
|
abbr: "r",
|
|
help: "Remove all existing error expectations.",
|
|
negatable: false);
|
|
parser.addMultiOption("remove",
|
|
help: "Remove error expectations for given front ends.",
|
|
allowed: sources);
|
|
parser.addFlag("insert-all",
|
|
abbr: "i",
|
|
help: "Insert error expectations for all front ends.",
|
|
negatable: false);
|
|
parser.addMultiOption("insert",
|
|
help: "Insert error expectations from given front ends.",
|
|
allowed: sources);
|
|
parser.addFlag("update-all",
|
|
abbr: "u",
|
|
help: "Replace error expectations for all front ends.",
|
|
negatable: false);
|
|
parser.addMultiOption("update",
|
|
help: "Update error expectations for given front ends.",
|
|
allowed: sources);
|
|
|
|
var results = parser.parse(args);
|
|
|
|
if (results["help"] as bool) {
|
|
print("Regenerates the test markers inside static error tests.");
|
|
print("");
|
|
print(_usage);
|
|
print("");
|
|
print(parser.usage);
|
|
exit(0);
|
|
}
|
|
|
|
var dryRun = results["dry-run"] as bool;
|
|
|
|
var includeContext = results["context"] as bool;
|
|
|
|
var removeSources = <ErrorSource>{};
|
|
var insertSources = <ErrorSource>{};
|
|
|
|
for (var source in results["remove"] as List<String>) {
|
|
removeSources.add(ErrorSource.find(source)!);
|
|
}
|
|
|
|
for (var source in results["insert"] as List<String>) {
|
|
insertSources.add(ErrorSource.find(source)!);
|
|
}
|
|
|
|
for (var source in results["update"] as List<String>) {
|
|
removeSources.add(ErrorSource.find(source)!);
|
|
insertSources.add(ErrorSource.find(source)!);
|
|
}
|
|
|
|
if (results["remove-all"] as bool) {
|
|
removeSources.addAll(ErrorSource.all);
|
|
}
|
|
|
|
if (results["insert-all"] as bool) {
|
|
insertSources.addAll(ErrorSource.all);
|
|
}
|
|
|
|
if (results["update-all"] as bool) {
|
|
removeSources.addAll(ErrorSource.all);
|
|
insertSources.addAll(ErrorSource.all);
|
|
}
|
|
|
|
if (removeSources.isEmpty && insertSources.isEmpty) {
|
|
_usageError(
|
|
parser, "Must provide at least one flag for an operation to perform.");
|
|
}
|
|
|
|
var processedFiles = 0;
|
|
var skippedMultitests = <String>[];
|
|
|
|
for (var result in results.rest) {
|
|
// Allow tests to be specified without the extension for compatibility with
|
|
// the regular test runner syntax.
|
|
if (!result.endsWith(".dart")) {
|
|
result += ".dart";
|
|
}
|
|
|
|
// Allow tests to be specified either relative to the "tests" directory
|
|
// or relative to the current directory.
|
|
var root = result.startsWith("tests") ? "." : "tests";
|
|
var glob = Glob(result, recursive: true);
|
|
for (var entry in glob
|
|
.listSync(root: root)
|
|
.whereType<File>()
|
|
.where((file) => file.path.endsWith('.dart'))) {
|
|
var processed = await _processFile(
|
|
entry,
|
|
dryRun: dryRun,
|
|
includeContext: includeContext,
|
|
remove: removeSources,
|
|
insert: insertSources,
|
|
);
|
|
|
|
if (processed) {
|
|
processedFiles++;
|
|
} else {
|
|
skippedMultitests.add(entry.path);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (skippedMultitests.isNotEmpty) {
|
|
// If no files were successfully processed, then the user is only pointing
|
|
// it at multitests and made a mistake.
|
|
if (processedFiles == 0) {
|
|
stderr.writeln("Error: This tool doesn't support updating static errors "
|
|
"in multitests. Couldn't update:");
|
|
} else {
|
|
stderr.writeln("Did not update the following multitests:");
|
|
}
|
|
|
|
for (var multitest in skippedMultitests) {
|
|
stderr.writeln(p.normalize(multitest));
|
|
}
|
|
}
|
|
}
|
|
|
|
void _usageError(ArgParser parser, String message) {
|
|
stderr.writeln(message);
|
|
stderr.writeln();
|
|
stderr.writeln(_usage);
|
|
stderr.writeln(parser.usage);
|
|
exit(64);
|
|
}
|
|
|
|
Future<bool> _processFile(File file,
|
|
{required bool dryRun,
|
|
required bool includeContext,
|
|
required Set<ErrorSource> remove,
|
|
required Set<ErrorSource> insert}) async {
|
|
var testFile = TestFile.read(Path("."), file.absolute.path);
|
|
// Canonicalize the path in the same way StaticError does, so it matches.
|
|
file = File(p.relative(file.path, from: Directory.current.path));
|
|
|
|
// Don't process multitests. The multitest file isn't necessarily a valid or
|
|
// meaningful Dart file that can be processed by front ends. To process them,
|
|
// we'd have to split the multitest, run each separate file on the front ends,
|
|
// and then try to merge the results back together.
|
|
//
|
|
// In practice, a test should either be a multitest or a static error test,
|
|
// but not both.
|
|
if (testFile.isMultitest) return false;
|
|
|
|
stdout.write("${file.path}...");
|
|
|
|
var experiments = [
|
|
if (testFile.experiments.isNotEmpty) ...testFile.experiments
|
|
];
|
|
|
|
var options = [
|
|
...testFile.sharedOptions,
|
|
if (experiments.isNotEmpty) "--enable-experiment=${experiments.join(',')}"
|
|
];
|
|
|
|
var errors = <StaticError>[];
|
|
if (insert.contains(ErrorSource.analyzer)) {
|
|
stdout.write("\r${file.path} (Running analyzer...)");
|
|
errors.addAll(await runAnalyzer(file, options));
|
|
}
|
|
|
|
// If we're inserting web errors, we also need to gather the CFE errors to
|
|
// tell which web errors are web-specific.
|
|
final cfeErrors = <StaticError>[];
|
|
if (insert.contains(ErrorSource.cfe) || insert.contains(ErrorSource.web)) {
|
|
var cfeOptions = [
|
|
if (testFile.requirements.contains(Feature.nnbdWeak)) "--nnbd-weak",
|
|
if (testFile.requirements.contains(Feature.nnbdStrong)) "--nnbd-strong",
|
|
...options
|
|
];
|
|
// Clear the previous line.
|
|
stdout.write("\r${file.path} ");
|
|
stdout.write("\r${file.path} (Running CFE...)");
|
|
cfeErrors.addAll(await runCfe(file, cfeOptions));
|
|
if (insert.contains(ErrorSource.cfe)) {
|
|
errors.addAll(cfeErrors);
|
|
}
|
|
}
|
|
|
|
if (insert.contains(ErrorSource.web)) {
|
|
// Clear the previous line.
|
|
stdout.write("\r${file.path} ");
|
|
stdout.write("\r${file.path} (Running dart2js...)");
|
|
errors.addAll(await runDart2js(file, options, cfeErrors));
|
|
}
|
|
|
|
// Error expectations can be in imported or part files: iterate over the set
|
|
// of paths that is the main file path plus all paths mentioned in
|
|
// expectations, updating them.
|
|
for (final path in {file.path, ...errors.map((e) => e.path)}) {
|
|
final pathErrors = errors.where((e) => e.path == path).toList();
|
|
var result = updateErrorExpectations(
|
|
path, File(path).readAsStringSync(), pathErrors,
|
|
remove: remove, includeContext: includeContext);
|
|
|
|
stdout.writeln("\r$path (Updated with ${pathErrors.length} errors)");
|
|
|
|
if (dryRun) {
|
|
print(result);
|
|
} else {
|
|
await File(path).writeAsString(result);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Invoke analyzer on [file] and gather all static errors it reports.
|
|
Future<List<StaticError>> runAnalyzer(File file, List<String> options) async {
|
|
// TODO(rnystrom): Running the analyzer command line each time is very slow.
|
|
// Either import the analyzer as a library, or at least invoke it in a batch
|
|
// mode.
|
|
var result = await Process.run(_dartPath, [
|
|
_analyzerPath,
|
|
...options,
|
|
"--format=json",
|
|
file.absolute.path,
|
|
]);
|
|
|
|
// Analyzer returns 3 when it detects errors, 2 when it detects
|
|
// warnings and --fatal-warnings is enabled, 1 when it detects
|
|
// hints and --fatal-hints or --fatal-infos are enabled.
|
|
if (result.exitCode < 0 || result.exitCode > 3) {
|
|
print("Analyzer run failed: ${result.stdout}\n${result.stderr}");
|
|
print("Error: failed to update ${file.path}");
|
|
return const [];
|
|
}
|
|
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
AnalysisCommandOutput.parseErrors(result.stdout as String, errors, warnings);
|
|
return [...errors, ...warnings];
|
|
}
|
|
|
|
/// Invoke CFE on [file] and gather all static errors it reports.
|
|
Future<List<StaticError>> runCfe(File file, List<String> options) async {
|
|
// TODO(rnystrom): Running the CFE command line each time is slow and wastes
|
|
// time generating code, which we don't care about. Import it as a library or
|
|
// at least run it in batch mode.
|
|
var result = await Process.run(_dartPath, [
|
|
"pkg/front_end/tool/_fasta/compile.dart",
|
|
...options,
|
|
"--verify",
|
|
"-o",
|
|
"dev:null", // Output is only created for file URIs.
|
|
file.absolute.path,
|
|
]);
|
|
|
|
// Running the above command may generate a dill file next to the test, which
|
|
// we don't want, so delete it if present.
|
|
var dill = File("${file.absolute.path}.dill");
|
|
if (await dill.exists()) {
|
|
await dill.delete();
|
|
}
|
|
if (result.exitCode != 0) {
|
|
print("CFE run failed: ${result.stdout}\n${result.stderr}");
|
|
print("Error: failed to update ${file.path}");
|
|
return const [];
|
|
}
|
|
var errors = <StaticError>[];
|
|
var warnings = <StaticError>[];
|
|
FastaCommandOutput.parseErrors(result.stdout as String, errors, warnings);
|
|
return [...errors, ...warnings];
|
|
}
|
|
|
|
/// Invoke dart2js on [file] and gather all static errors it reports.
|
|
Future<List<StaticError>> runDart2js(
|
|
File file, List<String> options, List<StaticError> cfeErrors) async {
|
|
var result = await Process.run(_dartPath, [
|
|
'compile',
|
|
'js',
|
|
...options,
|
|
"-o",
|
|
"dev:null", // Output is only created for file URIs.
|
|
file.absolute.path,
|
|
]);
|
|
|
|
var errors = <StaticError>[];
|
|
Dart2jsCompilerCommandOutput.parseErrors(result.stdout as String, errors);
|
|
|
|
// We only want the web-specific errors from dart2js, so filter out any errors
|
|
// that are also reported by the CFE.
|
|
errors.removeWhere((dart2jsError) {
|
|
return cfeErrors.any((cfeError) {
|
|
return dart2jsError.line == cfeError.line &&
|
|
dart2jsError.column == cfeError.column &&
|
|
dart2jsError.length == cfeError.length &&
|
|
dart2jsError.message == cfeError.message;
|
|
});
|
|
});
|
|
|
|
return errors;
|
|
}
|
|
|
|
/// Find the most recently-built binary [name] in any of the build directories.
|
|
String _findBinary(String name, String windowsExtension) {
|
|
var binary = Platform.isWindows ? "$name.$windowsExtension" : name;
|
|
|
|
String? newestPath;
|
|
DateTime? newestTime;
|
|
|
|
var buildDirectory = Directory(Platform.isMacOS ? "xcodebuild" : "out");
|
|
if (buildDirectory.existsSync()) {
|
|
for (var config in buildDirectory.listSync()) {
|
|
var path = p.join(config.path, "dart-sdk", "bin", binary);
|
|
var file = File(path);
|
|
if (!file.existsSync()) continue;
|
|
var modified = file.lastModifiedSync();
|
|
|
|
if (newestTime == null || modified.isAfter(newestTime)) {
|
|
newestPath = path;
|
|
newestTime = modified;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newestPath == null) {
|
|
// Clear the current line since we're in the middle of a progress line.
|
|
print("");
|
|
print("Could not find a built SDK with a $binary to run.");
|
|
print("Make sure to build the Dart SDK before running this tool.");
|
|
exit(1);
|
|
}
|
|
|
|
return newestPath;
|
|
}
|