f8b363a335
Change-Id: I24016d9a827e0506b2d5a6b6eed3356afaf90dbd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500602 Reviewed-by: Bob Nystrom <rnystrom@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
360 lines
12 KiB
Dart
360 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.
|
|
|
|
/// Converts a multi-test to a test using the new static error test framework
|
|
/// (see https://github.com/dart-lang/sdk/tree/main/docs/Testing.md#static-error-tests)
|
|
/// and a copy of the '/none' test.
|
|
library;
|
|
|
|
import 'dart:collection';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:test_runner/src/command_output.dart';
|
|
import 'package:test_runner/src/multitest.dart';
|
|
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';
|
|
|
|
import 'update_static_error_tests.dart' show dartPath;
|
|
|
|
final _analyzerPath = p.join('pkg', 'analyzer_cli', 'bin', 'analyzer.dart');
|
|
|
|
Future<List<StaticError>> getErrors(
|
|
List<String> options,
|
|
String filePath,
|
|
) async {
|
|
var analyzerErrors = await _runAnalyzer(File(filePath), options);
|
|
var cfeErrors = await _runCfe(File(filePath), options);
|
|
return [...analyzerErrors, ...cfeErrors];
|
|
}
|
|
|
|
bool areSameErrors(List<StaticError> first, List<StaticError> second) {
|
|
if (first.length != second.length) return false;
|
|
for (var i = 0; i < first.length; ++i) {
|
|
if (first[i].compareTo(second[i]) != 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Merges a list of error lists into a single list. The result is sorted with
|
|
/// respect to [StaticError.compareTo].
|
|
List<StaticError> mergeErrors(Iterable<List<StaticError>> errors) {
|
|
// Using a [SplayTreeSet] here results in a sorted list.
|
|
var result = SplayTreeSet<StaticError>();
|
|
for (var list in errors) {
|
|
result.addAll(list);
|
|
}
|
|
return result.toList();
|
|
}
|
|
|
|
const staticOutcomes = [
|
|
"syntax error",
|
|
"compile-time error",
|
|
"static type warning",
|
|
];
|
|
|
|
class UnableToConvertException {
|
|
final String message;
|
|
UnableToConvertException(this.message);
|
|
@override
|
|
String toString() => "unable to convert: $message";
|
|
}
|
|
|
|
class CleanedMultiTest {
|
|
final String text;
|
|
final Map<String, String> subTests;
|
|
CleanedMultiTest(this.text, this.subTests);
|
|
}
|
|
|
|
CleanedMultiTest removeMultiTestMarker(String test) {
|
|
var buffer = StringBuffer();
|
|
var subTests = <String, String>{};
|
|
var lines = LineSplitter.split(test)
|
|
.where((line) => !line.startsWith("// Test created from multitest named"))
|
|
.toList();
|
|
if (lines.length > 1 && lines.last.isEmpty) {
|
|
// If the file ends with a newline, remove the empty line - the loop below
|
|
// will add a newline to the end.
|
|
lines.length--;
|
|
}
|
|
for (var line in lines) {
|
|
var matches = multitestMarker.allMatches(line);
|
|
if (matches.length > 1) {
|
|
throw "internal error: cannot process line '$line'";
|
|
} else if (matches.length == 1) {
|
|
var match = matches.single;
|
|
var annotation = Annotation.tryParse(line)!;
|
|
if (annotation.outcomes.length != 1) {
|
|
throw UnableToConvertException("annotation has multiple outcomes");
|
|
}
|
|
var outcome = annotation.outcomes.single;
|
|
if (outcome == "continued" ||
|
|
outcome == "ok" ||
|
|
staticOutcomes.contains(outcome)) {
|
|
line = line.substring(0, match.start).trimRight();
|
|
if (line.endsWith("//")) {
|
|
line = line.substring(0, line.length - 2).trimRight();
|
|
}
|
|
if (outcome != "continued") {
|
|
subTests[annotation.key] = outcome;
|
|
}
|
|
} else {
|
|
throw UnableToConvertException("test contains dynamic outcome");
|
|
}
|
|
}
|
|
buffer.writeln(line);
|
|
}
|
|
return CleanedMultiTest(buffer.toString(), subTests);
|
|
}
|
|
|
|
Future createRuntimeTest(
|
|
String testFilePath,
|
|
String multiTestPath,
|
|
bool writeToFile,
|
|
) async {
|
|
var testName = basename(testFilePath);
|
|
String runtimeTestBase;
|
|
if (testName.endsWith("_test.dart")) {
|
|
runtimeTestBase = testName.substring(
|
|
0,
|
|
testName.length - "_test.dart".length,
|
|
);
|
|
} else if (testName.endsWith(".dart")) {
|
|
runtimeTestBase = testName.substring(0, testName.length - ".dart".length);
|
|
} else {
|
|
runtimeTestBase = testName;
|
|
}
|
|
var runtimeTestPath =
|
|
"${dirname(testFilePath)}/$runtimeTestBase"
|
|
"_runtime_test.dart";
|
|
var n = 1;
|
|
while (await File(runtimeTestPath).exists()) {
|
|
runtimeTestPath =
|
|
"${dirname(testFilePath)}/$runtimeTestBase"
|
|
"_runtime_${n++}_test.dart";
|
|
}
|
|
var testContent = await File(multiTestPath).readAsString();
|
|
var cleanedMultiTest = removeMultiTestMarker(testContent);
|
|
var runtimeTestContent = """
|
|
// TODO(multitest): This was automatically migrated from a multitest and may
|
|
// contain strange or dead code.
|
|
|
|
${cleanedMultiTest.text}""";
|
|
if (writeToFile) {
|
|
var outputFile = File(runtimeTestPath);
|
|
await outputFile.writeAsString(runtimeTestContent, mode: FileMode.append);
|
|
print("Runtime part of the test written to '$runtimeTestPath'.");
|
|
} else {
|
|
print("-- $runtimeTestPath:");
|
|
print(runtimeTestContent);
|
|
}
|
|
}
|
|
|
|
Future<void> convertFile(
|
|
String testFilePath,
|
|
bool writeToFile,
|
|
bool verbose,
|
|
List<String> experiments,
|
|
) async {
|
|
var testFile = File(testFilePath);
|
|
if (!await testFile.exists()) {
|
|
print("File '${testFile.uri.toFilePath()}' not found");
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
// Read test file and setup output directory.
|
|
var suiteDirectory = Path.raw(Uri.base.path);
|
|
var content = await testFile.readAsString();
|
|
var test = TestFile.read(suiteDirectory, testFilePath);
|
|
if (!content.contains(multitestMarker)) {
|
|
print("Test ${test.path.toNativePath()} is not a multi-test.");
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
var outputDirectory = await Directory(dirname(testFilePath)).createTemp();
|
|
if (verbose) {
|
|
print("Output directory for generated files: ${outputDirectory.uri.path}");
|
|
}
|
|
try {
|
|
// Generate the sub-tests of the multi-test in [outputDirectory].
|
|
var tests = [
|
|
test,
|
|
...splitMultitest(test, outputDirectory.uri.toFilePath(), suiteDirectory),
|
|
];
|
|
if (!tests[1].name.endsWith("/none")) {
|
|
throw "internal error: expected second test to be the '/none' test";
|
|
}
|
|
// Remove the multi-test marker from the test. We do this here to fail fast
|
|
// for cases we do not support, because generating the front-end errors is
|
|
// quite slow.
|
|
var cleanedTest = removeMultiTestMarker(content);
|
|
var contentWithoutMarkers = cleanedTest.text;
|
|
// Get the reported errors for the multi-test and all generated sub-tests
|
|
// from the analyser and the common front-end.
|
|
var options = [
|
|
...test.sharedOptions,
|
|
if (experiments.isNotEmpty)
|
|
"--enable-experiment=${experiments.join(',')}",
|
|
];
|
|
|
|
var errors = <List<StaticError>>[];
|
|
for (var test in tests) {
|
|
if (verbose) {
|
|
print("Processing ${test.path}");
|
|
}
|
|
errors.add(await getErrors(options, test.path.toNativePath()));
|
|
}
|
|
if (errors[1].isNotEmpty) {
|
|
throw UnableToConvertException("internal error: errors in '/none' test");
|
|
}
|
|
// Check that the multi-test generates the same errors as all sub-tests
|
|
// together - otherwise converting the test would be unsound.
|
|
var sortedOriginalErrors = errors[0].toList()..sort();
|
|
var mergedErrors = mergeErrors(errors.skip(2));
|
|
if (!areSameErrors(sortedOriginalErrors, mergedErrors)) {
|
|
if (verbose) {
|
|
print(
|
|
"Sub-tests have different errors!\n\n"
|
|
"Errors in sub-tests:\n$mergedErrors\n\n"
|
|
"Errors in original test:\n$sortedOriginalErrors\n",
|
|
);
|
|
}
|
|
throw UnableToConvertException(
|
|
"Test produces different errors than its sub-tests.",
|
|
);
|
|
}
|
|
// Insert the error message annotations for the static testing framework
|
|
// and output the result.
|
|
var annotatedContent = updateErrorExpectations(
|
|
testFilePath,
|
|
contentWithoutMarkers,
|
|
errors[0],
|
|
);
|
|
if (writeToFile) {
|
|
await testFile.writeAsString(annotatedContent);
|
|
print("Converted test '${test.path.toNativePath()}'.");
|
|
} else {
|
|
print("-- ${test.path.toNativePath()}:");
|
|
print(annotatedContent);
|
|
}
|
|
// Generate runtime tests for all sub-tests that are generated from the
|
|
// 'none' case and those with 'ok' annotations.
|
|
for (var i = 1; i < tests.length; ++i) {
|
|
var test = tests[i].path.toNativePath();
|
|
var base = basenameWithoutExtension(test);
|
|
var key = base.split("_").last;
|
|
if (key == "none" || cleanedTest.subTests[key] == "ok") {
|
|
await createRuntimeTest(
|
|
testFilePath,
|
|
tests[i].path.toNativePath(),
|
|
writeToFile,
|
|
);
|
|
}
|
|
}
|
|
} on UnableToConvertException catch (exception) {
|
|
print(
|
|
"Could not convert ${test.path.toNativePath()}: ${exception.message}",
|
|
);
|
|
exitCode = 1;
|
|
return;
|
|
} finally {
|
|
outputDirectory.delete(recursive: true);
|
|
}
|
|
}
|
|
|
|
Future<void> main(List<String> arguments) async {
|
|
var parser = ArgParser();
|
|
parser.addFlag("verbose", abbr: "v", help: "print additional information");
|
|
parser.addFlag("write", abbr: "w", help: "write output to input file");
|
|
parser.addMultiOption(
|
|
"enable-experiment",
|
|
defaultsTo: <String>[],
|
|
help: "Enable one or more experimental features",
|
|
);
|
|
|
|
var results = parser.parse(arguments);
|
|
if (results.rest.isEmpty) {
|
|
print("Usage: convert_multi_test.dart [-v] [-w] <input files>");
|
|
print(parser.usage);
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
var verbose = results["verbose"] as bool;
|
|
var filePaths = results.rest.map(
|
|
(path) => Uri.base.resolve(path).toFilePath(),
|
|
);
|
|
var writeToFile = results["write"] as bool;
|
|
for (var testFilePath in filePaths) {
|
|
await convertFile(
|
|
testFilePath,
|
|
writeToFile,
|
|
verbose,
|
|
(results["enable-experiment"] as List).cast<String>(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Invoke analyzer on [file] and gather all static errors it reports.
|
|
Future<List<StaticError>> _runAnalyzer(File file, List<String> options) async {
|
|
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 {
|
|
var absolutePath = file.absolute.path;
|
|
// 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/compile.dart",
|
|
...options,
|
|
"--verify",
|
|
"-o",
|
|
"dev:null", // Output is only created for file URIs.
|
|
absolutePath,
|
|
]);
|
|
|
|
// 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("$absolutePath.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];
|
|
}
|