[CFE] First pass at internal test utility
This is a first pass at running inlined tests (seemingly called doctests in for instance python) on internal CFE code. This is not useful for testing the entire compiler for instance, but could be useful for utility methods that take simple inputs and provide simple outputs. I've added tests to a few tings to try it out. The syntax for it here is probably overly verbose but we could iterate on that if need be. The way it's done is that the test-code is extracted from the comments and (virtually) "injected" in a weirdly named top level method of the same file, which is then called from a "fake" main; the whole thing is compiled to dill and run in an isolate. (This is an application where making advanced invalidation support adding top level methods would be great). Change-Id: I7927822bca0ba68780458c40fe585882330e3b60 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/203242 Commit-Queue: Jens Johansen <jensj@google.com> Reviewed-by: Dmitry Stefantsov <dmitryas@google.com> Reviewed-by: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
committed by
commit-bot@chromium.org
parent
fb5465c624
commit
5b5956bd4e
@@ -2249,6 +2249,55 @@ class IncrementalCompiler implements IncrementalKernelGenerator {
|
||||
}
|
||||
|
||||
/// Translate a parts "partUri" to an actual uri with handling of invalid uris.
|
||||
///
|
||||
/// ```
|
||||
/// DartDocTest(
|
||||
/// getPartUri(
|
||||
/// Uri.parse("file://path/to/parent.dart"),
|
||||
/// new LibraryPart([], "simple.dart")
|
||||
/// ),
|
||||
/// Uri.parse("file://path/to/simple.dart")
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// getPartUri(
|
||||
/// Uri.parse("file://path/to/parent.dart"),
|
||||
/// new LibraryPart([], "dir/simple.dart")
|
||||
/// ),
|
||||
/// Uri.parse("file://path/to/dir/simple.dart")
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// getPartUri(
|
||||
/// Uri.parse("file://path/to/parent.dart"),
|
||||
/// new LibraryPart([], "../simple.dart")
|
||||
/// ),
|
||||
/// Uri.parse("file://path/simple.dart")
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// getPartUri(
|
||||
/// Uri.parse("file://path/to/parent.dart"),
|
||||
/// new LibraryPart([], "file:///my/path/absolute.dart")
|
||||
/// ),
|
||||
/// Uri.parse("file:///my/path/absolute.dart")
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// getPartUri(
|
||||
/// Uri.parse("file://path/to/parent.dart"),
|
||||
/// new LibraryPart([], "package:foo/hello.dart")
|
||||
/// ),
|
||||
/// Uri.parse("package:foo/hello.dart")
|
||||
/// )
|
||||
/// ```
|
||||
/// And with invalid part uri:
|
||||
/// ```
|
||||
/// DartDocTest(
|
||||
/// getPartUri(
|
||||
/// Uri.parse("file://path/to/parent.dart"),
|
||||
/// new LibraryPart([], ":hello")
|
||||
/// ),
|
||||
/// new Uri(scheme: SourceLibraryBuilder.MALFORMED_URI_SCHEME,
|
||||
/// query: Uri.encodeQueryComponent(":hello"))
|
||||
/// )
|
||||
/// ```
|
||||
Uri getPartUri(Uri parentUri, LibraryPart part) {
|
||||
try {
|
||||
return parentUri.resolve(part.partUri);
|
||||
|
||||
@@ -6867,9 +6867,25 @@ Block combineStatements(Statement statement, Statement body) {
|
||||
}
|
||||
}
|
||||
|
||||
/// DartDocTest(
|
||||
/// debugName("myClassName", "myName", "myPrefix"),
|
||||
/// "myPrefix.myClassName.myName"
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// debugName("myClassName", "myName"),
|
||||
/// "myClassName.myName"
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// debugName("myClassName", ""),
|
||||
/// "myClassName"
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// debugName("", ""),
|
||||
/// ""
|
||||
/// )
|
||||
String debugName(String className, String name, [String prefix]) {
|
||||
String result = name.isEmpty ? className : "$className.$name";
|
||||
return prefix == null ? result : "$prefix.result";
|
||||
return prefix == null ? result : "$prefix.$result";
|
||||
}
|
||||
|
||||
// TODO(johnniwinther): This is a bit ad hoc. Call sites should know what kind
|
||||
|
||||
@@ -6,74 +6,6 @@
|
||||
|
||||
import "dart:math" as math;
|
||||
|
||||
main() {
|
||||
List<String> test = BinaryMdDillReader._getGenerics("Pair<A, B>");
|
||||
if (test.length != 2 || test[0] != "A" || test[1] != "B") {
|
||||
throw "Expected [A, B] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("List<Expression>");
|
||||
if (test.length != 1 || test[0] != "Expression") {
|
||||
throw "Expected [Expression] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("List<Pair<FileOffset, Expression>>");
|
||||
if (test.length != 1 || test[0] != "Pair<FileOffset, Expression>") {
|
||||
throw "Expected [Pair<FileOffset, Expression>] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("RList<Pair<UInt32, UInt32>>");
|
||||
if (test.length != 1 || test[0] != "Pair<UInt32, UInt32>") {
|
||||
throw "Expected [Pair<UInt32, UInt32>] got $test";
|
||||
}
|
||||
|
||||
test =
|
||||
BinaryMdDillReader._getGenerics("List<Pair<FieldReference, Expression>>");
|
||||
if (test.length != 1 || test[0] != "Pair<FieldReference, Expression>") {
|
||||
throw "Expected [Pair<FieldReference, Expression>] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics(
|
||||
"List<Pair<ConstantReference, ConstantReference>>");
|
||||
if (test.length != 1 ||
|
||||
test[0] != "Pair<ConstantReference, ConstantReference>") {
|
||||
throw "Expected [Pair<ConstantReference, ConstantReference>] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics(
|
||||
"List<Pair<FieldReference, ConstantReference>>");
|
||||
if (test.length != 1 ||
|
||||
test[0] != "Pair<FieldReference, ConstantReference>") {
|
||||
throw "Expected [Pair<FieldReference, ConstantReference>] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("Option<List<DartType>>");
|
||||
if (test.length != 1 || test[0] != "List<DartType>") {
|
||||
throw "Expected [List<DartType>] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("Foo<Bar<Baz>>");
|
||||
if (test.length != 1 || test[0] != "Bar<Baz>") {
|
||||
throw "Expected [Bar<Baz>] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("Foo<A, B<C, D>, E>");
|
||||
if (test.length != 3 ||
|
||||
test[0] != "A" ||
|
||||
test[1] != "B<C, D>" ||
|
||||
test[2] != "E") {
|
||||
throw "Expected [A, B<C, D>, E] got $test";
|
||||
}
|
||||
|
||||
test = BinaryMdDillReader._getGenerics("Foo<A, B<C, D<E<F<G>>>>, H>");
|
||||
if (test.length != 3 ||
|
||||
test[0] != "A" ||
|
||||
test[1] != "B<C, D<E<F<G>>>>" ||
|
||||
test[2] != "H") {
|
||||
throw "Expected [A, B<C, D<E<F<G>>>>, H] got $test";
|
||||
}
|
||||
}
|
||||
|
||||
class BinaryMdDillReader {
|
||||
final String _binaryMdContent;
|
||||
|
||||
@@ -317,6 +249,65 @@ class BinaryMdDillReader {
|
||||
///
|
||||
/// Note that the input string *has* to use generics, i.e. have '<' and '>'
|
||||
/// in it.
|
||||
///
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("Pair<A, B>"), ["A", "B"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("List<Expression>"), ["Expression"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("List<Pair<FileOffset, Expression>>"),
|
||||
/// ["Pair<FileOffset, Expression>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("RList<Pair<UInt32, UInt32>>"),
|
||||
/// ["Pair<UInt32, UInt32>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics(
|
||||
/// "List<Pair<FieldReference, Expression>>"),
|
||||
/// ["Pair<FieldReference, Expression>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics(
|
||||
/// "List<Pair<ConstantReference, ConstantReference>>"),
|
||||
/// ["Pair<ConstantReference, ConstantReference>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics(
|
||||
/// "List<Pair<FieldReference, ConstantReference>>"),
|
||||
/// ["Pair<FieldReference, ConstantReference>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("Option<List<DartType>>"),
|
||||
/// ["List<DartType>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("Foo<Bar<Baz>>"), ["Bar<Baz>"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("Foo<A, B<C, D>, E>"),
|
||||
/// ["A", "B<C, D>", "E"]
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// BinaryMdDillReader._getGenerics("Foo<A, B<C, D<E<F<G>>>>, H>"),
|
||||
/// ["A", "B<C, D<E<F<G>>>>", "H"]
|
||||
/// )
|
||||
///
|
||||
/// Expected failing run (expects to fail with unbalanced < >).
|
||||
/// TODO: Support this more elegantly.
|
||||
/// DartDocTest(() {
|
||||
/// try {
|
||||
/// BinaryMdDillReader._getGenerics("Foo<A, B<C, D, E>");
|
||||
/// return false;
|
||||
/// } catch(e) {
|
||||
/// return true;
|
||||
/// }
|
||||
/// }(),
|
||||
/// true
|
||||
/// )
|
||||
///
|
||||
static List<String> _getGenerics(String s) {
|
||||
s = s.substring(s.indexOf("<") + 1, s.lastIndexOf(">"));
|
||||
// Check that any '<' and '>' are balanced and split entries on comma for
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2021, 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.
|
||||
|
||||
// @dart = 2.9
|
||||
|
||||
import 'package:testing/testing.dart'
|
||||
show Chain, ChainContext, Result, Step, TestDescription, runMe;
|
||||
|
||||
import '../tool/dart_doctest_impl.dart';
|
||||
|
||||
main([List<String> arguments = const []]) =>
|
||||
runMe(arguments, createContext, configurationPath: "../testing.json");
|
||||
|
||||
Future<Context> createContext(
|
||||
Chain suite, Map<String, String> environment) async {
|
||||
return new Context(suite.name);
|
||||
}
|
||||
|
||||
class Context extends ChainContext {
|
||||
final String suiteName;
|
||||
|
||||
Context(this.suiteName);
|
||||
|
||||
final List<Step> steps = const <Step>[
|
||||
const DartDocTestStep(),
|
||||
];
|
||||
|
||||
// Override special handling of negative tests.
|
||||
@override
|
||||
Result processTestResult(
|
||||
TestDescription description, Result result, bool last) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Stream<DartDocTestTestDescription> list(Chain suite) async* {
|
||||
await for (TestDescription entry in super.list(suite)) {
|
||||
List<Test> tests = extractTestsFromUri(entry.uri);
|
||||
if (tests.isEmpty) continue;
|
||||
yield new DartDocTestTestDescription(entry.shortName, entry.uri, tests);
|
||||
}
|
||||
}
|
||||
|
||||
DartDocTest dartDocTest = new DartDocTest();
|
||||
}
|
||||
|
||||
class DartDocTestTestDescription extends TestDescription {
|
||||
final String shortName;
|
||||
final Uri uri;
|
||||
final List<Test> tests;
|
||||
|
||||
DartDocTestTestDescription(this.shortName, this.uri, this.tests);
|
||||
}
|
||||
|
||||
class DartDocTestStep extends Step<DartDocTestTestDescription,
|
||||
DartDocTestTestDescription, Context> {
|
||||
const DartDocTestStep();
|
||||
|
||||
String get name => "DartDocTest";
|
||||
|
||||
Future<Result<DartDocTestTestDescription>> run(
|
||||
DartDocTestTestDescription description, Context context) async {
|
||||
bool result = await context.dartDocTest
|
||||
.compileAndRun(description.uri, description.tests);
|
||||
if (result) {
|
||||
return new Result<DartDocTestTestDescription>.pass(description);
|
||||
} else {
|
||||
return new Result<DartDocTestTestDescription>.fail(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2021, 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.
|
||||
|
||||
@@ -28,6 +28,8 @@ allocations
|
||||
allowlist
|
||||
allowlisting
|
||||
alt
|
||||
amend
|
||||
amended
|
||||
amortized
|
||||
analyses
|
||||
animal
|
||||
@@ -147,6 +149,7 @@ columns
|
||||
commented
|
||||
commit
|
||||
commits
|
||||
communication
|
||||
companion
|
||||
comparative
|
||||
comparer
|
||||
@@ -189,7 +192,11 @@ d1b
|
||||
dacoharkes
|
||||
dadd
|
||||
daemon
|
||||
dart$doc$test$tester
|
||||
dart\$doc\$test\$tester
|
||||
dartanalyzer
|
||||
dartdoctest
|
||||
dartdoctestrun
|
||||
dartfile
|
||||
dartfmt
|
||||
dash
|
||||
@@ -237,6 +244,7 @@ dispose
|
||||
dist
|
||||
div
|
||||
divergent
|
||||
doctest
|
||||
doctype
|
||||
doesnt
|
||||
dog
|
||||
@@ -258,6 +266,7 @@ ec
|
||||
echo
|
||||
edits
|
||||
elapse
|
||||
elegantly
|
||||
ell
|
||||
entrypoint
|
||||
entrypoints
|
||||
|
||||
@@ -17,6 +17,7 @@ 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 'dartdoctest_suite.dart' as dartdoctest show createContext;
|
||||
import 'fasta/expression_suite.dart' as expression show createContext;
|
||||
import 'fasta/incremental_dartino_suite.dart' as incremental_dartino
|
||||
show createContext;
|
||||
@@ -351,6 +352,12 @@ class Suite {
|
||||
}
|
||||
|
||||
const List<Suite> suites = [
|
||||
const Suite(
|
||||
"dartdoctest",
|
||||
dartdoctest.createContext,
|
||||
"../testing.json",
|
||||
shardCount: 1,
|
||||
),
|
||||
const Suite(
|
||||
"fasta/expression",
|
||||
expression.createContext,
|
||||
|
||||
@@ -269,6 +269,19 @@
|
||||
"front_end/lib/src/fasta/fasta_codes_cfe_generated\\.dart$"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dartdoctest",
|
||||
"kind": "Chain",
|
||||
"source": "test/dartdoctest_suite.dart",
|
||||
"path": "../",
|
||||
"status": "test/dartdoctest_suite.status",
|
||||
"pattern": [
|
||||
"_fe_analyzer_shared/.*\\.dart$",
|
||||
"kernel/.*\\.dart$",
|
||||
"front_end/.*\\.dart$"
|
||||
],
|
||||
"exclude": []
|
||||
},
|
||||
{
|
||||
"name": "spelling_test_src",
|
||||
"kind": "Chain",
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
// Copyright (c) 2021, 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:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart'
|
||||
show ScannerConfiguration;
|
||||
|
||||
import 'package:_fe_analyzer_shared/src/scanner/token.dart';
|
||||
|
||||
import 'package:_fe_analyzer_shared/src/scanner/utf8_bytes_scanner.dart'
|
||||
show Utf8BytesScanner;
|
||||
|
||||
import 'package:_fe_analyzer_shared/src/scanner/token.dart' show Token;
|
||||
import 'package:front_end/src/api_prototype/compiler_options.dart';
|
||||
import 'package:front_end/src/api_prototype/experimental_flags.dart';
|
||||
import 'package:front_end/src/api_prototype/file_system.dart';
|
||||
import 'package:front_end/src/api_prototype/standard_file_system.dart';
|
||||
import 'package:front_end/src/base/processed_options.dart';
|
||||
import 'package:front_end/src/fasta/compiler_context.dart';
|
||||
import 'package:front_end/src/fasta/fasta_codes.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:front_end/src/fasta/incremental_compiler.dart';
|
||||
import 'package:front_end/src/fasta/kernel/utils.dart';
|
||||
import 'package:kernel/ast.dart';
|
||||
import 'package:kernel/target/targets.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:vm/target/vm.dart';
|
||||
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import '../test/incremental_suite.dart' show getOptions;
|
||||
|
||||
const _portMessageTest = "test";
|
||||
const _portMessageGood = "good";
|
||||
const _portMessageBad = "bad";
|
||||
const _portMessageBadDetails = "badDetails";
|
||||
const _portMessageCrash = "crash";
|
||||
const _portMessageParseError = "parseError";
|
||||
const _portMessageDone = "done";
|
||||
|
||||
// TODO: This doesn't work on parts... (Well, it might, depending on how
|
||||
// the part declares what file it's part of and if we've compiled other stuff
|
||||
// first so we know more stuff).
|
||||
class DartDocTest {
|
||||
IncrementalCompiler? incrementalCompiler;
|
||||
late CompilerOptions options;
|
||||
late ProcessedOptions processedOpts;
|
||||
bool errors = false;
|
||||
|
||||
/// All-in-one. Process a file and return if it was good.
|
||||
Future<bool> process(Uri uri) async {
|
||||
print("\n\nProcessing $uri");
|
||||
Stopwatch stopwatch = new Stopwatch()..start();
|
||||
|
||||
// Extract test cases in file.
|
||||
List<Test> tests = extractTestsFromUri(uri);
|
||||
|
||||
if (tests.isEmpty) {
|
||||
print("No tests found in file in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
return true;
|
||||
}
|
||||
print("Found ${tests.length} test(s) in file "
|
||||
"in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
|
||||
return await compileAndRun(uri, tests);
|
||||
}
|
||||
|
||||
Future<bool> compileAndRun(Uri uri, List<Test> tests) async {
|
||||
errors = false;
|
||||
|
||||
// Create code to amend the file with.
|
||||
StringBuffer sb = new StringBuffer();
|
||||
if (tests.isNotEmpty) {
|
||||
sb.writeln(
|
||||
r"Future<void> $dart$doc$test$tester(dynamic dartDocTest) async {");
|
||||
for (Test test in tests) {
|
||||
if (test is TestParseError) {
|
||||
sb.writeln("dartDocTest.parseError(\"${test.message}\");");
|
||||
} else {
|
||||
sb.writeln("try {");
|
||||
sb.writeln(" dartDocTest.test(${test.call}, ${test.result});");
|
||||
sb.writeln("} catch (e) {");
|
||||
sb.writeln(" dartDocTest.crash(e);");
|
||||
sb.writeln("}");
|
||||
}
|
||||
}
|
||||
sb.writeln("}");
|
||||
}
|
||||
|
||||
if (incrementalCompiler == null) {
|
||||
setupIncrementalCompiler(uri);
|
||||
}
|
||||
|
||||
processedOpts.inputs.clear();
|
||||
processedOpts.inputs.add(uri);
|
||||
AmendedFileSystem fileSystem =
|
||||
new AmendedFileSystem(StandardFileSystem.instance);
|
||||
fileSystem.amendFileUri = uri;
|
||||
fileSystem.amendWith = sb.toString();
|
||||
options.fileSystem = fileSystem;
|
||||
processedOpts.clearFileSystemCache();
|
||||
// Invalidate file and package uri to force compilation and re-finding of
|
||||
// packages (e.g. if we're now compiling somewhere else).
|
||||
incrementalCompiler!.invalidate(uri);
|
||||
incrementalCompiler!.invalidate(processedOpts.packagesUri);
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch()..start();
|
||||
Component component =
|
||||
await incrementalCompiler!.computeDelta(entryPoints: [uri]);
|
||||
if (errors) {
|
||||
print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
return false;
|
||||
}
|
||||
print("Compiled (1) in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
stopwatch.reset();
|
||||
|
||||
fileSystem.amendImportUri = component.uriToSource[uri]?.importUri;
|
||||
incrementalCompiler!.invalidate(AmendedFileSystem.mainUri);
|
||||
Component componentMain = await incrementalCompiler!.computeDelta(
|
||||
entryPoints: [AmendedFileSystem.mainUri], fullComponent: true);
|
||||
if (errors) {
|
||||
print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
return false;
|
||||
}
|
||||
print("Compiled (2) in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
stopwatch.reset();
|
||||
|
||||
Directory tmpDir = Directory.systemTemp.createTempSync();
|
||||
Uri dillOutUri = tmpDir.uri.resolve("dartdoctestrun.dill");
|
||||
await writeComponentToFile(componentMain, dillOutUri);
|
||||
print("Wrote dill in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
stopwatch.reset();
|
||||
|
||||
// Spawn URI (dill uri) to run tests.
|
||||
ReceivePort exitPort = new ReceivePort();
|
||||
ReceivePort errorPort = new ReceivePort();
|
||||
ReceivePort communicationPort = new ReceivePort();
|
||||
Completer<dynamic> completer = new Completer();
|
||||
bool error = false;
|
||||
exitPort.listen((message) {
|
||||
exitPort.close();
|
||||
errorPort.close();
|
||||
communicationPort.close();
|
||||
completer.complete();
|
||||
});
|
||||
errorPort.listen((message) {
|
||||
print("Isolate had an error: $message.");
|
||||
error = true;
|
||||
exitPort.close();
|
||||
errorPort.close();
|
||||
communicationPort.close();
|
||||
completer.complete();
|
||||
});
|
||||
int testCount = 0;
|
||||
int goodCount = 0;
|
||||
int badCount = 0;
|
||||
int crashCount = 0;
|
||||
int parseErrorCount = 0;
|
||||
bool done = false;
|
||||
communicationPort.listen((message) {
|
||||
if (done) {
|
||||
throw "Didn't expect any more messages. Got '$message'";
|
||||
}
|
||||
if (message == _portMessageTest) {
|
||||
testCount++;
|
||||
} else if (message == _portMessageGood) {
|
||||
goodCount++;
|
||||
} else if (message == _portMessageBad) {
|
||||
badCount++;
|
||||
} else if (message.toString().startsWith("$_portMessageBadDetails: ")) {
|
||||
print(message.toString().substring("$_portMessageBadDetails: ".length));
|
||||
} else if (message == _portMessageCrash) {
|
||||
crashCount++;
|
||||
} else if (message.toString().startsWith("$_portMessageParseError: ")) {
|
||||
parseErrorCount++;
|
||||
print(message.toString().substring("$_portMessageParseError: ".length));
|
||||
} else if (message == _portMessageDone) {
|
||||
done = true;
|
||||
// don't complete completer here. Expect the exit port to close.
|
||||
} else {
|
||||
throw "Didn't expect '$message'";
|
||||
}
|
||||
});
|
||||
// TODO: Possibly it should be launched in a process instead so we can
|
||||
// (sort of) catch and report exit calls in test code.
|
||||
await Isolate.spawnUri(
|
||||
dillOutUri,
|
||||
[],
|
||||
communicationPort.sendPort,
|
||||
onExit: exitPort.sendPort,
|
||||
onError: errorPort.sendPort,
|
||||
);
|
||||
await completer.future;
|
||||
tmpDir.deleteSync(recursive: true);
|
||||
if (error) {
|
||||
print("Completed with an error in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
return false;
|
||||
} else if (!done) {
|
||||
print(
|
||||
"Didn't complete correctly in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
return false;
|
||||
} else if (testCount != tests.length) {
|
||||
print("Didn't complete with error but ran "
|
||||
"${testCount} tests while expecting ${tests.length} "
|
||||
"in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
return false;
|
||||
} else {
|
||||
print("Processed $testCount test(s) "
|
||||
"in ${stopwatch.elapsedMilliseconds} ms.");
|
||||
if (goodCount == testCount &&
|
||||
badCount == 0 &&
|
||||
crashCount == 0 &&
|
||||
parseErrorCount == 0) {
|
||||
print("All tests passed.");
|
||||
return true;
|
||||
}
|
||||
print("$goodCount OK; "
|
||||
"$badCount bad; "
|
||||
"$crashCount crashed; "
|
||||
"$parseErrorCount parse errors.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setupIncrementalCompiler(Uri uri) {
|
||||
options = getOptions();
|
||||
TargetFlags targetFlags = new TargetFlags(enableNullSafety: true);
|
||||
// TODO: Target could possible be something else...
|
||||
Target target = new VmTarget(targetFlags);
|
||||
options.target = target;
|
||||
options.omitPlatform = true;
|
||||
options.onDiagnostic = (DiagnosticMessage message) {
|
||||
if (message.codeName == "InferredPackageUri") return;
|
||||
print(message.plainTextFormatted.first);
|
||||
if (message.severity == Severity.error) {
|
||||
errors = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Because we add a top-level method this doesn't currently do anything
|
||||
// but maybe it will in the future.
|
||||
options.explicitExperimentalFlags[
|
||||
ExperimentalFlag.alternativeInvalidationStrategy] = true;
|
||||
|
||||
processedOpts = new ProcessedOptions(options: options, inputs: [uri]);
|
||||
CompilerContext compilerContext = new CompilerContext(processedOpts);
|
||||
this.incrementalCompiler = new IncrementalCompiler(compilerContext);
|
||||
}
|
||||
}
|
||||
|
||||
List<Test> extractTestsFromUri(Uri uri) {
|
||||
// Extract test cases in file.
|
||||
File file = new File.fromUri(uri);
|
||||
Uint8List rawBytes = file.readAsBytesSync();
|
||||
Uint8List bytes = new Uint8List(rawBytes.length + 1);
|
||||
bytes.setRange(0, rawBytes.length, rawBytes);
|
||||
|
||||
ScannerConfiguration scannerConfiguration = new ScannerConfiguration(
|
||||
enableExtensionMethods: true,
|
||||
enableNonNullable: true,
|
||||
enableTripleShift: true);
|
||||
|
||||
Utf8BytesScanner scanner = new Utf8BytesScanner(
|
||||
bytes,
|
||||
includeComments: true,
|
||||
configuration: scannerConfiguration,
|
||||
languageVersionChanged: (scanner, languageVersion) {
|
||||
// For now don't do anything, but having it (making it non-null) means the
|
||||
// configuration won't be reset.
|
||||
},
|
||||
);
|
||||
Token firstToken = scanner.tokenize();
|
||||
Token token = firstToken;
|
||||
List<Test> tests = [];
|
||||
while (!token.isEof) {
|
||||
CommentToken? comment = token.precedingComments;
|
||||
if (comment != null) {
|
||||
tests.addAll(processComment(comment));
|
||||
// TODO: Use parser to verify there's at least no syntax errors in the
|
||||
// tests.
|
||||
}
|
||||
Token? next = token.next;
|
||||
if (next == null) break;
|
||||
token = next;
|
||||
}
|
||||
return tests;
|
||||
}
|
||||
|
||||
const int $LF = 10;
|
||||
const int $OPEN_PAREN = 40;
|
||||
const int $CLOSE_PAREN = 41;
|
||||
const int $COMMA = 44;
|
||||
|
||||
class Test {
|
||||
final String call;
|
||||
final String result;
|
||||
|
||||
Test(this.call, this.result);
|
||||
}
|
||||
|
||||
class TestParseError implements Test {
|
||||
final String message;
|
||||
|
||||
TestParseError(this.message);
|
||||
|
||||
@override
|
||||
String get call => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get result => throw UnimplementedError();
|
||||
}
|
||||
|
||||
List<Test> processComment(CommentToken comment) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
CommentToken? commentToken = comment;
|
||||
bool commentBlock = false;
|
||||
while (commentToken != null) {
|
||||
String data = commentToken.lexeme.trim();
|
||||
if (!commentBlock) {
|
||||
if (data.startsWith("///")) {
|
||||
data = data.substring(3).trim();
|
||||
} else if (data.startsWith("//")) {
|
||||
data = data.substring(2).trim();
|
||||
} else if (data.startsWith("/*")) {
|
||||
data = data.substring(2).trim();
|
||||
commentBlock = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (commentBlock && data.endsWith("*/")) {
|
||||
data = data.substring(0, data.length - 2).trim();
|
||||
commentBlock = false;
|
||||
}
|
||||
if (data.isNotEmpty) {
|
||||
sb.write(data);
|
||||
}
|
||||
Token? next = commentToken.next;
|
||||
commentToken = null;
|
||||
if (next is CommentToken) commentToken = next;
|
||||
}
|
||||
String comments = sb.toString();
|
||||
List<Test> result = [];
|
||||
int index = comments.indexOf("DartDocTest(");
|
||||
if (index < 0) {
|
||||
return result;
|
||||
}
|
||||
List<int> codeUnits = comments.codeUnits;
|
||||
|
||||
while (index >= 0) {
|
||||
// DartDocTest starts at (the current) $index --- now find the end,
|
||||
// parenthesis, comma etc.
|
||||
// TODO: This doesn't work for string literals with e.g. '(' and ',' in it.
|
||||
int parenDepth = 0;
|
||||
int firstParen = -1;
|
||||
int commaAt = -1;
|
||||
while (index < comments.length) {
|
||||
int codeUnit = codeUnits[index++];
|
||||
if (codeUnit == $OPEN_PAREN) {
|
||||
parenDepth++;
|
||||
if (parenDepth == 1) {
|
||||
firstParen = index;
|
||||
}
|
||||
} else if (codeUnit == $CLOSE_PAREN) {
|
||||
parenDepth--;
|
||||
if (parenDepth == 0) {
|
||||
break;
|
||||
}
|
||||
} else if (parenDepth == 1 && commaAt < 0 && codeUnit == $COMMA) {
|
||||
commaAt = index;
|
||||
}
|
||||
}
|
||||
|
||||
int end = index;
|
||||
if (parenDepth != 0 || firstParen < 0 || commaAt < 0) {
|
||||
// TODO: Insert code-snippet that didn't parse...
|
||||
result.add(new TestParseError("Parse error for test"));
|
||||
} else {
|
||||
result.add(new Test(
|
||||
comments.substring(firstParen, commaAt - 1).trim(),
|
||||
comments.substring(commaAt, end - 1).trim(),
|
||||
));
|
||||
}
|
||||
index = comments.indexOf("DartDocTest(", index);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class AmendedFileSystem implements FileSystem {
|
||||
static Uri mainUri = Uri.parse("dartdoctest:main");
|
||||
final FileSystem fs;
|
||||
late Uri amendFileUri;
|
||||
Uri? amendImportUri;
|
||||
late String amendWith;
|
||||
|
||||
AmendedFileSystem(this.fs);
|
||||
|
||||
@override
|
||||
FileSystemEntity entityForUri(Uri uri) {
|
||||
if (uri == mainUri) {
|
||||
return DartdoctestMainFile(amendImportUri ?? amendFileUri);
|
||||
}
|
||||
return new AmendedFileSystemEntity(
|
||||
fs.entityForUri(uri), uri == amendFileUri ? amendWith : null);
|
||||
}
|
||||
}
|
||||
|
||||
class DartdoctestMainFile implements FileSystemEntity {
|
||||
late final String content;
|
||||
late final List<int> contentBytes = utf8.encode(content);
|
||||
|
||||
DartdoctestMainFile(Uri amendImportUri) {
|
||||
content = """
|
||||
// Copyright (c) 2021, 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.
|
||||
|
||||
// @dart = 2.9
|
||||
|
||||
import "$amendImportUri" as tester;
|
||||
import "dart:isolate";
|
||||
|
||||
Future<void> main(List<String> args, SendPort port) async {
|
||||
DartDocTest test = new DartDocTest(port);
|
||||
await tester.\$dart\$doc\$test\$tester(test);
|
||||
port.send("$_portMessageDone");
|
||||
}
|
||||
|
||||
class DartDocTest {
|
||||
final SendPort port;
|
||||
|
||||
DartDocTest(this.port);
|
||||
|
||||
void test(dynamic actual, dynamic expected) {
|
||||
port.send("$_portMessageTest");
|
||||
if (_testImpl(actual, expected)) {
|
||||
port.send("$_portMessageGood");
|
||||
} else {
|
||||
port.send("$_portMessageBad");
|
||||
port.send("$_portMessageBadDetails: Expected '\$expected'; got '\$actual'.");
|
||||
}
|
||||
}
|
||||
|
||||
void crash(dynamic error) {
|
||||
port.send("$_portMessageTest");
|
||||
port.send("$_portMessageCrash");
|
||||
}
|
||||
|
||||
void parseError(String message) {
|
||||
port.send("$_portMessageTest");
|
||||
port.send("$_portMessageParseError: \$message");
|
||||
}
|
||||
|
||||
bool _testImpl(dynamic actual, dynamic expected) {
|
||||
if (identical(actual, expected)) return true;
|
||||
if (actual == expected) return true;
|
||||
if (actual == null || expected == null) return false;
|
||||
if (actual is List && expected is List) {
|
||||
if (actual.runtimeType != expected.runtimeType) return false;
|
||||
if (actual.length != expected.length) return false;
|
||||
for (int i = 0; i < actual.length; i++) {
|
||||
if (actual[i] != expected[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (actual is List || expected is List) return false;
|
||||
|
||||
if (actual is Map && expected is Map) {
|
||||
if (actual.runtimeType != expected.runtimeType) return false;
|
||||
if (actual.length != expected.length) return false;
|
||||
for (dynamic key in actual.keys) {
|
||||
if (!expected.containsKey(key)) return false;
|
||||
if (actual[key] != expected[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (actual is Map || expected is Map) return false;
|
||||
|
||||
if (actual is Set && expected is Set) {
|
||||
if (actual.runtimeType != expected.runtimeType) return false;
|
||||
if (actual.length != expected.length) return false;
|
||||
for (dynamic value in actual) {
|
||||
if (!expected.contains(value)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (actual is Set || expected is Set) return false;
|
||||
|
||||
// More stuff?
|
||||
return false;
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> exists() {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> existsAsyncIfPossible() {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> readAsBytes() {
|
||||
return Future.value(contentBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> readAsBytesAsyncIfPossible() {
|
||||
return Future.value(contentBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readAsString() {
|
||||
return Future.value(content);
|
||||
}
|
||||
|
||||
@override
|
||||
Uri get uri => AmendedFileSystem.mainUri;
|
||||
}
|
||||
|
||||
class AmendedFileSystemEntity implements FileSystemEntity {
|
||||
final FileSystemEntity entityForUri;
|
||||
final String? amendWith;
|
||||
AmendedFileSystemEntity(this.entityForUri, this.amendWith);
|
||||
|
||||
@override
|
||||
Future<bool> exists() {
|
||||
return entityForUri.exists();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> existsAsyncIfPossible() {
|
||||
return entityForUri.existsAsyncIfPossible();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> readAsBytes() async {
|
||||
List<int> result = await entityForUri.readAsBytes();
|
||||
return _amendIfNeeded(result);
|
||||
}
|
||||
|
||||
List<int> _amendIfNeeded(List<int> existing) {
|
||||
final String? amendWith = this.amendWith;
|
||||
if (amendWith != null) {
|
||||
List<int> encoded = utf8.encode(amendWith);
|
||||
if (encoded.length > 0) {
|
||||
Uint8List combined =
|
||||
new Uint8List(existing.length + 1 + encoded.length);
|
||||
combined.setRange(0, existing.length, existing);
|
||||
combined[existing.length] = $LF;
|
||||
combined.setRange(existing.length + 1, combined.length, encoded);
|
||||
return combined;
|
||||
}
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> readAsBytesAsyncIfPossible() async {
|
||||
List<int> result = await entityForUri.readAsBytesAsyncIfPossible();
|
||||
return _amendIfNeeded(result);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> readAsString() async {
|
||||
String result = await entityForUri.readAsString();
|
||||
if (amendWith != null) return "$result\n$amendWith";
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Uri get uri => entityForUri.uri;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2021, 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.
|
||||
|
||||
// @dart = 2.9
|
||||
|
||||
import 'dart_doctest_impl.dart';
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
DartDocTest dartDocTest = new DartDocTest();
|
||||
for (String filename in args) {
|
||||
await dartDocTest.process(Uri.base.resolve(filename));
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,34 @@ class _ImportTableBuilder extends RecursiveVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
/// DartDocTest(
|
||||
/// relativeUriPath(
|
||||
/// Uri.parse("file:///path/to/file1.dart"),
|
||||
/// Uri.parse("file:///path/to/file2.dart"),
|
||||
/// ),
|
||||
/// "file1.dart"
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// relativeUriPath(
|
||||
/// Uri.parse("file:///path/to/a/file1.dart"),
|
||||
/// Uri.parse("file:///path/to/file2.dart"),
|
||||
/// ),
|
||||
/// "a/file1.dart"
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// relativeUriPath(
|
||||
/// Uri.parse("file:///path/to/file1.dart"),
|
||||
/// Uri.parse("file:///path/to/b/file2.dart"),
|
||||
/// ),
|
||||
/// "../file1.dart"
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// relativeUriPath(
|
||||
/// Uri.parse("file:///path/to/file1.dart"),
|
||||
/// Uri.parse("file:///different/path/to/file2.dart"),
|
||||
/// ),
|
||||
/// "../../../path/to/file1.dart"
|
||||
/// )
|
||||
String relativeUriPath(Uri target, Uri ref) {
|
||||
List<String> targetSegments = target.pathSegments;
|
||||
List<String> refSegments = ref.pathSegments;
|
||||
|
||||
@@ -417,8 +417,86 @@ class _InnerTypeSubstitutor extends _TypeSubstitutor {
|
||||
///
|
||||
/// Here `!` denotes `Nullability.nonNullable`, `?` denotes
|
||||
/// `Nullability.nullable`, `*` denotes `Nullability.legacy`, and `%` denotes
|
||||
/// `Nullability.neither`. The table elements marked with N/A denote the
|
||||
/// `Nullability.undetermined`. The table elements marked with N/A denote the
|
||||
/// cases that should yield a type error before the substitution is performed.
|
||||
///
|
||||
/// a is nonNullable:
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nonNullable, Nullability.nonNullable),
|
||||
/// Nullability.nonNullable
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nonNullable, Nullability.nullable),
|
||||
/// Nullability.nullable
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nonNullable, Nullability.legacy),
|
||||
/// Nullability.legacy
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nonNullable, Nullability.undetermined),
|
||||
/// Nullability.nonNullable
|
||||
/// )
|
||||
///
|
||||
/// a is nullable:
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nullable, Nullability.nullable),
|
||||
/// Nullability.nullable
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nullable, Nullability.legacy),
|
||||
/// Nullability.nullable
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.nullable, Nullability.undetermined),
|
||||
/// Nullability.nullable
|
||||
/// )
|
||||
///
|
||||
/// a is legacy:
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.legacy, Nullability.nonNullable),
|
||||
/// Nullability.legacy
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.legacy, Nullability.nullable),
|
||||
/// Nullability.nullable
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.legacy, Nullability.legacy),
|
||||
/// Nullability.legacy
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.legacy, Nullability.undetermined),
|
||||
/// Nullability.legacy
|
||||
/// )
|
||||
///
|
||||
/// a is undetermined:
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.undetermined, Nullability.nullable),
|
||||
/// Nullability.nullable
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.undetermined, Nullability.legacy),
|
||||
/// Nullability.legacy
|
||||
/// )
|
||||
/// DartDocTest(
|
||||
/// combineNullabilitiesForSubstitution(
|
||||
/// Nullability.undetermined, Nullability.undetermined),
|
||||
/// Nullability.undetermined
|
||||
/// )
|
||||
Nullability combineNullabilitiesForSubstitution(Nullability a, Nullability b) {
|
||||
// In the table above we may extend the function given by it, replacing N/A
|
||||
// with whatever is easier to implement. In this implementation, we extend
|
||||
|
||||
Reference in New Issue
Block a user