[package:testing] Various updates

* Delete unused stuff
 * rename 'path' to 'root'
 * accept 'includeEndsWith' as a plain text string so we often can avoid
   using regexp
 * accept 'subRoots' to filter to directories faster and more precisly
   than when using regexps in 'pattern'
 * make 'list' async instead of async* (no more yield stuff which we
   promptly turn into a list when actually using it)

Note that some changes in testing.json is not 100% semantic-preserving,
e.g. the "parser_all" suite previously had a pattern "/tests/.*\\.dart$"
which was probably meant to include all dart files in the "tests" folder
in the root, but in fact included all dart files in a "tests" folder
anywhere. The updated version does not.

Change-Id: Idd014274f86bf6214dee0753a7738ec80bc6a49e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/358442
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
This commit is contained in:
Jens Johansen
2024-03-21 06:58:32 +00:00
committed by Commit Queue
parent 25071fc448
commit 2bd74882e2
15 changed files with 255 additions and 475 deletions
+62 -28
View File
@@ -4,8 +4,6 @@
library testing.chain;
import 'dart:async' show Future, Stream;
import 'dart:convert' show json, JsonEncoder;
import 'dart:io' show Directory, File, FileSystemEntity, exitCode;
@@ -31,27 +29,50 @@ typedef CreateContext = Future<ChainContext> Function(
class Chain extends Suite {
final Uri source;
final Uri uri;
final Uri root;
final List<Uri> subRoots;
final List<String> includeEndsWith;
final List<RegExp> pattern;
final List<RegExp> exclude;
Chain(String name, String kind, this.source, this.uri, Uri statusFile,
this.pattern, this.exclude)
Chain(String name, String kind, this.source, this.root, this.subRoots,
Uri statusFile, this.includeEndsWith, this.pattern, this.exclude)
: super(name, kind, statusFile);
factory Chain.fromJsonMap(Uri base, Map json, String name, String kind) {
Uri source = base.resolve(json["source"]);
String path = json["path"];
if (!path.endsWith("/")) {
path += "/";
String root = json["root"];
if (!root.endsWith("/")) {
root += "/";
}
Uri rootUri = base.resolve(root);
List<Uri> subRoots = [];
List? subRootsList = json["subRoots"];
if (subRootsList != null) {
for (String subRoot in subRootsList) {
if (!subRoot.endsWith("/")) {
subRoot += "/";
}
subRoots.add(rootUri.resolve(subRoot));
}
} else {
subRoots.add(rootUri);
}
Uri uri = base.resolve(path);
Uri statusFile = base.resolve(json["status"]);
List<RegExp> pattern = [for (final p in json['pattern']) RegExp(p)];
List<RegExp> exclude = [for (final e in json['exclude']) RegExp(e)];
return Chain(name, kind, source, uri, statusFile, pattern, exclude);
List<String> includeEndsWith =
List<String>.from(json['includeEndsWith'] ?? const []);
List<RegExp> pattern = [
for (final p in json['pattern'] ?? const []) new RegExp(p)
];
List<RegExp> exclude = [
for (final e in json['exclude'] ?? const []) new RegExp(e)
];
return Chain(name, kind, source, rootUri, subRoots, statusFile,
includeEndsWith, pattern, exclude);
}
void writeImportOn(StringSink sink) {
@@ -78,9 +99,10 @@ class Chain extends Suite {
"name": name,
"kind": kind,
"source": "$source",
"path": "$uri",
"root": "$root",
"status": "$statusFile",
"pattern": [for (final r in pattern) r.pattern],
"includeEndsWith": includeEndsWith,
"exclude": [for (final r in exclude) r.pattern],
};
}
@@ -106,8 +128,7 @@ abstract class ChainContext {
.toList();
TestExpectations expectations = readTestExpectations(
<String>[suite.statusFile!.toFilePath()], expectationSet);
Stream<TestDescription> stream = list(suite);
List<TestDescription> descriptions = await stream.toList();
List<TestDescription> descriptions = await list(suite);
descriptions.sort();
if (shards > 1) {
List<TestDescription> shardDescriptions = [];
@@ -242,22 +263,35 @@ abstract class ChainContext {
await postRun();
}
Stream<TestDescription> list(Chain suite) async* {
Directory testRoot = Directory.fromUri(suite.uri);
if (await testRoot.exists()) {
Stream<FileSystemEntity> files =
testRoot.list(recursive: true, followLinks: false);
await for (FileSystemEntity entity in files) {
if (entity is! File) continue;
String path = entity.uri.path;
if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
if (suite.pattern.any((RegExp r) => path.contains(r))) {
yield FileBasedTestDescription(suite.uri, entity);
Future<List<TestDescription>> list(Chain suite) async {
List<TestDescription> result = [];
for (Uri subRoot in suite.subRoots) {
Directory testRoot = Directory.fromUri(subRoot);
if (testRoot.existsSync()) {
for (FileSystemEntity entity
in testRoot.listSync(recursive: true, followLinks: false)) {
if (entity is! File) continue;
// Use `.uri.path` instead of just `.path` to ensure forward slashes.
String path = entity.uri.path;
if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
bool include = false;
if (suite.includeEndsWith.any((String end) => path.endsWith(end))) {
include = true;
}
if (!include && suite.pattern.any((RegExp r) => path.contains(r))) {
include = true;
}
if (include) {
result.add(new FileBasedTestDescription(suite.root, entity));
}
}
} else {
throw "$subRoot isn't a directory";
}
} else {
throw "${suite.uri} isn't a directory";
}
return result;
}
Set<Expectation> processExpectedOutcomes(
+2 -43
View File
@@ -18,17 +18,14 @@ import 'error_handling.dart' show withErrorHandling;
import 'chain.dart' show CreateContext;
import '../testing.dart'
show Chain, ChainContext, FileBasedTestDescription, listTests;
import '../testing.dart' show Chain, ChainContext;
import 'analyze.dart' show Analyze;
import 'log.dart'
show enableVerboseOutput, isVerbose, Logger, splitLines, StdoutLogger;
import 'suite.dart' show Dart, Suite;
import 'test_dart.dart' show TestDart;
import 'suite.dart' show Suite;
import 'zone_helper.dart' show acknowledgeControlMessages;
@@ -168,30 +165,12 @@ class SuiteRunner {
StringBuffer chain = StringBuffer();
bool hasRunnableTests = false;
await for (FileBasedTestDescription description in listDescriptions()) {
hasRunnableTests = true;
description.writeImportOn(imports);
description.writeClosureOn(dart);
}
await for (Chain suite in listChainSuites()) {
hasRunnableTests = true;
suite.writeImportOn(imports);
suite.writeClosureOn(chain);
}
bool isFirstTestDartSuite = true;
for (TestDart suite in listTestDartSuites()) {
if (shouldRunSuite(suite)) {
hasRunnableTests = true;
if (isFirstTestDartSuite) {
suite.writeFirstImportOn(imports);
}
isFirstTestDartSuite = false;
suite.writeRunCommandOn(chain);
}
}
if (!hasRunnableTests) return null;
return """
@@ -234,22 +213,6 @@ Future<Null> main() async {
return hasAnalyzerSuites;
}
Stream<FileBasedTestDescription> listDescriptions() async* {
for (Dart suite in suites.whereType<Dart>()) {
await for (FileBasedTestDescription description
in listTests(<Uri>[suite.uri], pattern: "")) {
testUris.add((await Isolate.resolvePackageUri(description.uri))!);
if (shouldRunSuite(suite)) {
String path = description.file.uri.path;
if (suite.exclude.any((RegExp r) => path.contains(r))) continue;
if (suite.pattern.any((RegExp r) => path.contains(r))) {
yield description;
}
}
}
}
}
Stream<Chain> listChainSuites() async* {
for (Chain suite in suites.whereType<Chain>()) {
testUris.add((await Isolate.resolvePackageUri(suite.source))!);
@@ -259,10 +222,6 @@ Future<Null> main() async {
}
}
Iterable<TestDart> listTestDartSuites() {
return suites.whereType<TestDart>();
}
Iterable<Analyze> listAnalyzerSuites() {
return suites.whereType<Analyze>();
}
-57
View File
@@ -6,8 +6,6 @@ library testing.suite;
import 'chain.dart' show Chain;
import 'test_dart.dart' show TestDart;
/// Records the properties of a test suite.
abstract class Suite {
final String name;
@@ -22,15 +20,9 @@ abstract class Suite {
String kind = json["kind"].toLowerCase();
String name = json["name"];
switch (kind) {
case "dart":
return Dart.fromJsonMap(base, json, name);
case "chain":
return Chain.fromJsonMap(base, json, name, kind);
case "test_dart":
return TestDart.fromJsonMap(base, json, name, kind);
default:
throw "Suite '$name' has unknown kind '$kind'.";
}
@@ -39,52 +31,3 @@ abstract class Suite {
@override
String toString() => "Suite($name, $kind)";
}
/// A suite of standalone tests. The tests are combined and run as one program.
///
/// A standalone test is a test with a `main` method. The test is considered
/// successful if main doesn't throw an error (or if `main` returns a future,
/// that future completes without errors).
///
/// The tests are combined by generating a Dart file which imports all the main
/// methods and calls them sequentially.
///
/// Example JSON configuration:
///
/// {
/// "name": "test",
/// "kind": "Dart",
/// # Root directory of tests in this suite.
/// "path": "test/",
/// # Files in `path` that match any of the following regular expressions
/// # are considered to be part of this suite.
/// "pattern": [
/// "_test.dart$"
/// ],
/// # Except if they match any of the following regular expressions.
/// "exclude": [
/// "/golden/"
/// ]
/// }
class Dart extends Suite {
final Uri uri;
final List<RegExp> pattern;
final List<RegExp> exclude;
Dart(String name, this.uri, this.pattern, this.exclude)
: super(name, "dart", null);
factory Dart.fromJsonMap(Uri base, Map json, String name) {
Uri uri = base.resolve(json["path"]);
List<RegExp> pattern =
List<RegExp>.from(json["pattern"].map((String p) => RegExp(p)));
List<RegExp> exclude =
List<RegExp>.from(json["exclude"].map((String p) => RegExp(p)));
return Dart(name, uri, pattern, exclude);
}
@override
String toString() => "Dart($name, $uri, $pattern, $exclude)";
}
-83
View File
@@ -1,83 +0,0 @@
// Copyright (c) 2016, 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.
library testing.test_dart;
import 'dart:convert' show json;
import 'dart:io' show Platform;
import 'suite.dart' show Suite;
/// A suite that runs test.dart.
class TestDart extends Suite {
final String common;
final String processes;
final List<String> commandLines;
TestDart(String name, this.common, this.processes, this.commandLines)
: super(
name,
"test_dart",
// This suite doesn't know what it's status file is because
// test.dart doesn't know.
null);
factory TestDart.fromJsonMap(Uri base, Map json, String name, String kind) {
String common = json["common"] ?? "";
String processes = json["processes"] ?? "-j${Platform.numberOfProcessors}";
List<String> commandLines = json["command-lines"] == null
? List<String>.from(json["command-lines"])
: <String>[];
return TestDart(name, common, processes, commandLines);
}
void writeFirstImportOn(StringSink sink) {
sink.writeln("import 'dart:io' as io;");
sink.writeln(
"import 'package:testing/src/stdio_process.dart' show StdioProcess;");
}
void writeRunCommandOn(StringSink sink) {
Uri dartVm;
if (Platform.isMacOS || Platform.isLinux) {
dartVm = Uri.base.resolve("tools/sdks/dart-sdk/bin/dart");
} else if (Platform.isWindows) {
dartVm = Uri.base.resolve("tools/sdks/dart-sdk/bin/dart.exe");
} else {
throw "Operating system not supported: ${Platform.operatingSystem}";
}
List<String> processedArguments = <String>[];
processedArguments.add(Uri.base
.resolve("pkg/test_runner/bin/package_testing_support.dart")
.toFilePath());
for (String commandLine in commandLines) {
String arguments = common;
arguments += " $processes";
arguments += " $commandLine";
processedArguments.add(arguments);
}
String executable = json.encode(dartVm.toFilePath());
String arguments = json.encode(processedArguments);
sink.write("""
{
print('Running $arguments');
StdioProcess process = await StdioProcess.run($executable, $arguments,
suppressOutput: false, timeout: null);
if (process.exitCode != 0) {
print(process.output);
io.exitCode = 1;
}
}
""");
}
@override
String toString() {
return "TestDart($name, ${json.encode(common)}, ${json.encode(processes)},"
" ${json.encode(commandLines)})";
}
}