56763db04b
Change-Id: Iff7fbc581eaf875cf9a09f4b994dff7bcf70cd3f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/137795 Reviewed-by: Jens Johansen <jensj@google.com> Commit-Queue: Johnni Winther <johnniwinther@google.com>
488 lines
16 KiB
Dart
488 lines
16 KiB
Dart
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE.md file.
|
|
|
|
library fasta.testing.kernel_chain;
|
|
|
|
import 'dart:async' show Future;
|
|
|
|
import 'dart:io' show Directory, File, IOSink, Platform;
|
|
|
|
import 'dart:typed_data' show Uint8List;
|
|
|
|
import 'package:_fe_analyzer_shared/src/util/relativize.dart'
|
|
show isWindows, relativizeUri;
|
|
|
|
import 'package:front_end/src/api_prototype/compiler_options.dart'
|
|
show CompilerOptions, DiagnosticMessage;
|
|
|
|
import 'package:front_end/src/base/processed_options.dart'
|
|
show ProcessedOptions;
|
|
|
|
import 'package:front_end/src/compute_platform_binaries_location.dart'
|
|
show computePlatformBinariesLocation;
|
|
|
|
import 'package:front_end/src/fasta/compiler_context.dart' show CompilerContext;
|
|
|
|
import 'package:front_end/src/fasta/fasta_codes.dart'
|
|
show templateInternalProblemUnhandled, templateUnspecified;
|
|
|
|
import 'package:front_end/src/fasta/kernel/utils.dart' show ByteSink;
|
|
|
|
import 'package:front_end/src/fasta/kernel/verifier.dart' show verifyComponent;
|
|
|
|
import 'package:front_end/src/fasta/messages.dart' show LocatedMessage;
|
|
|
|
import 'package:kernel/ast.dart' show Component, Library;
|
|
|
|
import 'package:kernel/binary/ast_from_binary.dart' show BinaryBuilder;
|
|
|
|
import 'package:kernel/binary/ast_to_binary.dart' show BinaryPrinter;
|
|
|
|
import 'package:kernel/error_formatter.dart' show ErrorFormatter;
|
|
|
|
import 'package:kernel/kernel.dart' show loadComponentFromBinary;
|
|
|
|
import 'package:kernel/naive_type_checker.dart' show NaiveTypeChecker;
|
|
|
|
import 'package:kernel/text/ast_to_text.dart' show Printer;
|
|
|
|
import 'package:kernel/text/text_serialization_verifier.dart'
|
|
show
|
|
TextDeserializationFailure,
|
|
TextRoundTripFailure,
|
|
TextSerializationFailure,
|
|
TextSerializationVerificationFailure,
|
|
TextSerializationVerifier;
|
|
|
|
import 'package:testing/testing.dart'
|
|
show
|
|
ChainContext,
|
|
Expectation,
|
|
ExpectationSet,
|
|
Result,
|
|
StdioProcess,
|
|
Step,
|
|
TestDescription;
|
|
|
|
final Uri platformBinariesLocation = computePlatformBinariesLocation();
|
|
|
|
abstract class MatchContext implements ChainContext {
|
|
bool get updateExpectations;
|
|
|
|
ExpectationSet get expectationSet;
|
|
|
|
Expectation get expectationFileMismatch =>
|
|
expectationSet["ExpectationFileMismatch"];
|
|
|
|
Expectation get expectationFileMismatchSerialized =>
|
|
expectationSet["ExpectationFileMismatchSerialized"];
|
|
|
|
Expectation get expectationFileMissing =>
|
|
expectationSet["ExpectationFileMissing"];
|
|
|
|
Future<Result<O>> match<O>(String suffix, String actual, Uri uri, O output,
|
|
{Expectation onMismatch}) async {
|
|
actual = actual.trim();
|
|
if (actual.isNotEmpty) {
|
|
actual += "\n";
|
|
}
|
|
File expectedFile = new File("${uri.toFilePath()}$suffix");
|
|
if (await expectedFile.exists()) {
|
|
String expected = await expectedFile.readAsString();
|
|
if (expected != actual) {
|
|
if (updateExpectations) {
|
|
return updateExpectationFile<O>(expectedFile.uri, actual, output);
|
|
}
|
|
String diff = await runDiff(expectedFile.uri, actual);
|
|
onMismatch ??= expectationFileMismatch;
|
|
return new Result<O>(output, onMismatch,
|
|
"$uri doesn't match ${expectedFile.uri}\n$diff", null);
|
|
} else {
|
|
return new Result<O>.pass(output);
|
|
}
|
|
} else {
|
|
if (actual.isEmpty) return new Result<O>.pass(output);
|
|
if (updateExpectations) {
|
|
return updateExpectationFile(expectedFile.uri, actual, output);
|
|
}
|
|
return new Result<O>(
|
|
output,
|
|
expectationFileMissing,
|
|
"""
|
|
Please create file ${expectedFile.path} with this content:
|
|
$actual""",
|
|
null);
|
|
}
|
|
}
|
|
|
|
Future<Result<O>> updateExpectationFile<O>(
|
|
Uri uri,
|
|
String actual,
|
|
O output,
|
|
) async {
|
|
if (actual.isEmpty) {
|
|
await new File.fromUri(uri).delete();
|
|
} else {
|
|
await openWrite(uri, (IOSink sink) {
|
|
sink.write(actual);
|
|
});
|
|
}
|
|
return new Result<O>.pass(output);
|
|
}
|
|
}
|
|
|
|
class Print extends Step<ComponentResult, ComponentResult, ChainContext> {
|
|
const Print();
|
|
|
|
String get name => "print";
|
|
|
|
Future<Result<ComponentResult>> run(ComponentResult result, _) async {
|
|
Component component = result.component;
|
|
|
|
StringBuffer sb = new StringBuffer();
|
|
await CompilerContext.runWithDefaultOptions((compilerContext) async {
|
|
compilerContext.uriToSource.addAll(component.uriToSource);
|
|
|
|
Printer printer = new Printer(sb);
|
|
for (Library library in component.libraries) {
|
|
if (result.userLibraries.contains(library.importUri)) {
|
|
printer.writeLibraryFile(library);
|
|
}
|
|
}
|
|
printer.writeConstantTable(component);
|
|
});
|
|
print("$sb");
|
|
return pass(result);
|
|
}
|
|
}
|
|
|
|
class Verify extends Step<ComponentResult, ComponentResult, ChainContext> {
|
|
final bool fullCompile;
|
|
|
|
const Verify(this.fullCompile);
|
|
|
|
String get name => "verify";
|
|
|
|
Future<Result<ComponentResult>> run(
|
|
ComponentResult result, ChainContext context) async {
|
|
Component component = result.component;
|
|
StringBuffer messages = new StringBuffer();
|
|
ProcessedOptions options = new ProcessedOptions(
|
|
options: new CompilerOptions()
|
|
..onDiagnostic = (DiagnosticMessage message) {
|
|
if (messages.isNotEmpty) {
|
|
messages.write("\n");
|
|
}
|
|
messages.writeAll(message.plainTextFormatted, "\n");
|
|
});
|
|
return await CompilerContext.runWithOptions(options,
|
|
(compilerContext) async {
|
|
compilerContext.uriToSource.addAll(component.uriToSource);
|
|
List<LocatedMessage> verificationErrors = verifyComponent(component,
|
|
isOutline: !fullCompile, skipPlatform: true);
|
|
assert(verificationErrors.isEmpty || messages.isNotEmpty);
|
|
if (messages.isEmpty) {
|
|
return pass(result);
|
|
} else {
|
|
return new Result<ComponentResult>(null,
|
|
context.expectationSet["VerificationError"], "$messages", null);
|
|
}
|
|
}, errorOnMissingInput: false);
|
|
}
|
|
}
|
|
|
|
class TypeCheck extends Step<ComponentResult, ComponentResult, ChainContext> {
|
|
const TypeCheck();
|
|
|
|
String get name => "typeCheck";
|
|
|
|
Future<Result<ComponentResult>> run(
|
|
ComponentResult result, ChainContext context) async {
|
|
Component component = result.component;
|
|
ErrorFormatter errorFormatter = new ErrorFormatter();
|
|
NaiveTypeChecker checker =
|
|
new NaiveTypeChecker(errorFormatter, component, ignoreSdk: true);
|
|
checker.checkComponent(component);
|
|
if (errorFormatter.numberOfFailures == 0) {
|
|
return pass(result);
|
|
} else {
|
|
errorFormatter.failures.forEach(print);
|
|
print('------- Found ${errorFormatter.numberOfFailures} errors -------');
|
|
return new Result<ComponentResult>(
|
|
null,
|
|
context.expectationSet["TypeCheckError"],
|
|
'${errorFormatter.numberOfFailures} type errors',
|
|
null);
|
|
}
|
|
}
|
|
}
|
|
|
|
class MatchExpectation
|
|
extends Step<ComponentResult, ComponentResult, MatchContext> {
|
|
final String suffix;
|
|
final bool serializeFirst;
|
|
|
|
/// Check if a textual representation of the component matches the expectation
|
|
/// located at [suffix]. If [serializeFirst] is true, the input component will
|
|
/// be serialized, deserialized, and the textual representation of that is
|
|
/// compared. It is still the original component that is returned though.
|
|
const MatchExpectation(this.suffix, {this.serializeFirst: false});
|
|
|
|
String get name => "match expectations";
|
|
|
|
Future<Result<ComponentResult>> run(
|
|
ComponentResult result, MatchContext context) {
|
|
Component component = result.component;
|
|
|
|
Component componentToText = component;
|
|
if (serializeFirst) {
|
|
component.computeCanonicalNames();
|
|
List<Library> sdkLibraries =
|
|
component.libraries.where((l) => !result.isUserLibrary(l)).toList();
|
|
|
|
ByteSink sink = new ByteSink();
|
|
Component writeMe = new Component(
|
|
libraries: component.libraries.where(result.isUserLibrary).toList());
|
|
writeMe.uriToSource.addAll(component.uriToSource);
|
|
if (component.problemsAsJson != null) {
|
|
writeMe.problemsAsJson =
|
|
new List<String>.from(component.problemsAsJson);
|
|
}
|
|
BinaryPrinter binaryPrinter = new BinaryPrinter(sink);
|
|
binaryPrinter.writeComponentFile(writeMe);
|
|
List<int> bytes = sink.builder.takeBytes();
|
|
|
|
BinaryBuilder binaryBuilder = new BinaryBuilder(bytes);
|
|
componentToText = new Component(libraries: sdkLibraries);
|
|
binaryBuilder.readComponent(componentToText);
|
|
component.adoptChildren();
|
|
}
|
|
|
|
StringBuffer messages =
|
|
(context as dynamic).componentToDiagnostics[component];
|
|
Uri uri =
|
|
component.uriToSource.keys.firstWhere((uri) => uri?.scheme == "file");
|
|
Iterable<Library> libraries =
|
|
componentToText.libraries.where(result.isUserLibrary);
|
|
Uri base = uri.resolve(".");
|
|
Uri dartBase = Uri.base;
|
|
StringBuffer buffer = new StringBuffer();
|
|
messages.clear();
|
|
Printer printer = new Printer(buffer)
|
|
..writeProblemsAsJson(
|
|
"Problems in component", componentToText.problemsAsJson);
|
|
libraries.forEach((Library library) {
|
|
printer.writeLibraryFile(library);
|
|
printer.endLine();
|
|
});
|
|
printer.writeConstantTable(componentToText);
|
|
String actual = "$buffer";
|
|
String binariesPath =
|
|
relativizeUri(Uri.base, platformBinariesLocation, isWindows);
|
|
if (binariesPath.endsWith("/dart-sdk/lib/_internal/")) {
|
|
// We are running from the built SDK.
|
|
actual = actual.replaceAll(
|
|
binariesPath.substring(
|
|
0, binariesPath.length - "lib/_internal/".length),
|
|
"sdk/");
|
|
}
|
|
actual = actual.replaceAll("$base", "org-dartlang-testcase:///");
|
|
actual = actual.replaceAll("$dartBase", "org-dartlang-testcase-sdk:///");
|
|
actual = actual.replaceAll("\\n", "\n");
|
|
return context.match<ComponentResult>(suffix, actual, uri, result,
|
|
onMismatch: serializeFirst
|
|
? context.expectationFileMismatchSerialized
|
|
: context.expectationFileMismatch);
|
|
}
|
|
}
|
|
|
|
class KernelTextSerialization
|
|
extends Step<ComponentResult, ComponentResult, ChainContext> {
|
|
const KernelTextSerialization();
|
|
|
|
String get name => "kernel text serialization";
|
|
|
|
Future<Result<ComponentResult>> run(
|
|
ComponentResult result, ChainContext context) async {
|
|
Component component = result.component;
|
|
StringBuffer messages = new StringBuffer();
|
|
ProcessedOptions options = new ProcessedOptions(
|
|
options: new CompilerOptions()
|
|
..onDiagnostic = (DiagnosticMessage message) {
|
|
if (messages.isNotEmpty) {
|
|
messages.write("\n");
|
|
}
|
|
messages.writeAll(message.plainTextFormatted, "\n");
|
|
});
|
|
return await CompilerContext.runWithOptions(options,
|
|
(compilerContext) async {
|
|
compilerContext.uriToSource.addAll(component.uriToSource);
|
|
TextSerializationVerifier verifier = new TextSerializationVerifier();
|
|
for (Library library in component.libraries) {
|
|
if (library.importUri.scheme != "dart" &&
|
|
library.importUri.scheme != "package") {
|
|
library.accept(verifier);
|
|
}
|
|
}
|
|
for (TextSerializationVerificationFailure failure in verifier.failures) {
|
|
LocatedMessage message;
|
|
if (failure is TextSerializationFailure) {
|
|
message = templateUnspecified
|
|
.withArguments(
|
|
"Failed to serialize a node: ${failure.message.isNotEmpty}")
|
|
.withLocation(failure.uri, failure.offset, 1);
|
|
} else if (failure is TextDeserializationFailure) {
|
|
message = templateUnspecified
|
|
.withArguments(
|
|
"Failed to deserialize a node: ${failure.message.isNotEmpty}")
|
|
.withLocation(failure.uri, failure.offset, 1);
|
|
} else if (failure is TextRoundTripFailure) {
|
|
String formattedInitial =
|
|
failure.initial.isNotEmpty ? failure.initial : "<empty>";
|
|
String formattedSerialized =
|
|
failure.serialized.isNotEmpty ? failure.serialized : "<empty>";
|
|
message = templateUnspecified
|
|
.withArguments(
|
|
"Round trip failure: initial doesn't match serialized.\n"
|
|
" Initial : $formattedInitial\n"
|
|
" Serialized : $formattedSerialized")
|
|
.withLocation(failure.uri, failure.offset, 1);
|
|
} else {
|
|
message = templateInternalProblemUnhandled
|
|
.withArguments(
|
|
"${failure.runtimeType}", "KernelTextSerialization.run")
|
|
.withLocation(failure.uri, failure.offset, 1);
|
|
}
|
|
options.report(message, message.code.severity);
|
|
}
|
|
|
|
if (verifier.failures.isNotEmpty) {
|
|
return new Result<ComponentResult>(
|
|
null,
|
|
context.expectationSet["TextSerializationFailure"],
|
|
"$messages",
|
|
null);
|
|
}
|
|
return pass(result);
|
|
});
|
|
}
|
|
}
|
|
|
|
class WriteDill extends Step<ComponentResult, ComponentResult, ChainContext> {
|
|
const WriteDill();
|
|
|
|
String get name => "write .dill";
|
|
|
|
Future<Result<ComponentResult>> run(ComponentResult result, _) async {
|
|
Component component = result.component;
|
|
Directory tmp = await Directory.systemTemp.createTemp();
|
|
Uri uri = tmp.uri.resolve("generated.dill");
|
|
File generated = new File.fromUri(uri);
|
|
IOSink sink = generated.openWrite();
|
|
result = new ComponentResult(
|
|
result.description, result.component, result.userLibraries, uri);
|
|
try {
|
|
new BinaryPrinter(sink).writeComponentFile(component);
|
|
} catch (e, s) {
|
|
return fail(result, e, s);
|
|
} finally {
|
|
print("Wrote `${generated.path}`");
|
|
await sink.close();
|
|
}
|
|
return pass(result);
|
|
}
|
|
}
|
|
|
|
class ReadDill extends Step<Uri, Uri, ChainContext> {
|
|
const ReadDill();
|
|
|
|
String get name => "read .dill";
|
|
|
|
Future<Result<Uri>> run(Uri uri, _) async {
|
|
try {
|
|
loadComponentFromBinary(uri.toFilePath());
|
|
} catch (e, s) {
|
|
return fail(uri, e, s);
|
|
}
|
|
return pass(uri);
|
|
}
|
|
}
|
|
|
|
class BytesCollector implements Sink<List<int>> {
|
|
final List<List<int>> lists = <List<int>>[];
|
|
|
|
int length = 0;
|
|
|
|
void add(List<int> data) {
|
|
lists.add(data);
|
|
length += data.length;
|
|
}
|
|
|
|
Uint8List collect() {
|
|
Uint8List result = new Uint8List(length);
|
|
int offset = 0;
|
|
for (List<int> list in lists) {
|
|
result.setRange(offset, offset += list.length, list);
|
|
}
|
|
lists.clear();
|
|
length = 0;
|
|
return result;
|
|
}
|
|
|
|
void close() {}
|
|
}
|
|
|
|
Future<String> runDiff(Uri expected, String actual) async {
|
|
if (Platform.isWindows) {
|
|
// TODO(johnniwinther): Work-around for Windows. For some reason piping
|
|
// the actual result through stdin doesn't work; it shows a diff as if the
|
|
// actual result is the empty string.
|
|
Directory tempDirectory = Directory.systemTemp.createTempSync();
|
|
Uri uri = tempDirectory.uri.resolve('actual');
|
|
File file = new File.fromUri(uri)..writeAsStringSync(actual);
|
|
StdioProcess process = await StdioProcess.run(
|
|
"git",
|
|
<String>[
|
|
"diff",
|
|
"--no-index",
|
|
"-u",
|
|
expected.toFilePath(),
|
|
uri.toFilePath()
|
|
],
|
|
runInShell: true);
|
|
file.deleteSync();
|
|
tempDirectory.deleteSync();
|
|
return process.output;
|
|
} else {
|
|
StdioProcess process = await StdioProcess.run(
|
|
"git", <String>["diff", "--no-index", "-u", expected.toFilePath(), "-"],
|
|
input: actual, runInShell: true);
|
|
return process.output;
|
|
}
|
|
}
|
|
|
|
Future<void> openWrite(Uri uri, f(IOSink sink)) async {
|
|
IOSink sink = new File.fromUri(uri).openWrite();
|
|
try {
|
|
await f(sink);
|
|
} finally {
|
|
await sink.close();
|
|
}
|
|
print("Wrote $uri");
|
|
}
|
|
|
|
class ComponentResult {
|
|
final TestDescription description;
|
|
final Component component;
|
|
final Set<Uri> userLibraries;
|
|
final Uri outputUri;
|
|
|
|
ComponentResult(this.description, this.component, this.userLibraries,
|
|
[this.outputUri]);
|
|
|
|
bool isUserLibrary(Library library) {
|
|
return userLibraries.contains(library.importUri);
|
|
}
|
|
}
|