e40cad30a3
Part of removing uses of 'fasta'. TEST=existing Change-Id: Ibf144a126d8ff4ba15b6cfd2c4fb898e9a499362 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/371685 Reviewed-by: Alexander Markov <alexmarkov@google.com> Reviewed-by: Jens Johansen <jensj@google.com> Commit-Queue: Johnni Winther <johnniwinther@google.com> Reviewed-by: Srujan Gaddam <srujzs@google.com>
938 lines
31 KiB
Dart
938 lines
31 KiB
Dart
// 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/messages/codes.dart' as codes;
|
|
import 'package:_fe_analyzer_shared/src/parser/async_modifier.dart'
|
|
show AsyncModifier;
|
|
import 'package:_fe_analyzer_shared/src/parser/forwarding_listener.dart'
|
|
show NullListener;
|
|
import 'package:_fe_analyzer_shared/src/parser/parser.dart' show Parser;
|
|
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:front_end/src/api_prototype/compiler_options.dart';
|
|
import 'package:front_end/src/api_prototype/file_system.dart';
|
|
import 'package:front_end/src/api_prototype/incremental_kernel_generator.dart';
|
|
import 'package:front_end/src/api_prototype/memory_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/builder/library_builder.dart';
|
|
import 'package:front_end/src/codes/cfe_codes.dart';
|
|
import 'package:front_end/src/fasta/combinator.dart';
|
|
import 'package:front_end/src/fasta/command_line_reporting.dart'
|
|
as command_line_reporting;
|
|
import 'package:front_end/src/fasta/compiler_context.dart';
|
|
// ignore: import_of_legacy_library_into_null_safe
|
|
import 'package:front_end/src/fasta/dill/dill_library_builder.dart';
|
|
// ignore: import_of_legacy_library_into_null_safe
|
|
import 'package:front_end/src/fasta/dill/dill_target.dart';
|
|
import 'package:front_end/src/fasta/hybrid_file_system.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:front_end/src/fasta/scope.dart';
|
|
import 'package:front_end/src/fasta/source/diet_parser.dart'
|
|
show useImplicitCreationExpressionInCfe;
|
|
// ignore: import_of_legacy_library_into_null_safe
|
|
import 'package:front_end/src/fasta/source/source_library_builder.dart';
|
|
import 'package:front_end/src/fasta/source/source_loader.dart';
|
|
import 'package:front_end/src/fasta/uri_translator.dart';
|
|
import 'package:kernel/kernel.dart' as kernel
|
|
show Combinator, Component, LibraryDependency, Location, Source;
|
|
import 'package:kernel/reference_from_index.dart';
|
|
import 'package:kernel/target/targets.dart';
|
|
// ignore: import_of_legacy_library_into_null_safe
|
|
import 'package:vm/modular/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 _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 {
|
|
DocTestIncrementalCompiler? incrementalCompiler;
|
|
late CompilerOptions options;
|
|
late ProcessedOptions processedOpts;
|
|
bool errors = false;
|
|
List<String> errorStrings = [];
|
|
final FileSystem? underlyingFileSystem;
|
|
final bool silent;
|
|
|
|
DartDocTest({this.underlyingFileSystem, this.silent = false});
|
|
|
|
FileSystem _getFileSystem() =>
|
|
underlyingFileSystem ?? StandardFileSystem.instance;
|
|
|
|
void _print(Object? object) {
|
|
if (!silent) print(object);
|
|
}
|
|
|
|
/// All-in-one. Process a file and return if it was good.
|
|
Future<List<TestResult>> process(Uri uri) async {
|
|
_print("\n\nProcessing $uri");
|
|
Stopwatch stopwatch = new Stopwatch()..start();
|
|
|
|
// Extract test cases in file.
|
|
List<Test> tests = await extractTestsFromUri(uri);
|
|
|
|
if (tests.isEmpty) {
|
|
_print("No tests found in file in ${stopwatch.elapsedMilliseconds} ms.");
|
|
return [];
|
|
}
|
|
_print("Found ${tests.length} test(s) in file "
|
|
"in ${stopwatch.elapsedMilliseconds} ms.");
|
|
|
|
return await compileAndRun(uri, tests);
|
|
}
|
|
|
|
Future<List<Test>> extractTestsFromUri(Uri uri) async {
|
|
// Extract test cases in file.
|
|
FileSystemEntity file = _getFileSystem().entityForUri(uri);
|
|
List<int> rawBytes = await file.readAsBytes();
|
|
return extractTests(
|
|
rawBytes is Uint8List ? rawBytes : new Uint8List.fromList(rawBytes),
|
|
uri);
|
|
}
|
|
|
|
Future<List<TestResult>> compileAndRun(Uri uri, List<Test> tests,
|
|
{bool silent = false}) async {
|
|
errors = false;
|
|
errorStrings.clear();
|
|
|
|
// 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(\"Parse error @ ${test.position}\");");
|
|
} else if (test is ExpectTest) {
|
|
sb.writeln("try {");
|
|
sb.writeln(" dartDocTest.test(${test.call}, ${test.result});");
|
|
sb.writeln("} catch (e) {");
|
|
sb.writeln(" dartDocTest.crash(e);");
|
|
sb.writeln("}");
|
|
} else {
|
|
throw "Unknown test type: ${test.runtimeType}";
|
|
}
|
|
}
|
|
sb.writeln("}");
|
|
}
|
|
|
|
if (incrementalCompiler == null) {
|
|
setupIncrementalCompiler(uri);
|
|
}
|
|
|
|
processedOpts.inputs.clear();
|
|
processedOpts.inputs.add(uri);
|
|
HybridFileSystem fileSystem = new HybridFileSystem(
|
|
new MemoryFileSystem(new Uri(scheme: "dartdoctest", path: "/")),
|
|
_getFileSystem());
|
|
options.fileSystem = fileSystem;
|
|
processedOpts.clearFileSystemCache();
|
|
// Invalidate package uri to force re-finding of packages
|
|
// (e.g. if we're now compiling somewhere else).
|
|
incrementalCompiler!.invalidate(processedOpts.packagesUri);
|
|
|
|
Stopwatch stopwatch = new Stopwatch()..start();
|
|
IncrementalCompilerResult compilerResult =
|
|
await incrementalCompiler!.computeDelta(entryPoints: [uri]);
|
|
kernel.Component component = compilerResult.component;
|
|
if (errors) {
|
|
_print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
|
|
return [
|
|
new TestResult(null, TestOutcome.CompilationError)
|
|
..message = errorStrings.join("\n")
|
|
];
|
|
}
|
|
_print("Compiled (1) in ${stopwatch.elapsedMilliseconds} ms.");
|
|
stopwatch.reset();
|
|
|
|
await incrementalCompiler!.compileDartDocTestLibrary(
|
|
sb.toString(), component.uriToSource[uri]?.importUri ?? uri);
|
|
|
|
final Uri dartDocMainUri = new Uri(scheme: "dartdoctest", path: "main");
|
|
fileSystem.memory
|
|
.entityForUri(dartDocMainUri)
|
|
.writeAsStringSync(mainFileContent);
|
|
|
|
incrementalCompiler!.invalidate(dartDocMainUri);
|
|
IncrementalCompilerResult compilerMainResult = await incrementalCompiler!
|
|
.computeDelta(entryPoints: [dartDocMainUri], fullComponent: true);
|
|
kernel.Component componentMain = compilerMainResult.component;
|
|
if (errors) {
|
|
_print("Got errors in ${stopwatch.elapsedMilliseconds} ms.");
|
|
return [
|
|
new TestResult(null, TestOutcome.CompilationError)
|
|
..message = errorStrings.join("\n")
|
|
];
|
|
}
|
|
_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;
|
|
Test? currentTest;
|
|
|
|
List<TestResult> result = [];
|
|
|
|
communicationPort.listen((message) {
|
|
if (done) {
|
|
throw "Didn't expect any more messages. Got '$message'";
|
|
}
|
|
if (message == _portMessageTest) {
|
|
currentTest = tests[testCount];
|
|
testCount++;
|
|
} else if (message == _portMessageGood) {
|
|
goodCount++;
|
|
result.add(new TestResult(currentTest!, TestOutcome.Pass));
|
|
} else if (message.toString().startsWith("$_portMessageBad: ")) {
|
|
badCount++;
|
|
String strippedMessage =
|
|
message.toString().substring("$_portMessageBad: ".length);
|
|
result.add(new TestResult(currentTest!, TestOutcome.Failed)
|
|
..message = strippedMessage);
|
|
_print(strippedMessage);
|
|
} else if (message.toString().startsWith("$_portMessageCrash: ")) {
|
|
String strippedMessage =
|
|
message.toString().substring("$_portMessageCrash: ".length);
|
|
result.add(new TestResult(currentTest!, TestOutcome.Crash)
|
|
..message = strippedMessage);
|
|
crashCount++;
|
|
_print(strippedMessage);
|
|
} else if (message.toString().startsWith("$_portMessageParseError: ")) {
|
|
String strippedMessage =
|
|
message.toString().substring("$_portMessageParseError: ".length);
|
|
result.add(
|
|
new TestResult(currentTest!, TestOutcome.TestCompilationError)
|
|
..message = strippedMessage);
|
|
parseErrorCount++;
|
|
_print(strippedMessage);
|
|
} 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 [new TestResult(null, TestOutcome.RuntimeError)];
|
|
} else if (!done) {
|
|
_print(
|
|
"Didn't complete correctly in ${stopwatch.elapsedMilliseconds} ms.");
|
|
return [new TestResult(null, TestOutcome.FrameworkError)];
|
|
} else if (testCount != tests.length) {
|
|
_print("Didn't complete with error but ran "
|
|
"${testCount} tests while expecting ${tests.length} "
|
|
"in ${stopwatch.elapsedMilliseconds} ms.");
|
|
return [new TestResult(null, TestOutcome.FrameworkError)];
|
|
} else {
|
|
_print("Processed $testCount test(s) "
|
|
"in ${stopwatch.elapsedMilliseconds} ms.");
|
|
if (goodCount == testCount &&
|
|
badCount == 0 &&
|
|
crashCount == 0 &&
|
|
parseErrorCount == 0) {
|
|
_print("All tests passed.");
|
|
} else {
|
|
_print("$goodCount OK; "
|
|
"$badCount bad; "
|
|
"$crashCount crashed; "
|
|
"$parseErrorCount parse errors.");
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
void setupIncrementalCompiler(Uri uri) {
|
|
options = getOptions();
|
|
TargetFlags targetFlags = new TargetFlags();
|
|
// TODO: Target could possible be something else...
|
|
Target target = new VmTarget(targetFlags);
|
|
options.target = target;
|
|
options.omitPlatform = true;
|
|
options.onDiagnostic = (DiagnosticMessage message) {
|
|
_print(message.plainTextFormatted.first);
|
|
if (message.severity == Severity.error) {
|
|
errors = true;
|
|
for (String errorString in message.plainTextFormatted) {
|
|
errorStrings.add(errorString);
|
|
}
|
|
}
|
|
};
|
|
processedOpts = new ProcessedOptions(options: options, inputs: [uri]);
|
|
CompilerContext compilerContext = new CompilerContext(processedOpts);
|
|
this.incrementalCompiler = new DocTestIncrementalCompiler(compilerContext);
|
|
}
|
|
}
|
|
|
|
final String mainFileContent = """
|
|
// 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 "${DocTestIncrementalCompiler.dartDocTestUri}" 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: Expected '\$expected'; got '\$actual'.");
|
|
}
|
|
}
|
|
|
|
void crash(dynamic error) {
|
|
port.send("$_portMessageTest");
|
|
port.send("$_portMessageCrash: \$error");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
""";
|
|
|
|
List<Test> extractTests(Uint8List rawBytes, Uri uriForReporting) {
|
|
String rawString = utf8.decode(rawBytes);
|
|
List<int> lineStarts = [];
|
|
Token firstToken = scanRawBytes(rawBytes, lineStarts: lineStarts);
|
|
kernel.Source source =
|
|
new kernel.Source(lineStarts, rawBytes, uriForReporting, uriForReporting);
|
|
Token token = firstToken;
|
|
List<Test> tests = [];
|
|
while (true) {
|
|
CommentToken? comment = token.precedingComments;
|
|
if (comment != null) {
|
|
tests.addAll(extractTestsFromComment(comment, rawString, source));
|
|
}
|
|
if (token.isEof) break;
|
|
Token? next = token.next;
|
|
if (next == null) break;
|
|
token = next;
|
|
}
|
|
return tests;
|
|
}
|
|
|
|
Token scanRawBytes(Uint8List rawBytes, {List<int>? lineStarts}) {
|
|
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 result = scanner.tokenize();
|
|
if (lineStarts != null) {
|
|
lineStarts.clear();
|
|
lineStarts.addAll(scanner.lineStarts);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const int $LF = 10;
|
|
const int $SPACE = 32;
|
|
const int $STAR = 42;
|
|
|
|
class Test {}
|
|
|
|
class ExpectTest implements Test {
|
|
final String call;
|
|
final String result;
|
|
|
|
ExpectTest(this.call, this.result);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is! ExpectTest) return false;
|
|
if (other.call != call) return false;
|
|
if (other.result != result) return false;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return "ExpectTest[$call, $result]";
|
|
}
|
|
}
|
|
|
|
class TestParseError implements Test {
|
|
final String message;
|
|
final int position;
|
|
|
|
TestParseError(this.message, this.position);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is! TestParseError) return false;
|
|
if (other.message != message) return false;
|
|
if (other.position != position) return false;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return "TestParseError[$position, $message]";
|
|
}
|
|
}
|
|
|
|
enum TestOutcome {
|
|
Pass,
|
|
Failed,
|
|
Crash,
|
|
TestCompilationError,
|
|
CompilationError,
|
|
RuntimeError,
|
|
FrameworkError
|
|
}
|
|
|
|
class TestResult {
|
|
final Test? test;
|
|
final TestOutcome outcome;
|
|
String? message;
|
|
|
|
TestResult(this.test, this.outcome);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is! TestResult) return false;
|
|
if (other.test != test) return false;
|
|
if (other.outcome != outcome) return false;
|
|
if (other.message != message) return false;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
if (message != null) {
|
|
return "TestResult[$outcome, $test, $message]";
|
|
}
|
|
return "TestResult[$outcome, $test]";
|
|
}
|
|
}
|
|
|
|
List<Test> extractTestsFromComment(
|
|
CommentToken comment, String rawString, kernel.Source source) {
|
|
CommentString commentsData = extractComments(comment, rawString);
|
|
final String comments = commentsData.string;
|
|
List<Test> result = [];
|
|
int index = comments.indexOf("DartDocTest(");
|
|
if (index < 0) {
|
|
return result;
|
|
}
|
|
|
|
Test scanDartDoc(int scanOffset) {
|
|
final Token firstToken =
|
|
scanRawBytes(utf8.encode(comments.substring(scanOffset)));
|
|
final ErrorListener listener = new ErrorListener();
|
|
final Parser parser = new Parser(listener,
|
|
useImplicitCreationExpression: useImplicitCreationExpressionInCfe);
|
|
parser.asyncState = AsyncModifier.Async;
|
|
|
|
final Token pastErrors = parser.skipErrorTokens(firstToken);
|
|
assert(pastErrors.isIdentifier);
|
|
assert(pastErrors.lexeme == "DartDocTest");
|
|
|
|
final Token startParen = pastErrors.next!;
|
|
assert(identical("(", startParen.stringValue));
|
|
|
|
// Advance index so we don't parse the same thing again (for error cases).
|
|
index = scanOffset + startParen.charEnd;
|
|
|
|
final Token beforeComma = parser.parseExpression(startParen);
|
|
final Token comma = beforeComma.next!;
|
|
|
|
if (listener.hasErrors) {
|
|
StringBuffer sb = new StringBuffer();
|
|
int firstPosition = _createParseErrorMessages(
|
|
listener, sb, commentsData, scanOffset, source);
|
|
return new TestParseError(sb.toString(), firstPosition);
|
|
} else if (!identical(",", comma.stringValue)) {
|
|
int position = commentsData.charOffset + scanOffset + comma.charOffset;
|
|
Message message = codes.templateExpectedButGot.withArguments(',');
|
|
return new TestParseError(
|
|
_createParseErrorMessage(source, position, comma, comma, message),
|
|
position,
|
|
);
|
|
}
|
|
|
|
Token beforeEndParen = parser.parseExpression(comma);
|
|
Token endParen = beforeEndParen.next!;
|
|
|
|
if (listener.hasErrors) {
|
|
StringBuffer sb = new StringBuffer();
|
|
int firstPosition = _createParseErrorMessages(
|
|
listener, sb, commentsData, scanOffset, source);
|
|
return new TestParseError(sb.toString(), firstPosition);
|
|
} else if (!identical(")", endParen.stringValue)) {
|
|
int position = commentsData.charOffset + scanOffset + endParen.charOffset;
|
|
Message message = codes.templateExpectedButGot.withArguments(')');
|
|
return new TestParseError(
|
|
_createParseErrorMessage(source, position, comma, comma, message),
|
|
position,
|
|
);
|
|
}
|
|
|
|
// Advance index so we don't parse the same thing again (success case).
|
|
index = scanOffset + endParen.charEnd;
|
|
|
|
int startPos = scanOffset + startParen.next!.charOffset;
|
|
int midEndPos = scanOffset + beforeComma.charEnd;
|
|
int midStartPos = scanOffset + comma.next!.charOffset;
|
|
int endPos = scanOffset + beforeEndParen.charEnd;
|
|
return new ExpectTest(
|
|
comments.substring(startPos, midEndPos),
|
|
comments.substring(midStartPos, endPos),
|
|
);
|
|
}
|
|
|
|
while (index >= 0) {
|
|
result.add(scanDartDoc(index));
|
|
index = comments.indexOf("DartDocTest(", index);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int _createParseErrorMessages(ErrorListener listener, StringBuffer sb,
|
|
CommentString commentsData, int scanOffset, kernel.Source source) {
|
|
assert(listener.recoverableErrors.isNotEmpty);
|
|
sb.writeln("Parse error(s):");
|
|
int? firstPosition;
|
|
for (RecoverableError recoverableError in listener.recoverableErrors) {
|
|
final int position = commentsData.charOffset +
|
|
scanOffset +
|
|
recoverableError.startToken.charOffset;
|
|
firstPosition ??= position;
|
|
sb.writeln("");
|
|
sb.write(_createParseErrorMessage(
|
|
source,
|
|
position,
|
|
recoverableError.startToken,
|
|
recoverableError.endToken,
|
|
recoverableError.message,
|
|
));
|
|
}
|
|
return firstPosition!;
|
|
}
|
|
|
|
String _createParseErrorMessage(kernel.Source source, int position,
|
|
Token startToken, Token endToken, Message message) {
|
|
kernel.Location location = source.getLocation(source.importUri!, position);
|
|
return command_line_reporting.formatErrorMessage(
|
|
source.getTextLine(location.line),
|
|
location,
|
|
endToken.charEnd - startToken.charOffset,
|
|
source.importUri!.toString(),
|
|
message.problemMessage);
|
|
}
|
|
|
|
CommentString extractComments(CommentToken comment, String rawString) {
|
|
List<int> fileCodeUnits = rawString.codeUnits;
|
|
final int charOffset = comment.charOffset;
|
|
int expectedCharOffset = charOffset;
|
|
StringBuffer sb = new StringBuffer();
|
|
CommentToken? commentToken = comment;
|
|
bool commentBlock = false;
|
|
bool commentBlockStar = false;
|
|
while (commentToken != null) {
|
|
if (expectedCharOffset != commentToken.charOffset) {
|
|
// Missing spaces/linebreaks.
|
|
assert(expectedCharOffset < commentToken.offset);
|
|
for (int i = expectedCharOffset; i < commentToken.offset; i++) {
|
|
if (fileCodeUnits[i] == $LF) {
|
|
sb.writeCharCode($LF);
|
|
} else {
|
|
sb.write(" ");
|
|
}
|
|
}
|
|
}
|
|
expectedCharOffset = commentToken.charEnd;
|
|
String data = commentToken.lexeme;
|
|
if (!commentBlock) {
|
|
if (data.startsWith("///")) {
|
|
data = data.substring(3);
|
|
sb.write(" ");
|
|
} else if (data.startsWith("//")) {
|
|
data = data.substring(2);
|
|
sb.write(" ");
|
|
} else if (data.startsWith("/**")) {
|
|
data = data.substring(3);
|
|
commentBlock = true;
|
|
commentBlockStar = true;
|
|
sb.write(" ");
|
|
} else if (data.startsWith("/*")) {
|
|
data = data.substring(2);
|
|
commentBlock = true;
|
|
sb.write(" ");
|
|
}
|
|
}
|
|
|
|
if (commentBlock && data.endsWith("*/")) {
|
|
// Remove ending "*/"" as well as "starting" "*" if in a "/**" block.
|
|
List<int> codeUnits = data.codeUnits;
|
|
bool sawNewlineLast = false;
|
|
for (int i = 0; i < codeUnits.length - 2; i++) {
|
|
int codeUnit = codeUnits[i];
|
|
if (codeUnit == $LF) {
|
|
sb.writeCharCode($LF);
|
|
sawNewlineLast = true;
|
|
} else if (codeUnit <= $SPACE) {
|
|
sb.writeCharCode($SPACE);
|
|
} else if (commentBlockStar && sawNewlineLast && codeUnit == $STAR) {
|
|
sb.writeCharCode($SPACE);
|
|
sawNewlineLast = false;
|
|
} else {
|
|
sawNewlineLast = false;
|
|
sb.writeCharCode(codeUnit);
|
|
}
|
|
}
|
|
sb.write(" ");
|
|
commentBlock = false;
|
|
commentBlockStar = false;
|
|
} else {
|
|
sb.write(data);
|
|
}
|
|
Token? next = commentToken.next;
|
|
commentToken = null;
|
|
if (next is CommentToken) commentToken = next;
|
|
}
|
|
return new CommentString(sb.toString(), charOffset);
|
|
}
|
|
|
|
class CommentString {
|
|
final String string;
|
|
final int charOffset;
|
|
|
|
CommentString(this.string, this.charOffset);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (other is! CommentString) return false;
|
|
if (other.string != string) return false;
|
|
if (other.charOffset != charOffset) return false;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return "CommentString[$charOffset, $string]";
|
|
}
|
|
}
|
|
|
|
class ErrorListener extends NullListener {
|
|
List<RecoverableError> recoverableErrors = [];
|
|
|
|
@override
|
|
void handleRecoverableError(
|
|
Message message, Token startToken, Token endToken) {
|
|
super.handleRecoverableError(message, startToken, endToken);
|
|
recoverableErrors.add(new RecoverableError(message, startToken, endToken));
|
|
}
|
|
}
|
|
|
|
class RecoverableError {
|
|
final Message message;
|
|
final Token startToken;
|
|
final Token endToken;
|
|
|
|
RecoverableError(this.message, this.startToken, this.endToken);
|
|
}
|
|
|
|
class DocTestIncrementalCompiler extends IncrementalCompiler {
|
|
static final Uri dartDocTestUri =
|
|
new Uri(scheme: "dartdoctest", path: "tester");
|
|
DocTestIncrementalCompiler(CompilerContext context) : super(context);
|
|
|
|
@override
|
|
bool dontReissueLibraryProblemsFor(Uri? uri) {
|
|
return super.dontReissueLibraryProblemsFor(uri) || uri == dartDocTestUri;
|
|
}
|
|
|
|
@override
|
|
IncrementalKernelTarget createIncrementalKernelTarget(
|
|
FileSystem fileSystem,
|
|
bool includeComments,
|
|
DillTarget dillTarget,
|
|
UriTranslator uriTranslator) {
|
|
return new DocTestIncrementalKernelTarget(
|
|
this, fileSystem, includeComments, dillTarget, uriTranslator);
|
|
}
|
|
|
|
LibraryBuilder? _dartDocTestLibraryBuilder;
|
|
String? _dartDocTestCode;
|
|
|
|
Future<kernel.Component> compileDartDocTestLibrary(
|
|
String dartDocTestCode, Uri libraryUri) async {
|
|
assert(dillTargetForTesting != null && kernelTargetForTesting != null);
|
|
|
|
return await context.runInContext((_) async {
|
|
LibraryBuilder libraryBuilder = kernelTargetForTesting!.loader
|
|
.lookupLoadedLibraryBuilder(libraryUri)!;
|
|
|
|
kernelTargetForTesting!.loader.resetSeenMessages();
|
|
|
|
_dartDocTestLibraryBuilder = libraryBuilder;
|
|
_dartDocTestCode = dartDocTestCode;
|
|
|
|
invalidate(dartDocTestUri);
|
|
IncrementalCompilerResult compilerResult = await computeDelta(
|
|
entryPoints: [dartDocTestUri], fullComponent: true);
|
|
kernel.Component result = compilerResult.component;
|
|
_dartDocTestLibraryBuilder = null;
|
|
_dartDocTestCode = null;
|
|
|
|
kernelTargetForTesting!.uriToSource.remove(dartDocTestUri);
|
|
kernelTargetForTesting!.loader.sourceBytes.remove(dartDocTestUri);
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
SourceLibraryBuilder createDartDocTestLibrary(
|
|
SourceLoader loader, LibraryBuilder libraryBuilder) {
|
|
SourceLibraryBuilder dartDocTestLibrary = new SourceLibraryBuilder(
|
|
importUri: dartDocTestUri,
|
|
fileUri: dartDocTestUri,
|
|
packageLanguageVersion:
|
|
new ImplicitLanguageVersion(libraryBuilder.library.languageVersion),
|
|
loader: loader,
|
|
// TODO(jensj): Should probably set up scopes the same was as it's done
|
|
// (now) for expression compilation.
|
|
scope: libraryBuilder.scope
|
|
.createNestedScope(debugName: "dartdoctest", kind: ScopeKind.library),
|
|
nameOrigin: libraryBuilder,
|
|
isUnsupported: false,
|
|
isAugmentation: false,
|
|
isPatch: false,
|
|
);
|
|
|
|
if (libraryBuilder is DillLibraryBuilder) {
|
|
for (kernel.LibraryDependency dependency
|
|
in libraryBuilder.library.dependencies) {
|
|
if (!dependency.isImport) continue;
|
|
|
|
List<CombinatorBuilder>? combinators;
|
|
|
|
for (kernel.Combinator combinator in dependency.combinators) {
|
|
combinators ??= <CombinatorBuilder>[];
|
|
|
|
combinators.add(combinator.isShow
|
|
? new CombinatorBuilder.show(combinator.names,
|
|
combinator.fileOffset, libraryBuilder.fileUri)
|
|
: new CombinatorBuilder.hide(combinator.names,
|
|
combinator.fileOffset, libraryBuilder.fileUri));
|
|
}
|
|
|
|
dartDocTestLibrary.compilationUnit.addImport(
|
|
metadata: null,
|
|
isAugmentationImport: false,
|
|
uri: dependency.importedLibraryReference.asLibrary.importUri
|
|
.toString(),
|
|
configurations: null,
|
|
prefix: dependency.name,
|
|
combinators: combinators,
|
|
deferred: dependency.isDeferred,
|
|
charOffset: -1,
|
|
prefixCharOffset: -1,
|
|
uriOffset: -1,
|
|
importIndex: -1);
|
|
}
|
|
|
|
dartDocTestLibrary.compilationUnit.addImport(
|
|
metadata: null,
|
|
isAugmentationImport: false,
|
|
uri: libraryBuilder.importUri.toString(),
|
|
configurations: null,
|
|
prefix: null,
|
|
combinators: null,
|
|
deferred: false,
|
|
charOffset: -1,
|
|
prefixCharOffset: -1,
|
|
uriOffset: -1,
|
|
importIndex: -1);
|
|
|
|
dartDocTestLibrary.addImportsToScope();
|
|
} else {
|
|
throw "Got ${libraryBuilder.runtimeType}";
|
|
}
|
|
|
|
return dartDocTestLibrary;
|
|
}
|
|
}
|
|
|
|
class DocTestIncrementalKernelTarget extends IncrementalKernelTarget {
|
|
final DocTestIncrementalCompiler compiler;
|
|
DocTestIncrementalKernelTarget(this.compiler, FileSystem fileSystem,
|
|
bool includeComments, DillTarget dillTarget, UriTranslator uriTranslator)
|
|
: super(fileSystem, includeComments, dillTarget, uriTranslator);
|
|
|
|
@override
|
|
SourceLoader createLoader() {
|
|
return new DocTestSourceLoader(compiler, fileSystem, includeComments, this);
|
|
}
|
|
}
|
|
|
|
class DocTestSourceLoader extends SourceLoader {
|
|
final DocTestIncrementalCompiler compiler;
|
|
|
|
DocTestSourceLoader(this.compiler, FileSystem fileSystem,
|
|
bool includeComments, DocTestIncrementalKernelTarget target)
|
|
: super(fileSystem, includeComments, target);
|
|
|
|
@override
|
|
SourceLibraryBuilder createLibraryBuilder(
|
|
{required Uri importUri,
|
|
required Uri fileUri,
|
|
Uri? packageUri,
|
|
required LanguageVersion packageLanguageVersion,
|
|
SourceLibraryBuilder? origin,
|
|
IndexedLibrary? referencesFromIndex,
|
|
bool? referenceIsPartOwner,
|
|
bool isAugmentation = false,
|
|
bool isPatch = false}) {
|
|
if (importUri == DocTestIncrementalCompiler.dartDocTestUri) {
|
|
HybridFileSystem hfs = target.fileSystem as HybridFileSystem;
|
|
MemoryFileSystem fs = hfs.memory;
|
|
fs
|
|
.entityForUri(DocTestIncrementalCompiler.dartDocTestUri)
|
|
.writeAsStringSync(compiler._dartDocTestCode!);
|
|
return compiler.createDartDocTestLibrary(
|
|
this, compiler._dartDocTestLibraryBuilder!);
|
|
}
|
|
return super.createLibraryBuilder(
|
|
importUri: importUri,
|
|
fileUri: fileUri,
|
|
packageUri: packageUri,
|
|
packageLanguageVersion: packageLanguageVersion,
|
|
origin: origin,
|
|
referencesFromIndex: referencesFromIndex,
|
|
referenceIsPartOwner: referenceIsPartOwner,
|
|
isAugmentation: isAugmentation,
|
|
isPatch: isPatch);
|
|
}
|
|
}
|