From 5b5956bd4e0332ed65e3579f3b7a78d0a7bddcb7 Mon Sep 17 00:00:00 2001 From: Jens Johansen Date: Tue, 22 Jun 2021 08:12:58 +0000 Subject: [PATCH] [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 Reviewed-by: Dmitry Stefantsov Reviewed-by: Johnni Winther --- .../lib/src/fasta/incremental_compiler.dart | 49 ++ .../lib/src/fasta/kernel/body_builder.dart | 18 +- pkg/front_end/test/binary_md_dill_reader.dart | 127 ++-- pkg/front_end/test/dartdoctest_suite.dart | 71 +++ pkg/front_end/test/dartdoctest_suite.status | 4 + .../test/spell_checking_list_tests.txt | 9 + pkg/front_end/test/unit_test_suites_impl.dart | 7 + pkg/front_end/testing.json | 13 + pkg/front_end/tool/dart_doctest_impl.dart | 582 ++++++++++++++++++ pkg/front_end/tool/dart_doctest_runner.dart | 14 + pkg/kernel/lib/import_table.dart | 28 + pkg/kernel/lib/type_algebra.dart | 80 ++- 12 files changed, 932 insertions(+), 70 deletions(-) create mode 100644 pkg/front_end/test/dartdoctest_suite.dart create mode 100644 pkg/front_end/test/dartdoctest_suite.status create mode 100644 pkg/front_end/tool/dart_doctest_impl.dart create mode 100644 pkg/front_end/tool/dart_doctest_runner.dart diff --git a/pkg/front_end/lib/src/fasta/incremental_compiler.dart b/pkg/front_end/lib/src/fasta/incremental_compiler.dart index e4dbd47646a..0cc4f5d2003 100644 --- a/pkg/front_end/lib/src/fasta/incremental_compiler.dart +++ b/pkg/front_end/lib/src/fasta/incremental_compiler.dart @@ -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); diff --git a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart index 9e7c46c6ca2..953a36059b1 100644 --- a/pkg/front_end/lib/src/fasta/kernel/body_builder.dart +++ b/pkg/front_end/lib/src/fasta/kernel/body_builder.dart @@ -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 diff --git a/pkg/front_end/test/binary_md_dill_reader.dart b/pkg/front_end/test/binary_md_dill_reader.dart index 7894e3c0b86..f427697077e 100644 --- a/pkg/front_end/test/binary_md_dill_reader.dart +++ b/pkg/front_end/test/binary_md_dill_reader.dart @@ -6,74 +6,6 @@ import "dart:math" as math; -main() { - List test = BinaryMdDillReader._getGenerics("Pair"); - if (test.length != 2 || test[0] != "A" || test[1] != "B") { - throw "Expected [A, B] got $test"; - } - - test = BinaryMdDillReader._getGenerics("List"); - if (test.length != 1 || test[0] != "Expression") { - throw "Expected [Expression] got $test"; - } - - test = BinaryMdDillReader._getGenerics("List>"); - if (test.length != 1 || test[0] != "Pair") { - throw "Expected [Pair] got $test"; - } - - test = BinaryMdDillReader._getGenerics("RList>"); - if (test.length != 1 || test[0] != "Pair") { - throw "Expected [Pair] got $test"; - } - - test = - BinaryMdDillReader._getGenerics("List>"); - if (test.length != 1 || test[0] != "Pair") { - throw "Expected [Pair] got $test"; - } - - test = BinaryMdDillReader._getGenerics( - "List>"); - if (test.length != 1 || - test[0] != "Pair") { - throw "Expected [Pair] got $test"; - } - - test = BinaryMdDillReader._getGenerics( - "List>"); - if (test.length != 1 || - test[0] != "Pair") { - throw "Expected [Pair] got $test"; - } - - test = BinaryMdDillReader._getGenerics("Option>"); - if (test.length != 1 || test[0] != "List") { - throw "Expected [List] got $test"; - } - - test = BinaryMdDillReader._getGenerics("Foo>"); - if (test.length != 1 || test[0] != "Bar") { - throw "Expected [Bar] got $test"; - } - - test = BinaryMdDillReader._getGenerics("Foo, E>"); - if (test.length != 3 || - test[0] != "A" || - test[1] != "B" || - test[2] != "E") { - throw "Expected [A, B, E] got $test"; - } - - test = BinaryMdDillReader._getGenerics("Foo>>>, H>"); - if (test.length != 3 || - test[0] != "A" || - test[1] != "B>>>" || - test[2] != "H") { - throw "Expected [A, B>>>, 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"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("List"), ["Expression"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("List>"), + /// ["Pair"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("RList>"), + /// ["Pair"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics( + /// "List>"), + /// ["Pair"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics( + /// "List>"), + /// ["Pair"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics( + /// "List>"), + /// ["Pair"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("Option>"), + /// ["List"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("Foo>"), ["Bar"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("Foo, E>"), + /// ["A", "B", "E"] + /// ) + /// DartDocTest( + /// BinaryMdDillReader._getGenerics("Foo>>>, H>"), + /// ["A", "B>>>", "H"] + /// ) + /// + /// Expected failing run (expects to fail with unbalanced < >). + /// TODO: Support this more elegantly. + /// DartDocTest(() { + /// try { + /// BinaryMdDillReader._getGenerics("Foo"); + /// return false; + /// } catch(e) { + /// return true; + /// } + /// }(), + /// true + /// ) + /// static List _getGenerics(String s) { s = s.substring(s.indexOf("<") + 1, s.lastIndexOf(">")); // Check that any '<' and '>' are balanced and split entries on comma for diff --git a/pkg/front_end/test/dartdoctest_suite.dart b/pkg/front_end/test/dartdoctest_suite.dart new file mode 100644 index 00000000000..77266f1762e --- /dev/null +++ b/pkg/front_end/test/dartdoctest_suite.dart @@ -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 arguments = const []]) => + runMe(arguments, createContext, configurationPath: "../testing.json"); + +Future createContext( + Chain suite, Map environment) async { + return new Context(suite.name); +} + +class Context extends ChainContext { + final String suiteName; + + Context(this.suiteName); + + final List steps = const [ + const DartDocTestStep(), + ]; + + // Override special handling of negative tests. + @override + Result processTestResult( + TestDescription description, Result result, bool last) { + return result; + } + + Stream list(Chain suite) async* { + await for (TestDescription entry in super.list(suite)) { + List 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 tests; + + DartDocTestTestDescription(this.shortName, this.uri, this.tests); +} + +class DartDocTestStep extends Step { + const DartDocTestStep(); + + String get name => "DartDocTest"; + + Future> run( + DartDocTestTestDescription description, Context context) async { + bool result = await context.dartDocTest + .compileAndRun(description.uri, description.tests); + if (result) { + return new Result.pass(description); + } else { + return new Result.fail(description); + } + } +} diff --git a/pkg/front_end/test/dartdoctest_suite.status b/pkg/front_end/test/dartdoctest_suite.status new file mode 100644 index 00000000000..48716a2eff2 --- /dev/null +++ b/pkg/front_end/test/dartdoctest_suite.status @@ -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. + diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt index 58478ccb392..e88b901bb0e 100644 --- a/pkg/front_end/test/spell_checking_list_tests.txt +++ b/pkg/front_end/test/spell_checking_list_tests.txt @@ -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 diff --git a/pkg/front_end/test/unit_test_suites_impl.dart b/pkg/front_end/test/unit_test_suites_impl.dart index c9d550c2263..5c6ccffc36d 100644 --- a/pkg/front_end/test/unit_test_suites_impl.dart +++ b/pkg/front_end/test/unit_test_suites_impl.dart @@ -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 suites = [ + const Suite( + "dartdoctest", + dartdoctest.createContext, + "../testing.json", + shardCount: 1, + ), const Suite( "fasta/expression", expression.createContext, diff --git a/pkg/front_end/testing.json b/pkg/front_end/testing.json index ab205e06c3e..8c5b79ebb07 100644 --- a/pkg/front_end/testing.json +++ b/pkg/front_end/testing.json @@ -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", diff --git a/pkg/front_end/tool/dart_doctest_impl.dart b/pkg/front_end/tool/dart_doctest_impl.dart new file mode 100644 index 00000000000..7939fecb51b --- /dev/null +++ b/pkg/front_end/tool/dart_doctest_impl.dart @@ -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 process(Uri uri) async { + print("\n\nProcessing $uri"); + Stopwatch stopwatch = new Stopwatch()..start(); + + // Extract test cases in file. + List 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 compileAndRun(Uri uri, List tests) async { + errors = false; + + // Create code to amend the file with. + StringBuffer sb = new StringBuffer(); + if (tests.isNotEmpty) { + sb.writeln( + r"Future $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 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 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 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 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 result = []; + int index = comments.indexOf("DartDocTest("); + if (index < 0) { + return result; + } + List 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 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 main(List 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 exists() { + return Future.value(true); + } + + @override + Future existsAsyncIfPossible() { + return Future.value(true); + } + + @override + Future> readAsBytes() { + return Future.value(contentBytes); + } + + @override + Future> readAsBytesAsyncIfPossible() { + return Future.value(contentBytes); + } + + @override + Future 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 exists() { + return entityForUri.exists(); + } + + @override + Future existsAsyncIfPossible() { + return entityForUri.existsAsyncIfPossible(); + } + + @override + Future> readAsBytes() async { + List result = await entityForUri.readAsBytes(); + return _amendIfNeeded(result); + } + + List _amendIfNeeded(List existing) { + final String? amendWith = this.amendWith; + if (amendWith != null) { + List 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> readAsBytesAsyncIfPossible() async { + List result = await entityForUri.readAsBytesAsyncIfPossible(); + return _amendIfNeeded(result); + } + + @override + Future readAsString() async { + String result = await entityForUri.readAsString(); + if (amendWith != null) return "$result\n$amendWith"; + return result; + } + + @override + Uri get uri => entityForUri.uri; +} diff --git a/pkg/front_end/tool/dart_doctest_runner.dart b/pkg/front_end/tool/dart_doctest_runner.dart new file mode 100644 index 00000000000..7791e66cad9 --- /dev/null +++ b/pkg/front_end/tool/dart_doctest_runner.dart @@ -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 main(List args) async { + DartDocTest dartDocTest = new DartDocTest(); + for (String filename in args) { + await dartDocTest.process(Uri.base.resolve(filename)); + } +} diff --git a/pkg/kernel/lib/import_table.dart b/pkg/kernel/lib/import_table.dart index d6bf7ee68bd..bf9f7d5476b 100644 --- a/pkg/kernel/lib/import_table.dart +++ b/pkg/kernel/lib/import_table.dart @@ -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 targetSegments = target.pathSegments; List refSegments = ref.pathSegments; diff --git a/pkg/kernel/lib/type_algebra.dart b/pkg/kernel/lib/type_algebra.dart index c27349422e4..4d28929229b 100644 --- a/pkg/kernel/lib/type_algebra.dart +++ b/pkg/kernel/lib/type_algebra.dart @@ -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