[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:
Jens Johansen
2021-06-22 08:12:58 +00:00
committed by commit-bot@chromium.org
parent fb5465c624
commit 5b5956bd4e
12 changed files with 932 additions and 70 deletions
@@ -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
+59 -68
View File
@@ -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
+71
View File
@@ -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,
+13
View File
@@ -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",
+582
View File
@@ -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));
}
}
+28
View File
@@ -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;
+79 -1
View File
@@ -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