// 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. // @dart = 2.9 library fasta.testing.kernel_chain; 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 templateUnspecified; import 'package:front_end/src/fasta/kernel/kernel_target.dart' show KernelTarget; import 'package:front_end/src/fasta/kernel/utils.dart' show ByteSink; import 'package:front_end/src/fasta/messages.dart' show DiagnosticMessageFromJson, LocatedMessage; import 'package:kernel/ast.dart' show Component, Library, Reference, Source; 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 RoundTripStatus, TextSerializationVerifier; import 'package:testing/testing.dart' show ChainContext, Expectation, ExpectationSet, Result, StdioProcess, Step, TestDescription; import '../fasta/testing/suite.dart' show CompilationSetup; final Uri platformBinariesLocation = computePlatformBinariesLocation(); abstract class MatchContext implements ChainContext { bool get updateExpectations; String get updateExpectationsOption; bool get canBeFixWithUpdateExpectations; ExpectationSet get expectationSet; Expectation get expectationFileMismatch => expectationSet["ExpectationFileMismatch"]; Expectation get expectationFileMismatchSerialized => expectationSet["ExpectationFileMismatchSerialized"]; Expectation get expectationFileMissing => expectationSet["ExpectationFileMissing"]; Future> match(String suffix, String actual, Uri uri, O output, {Expectation onMismatch, bool overwriteUpdateExpectationsWith}) async { bool updateExpectations = overwriteUpdateExpectationsWith ?? this.updateExpectations; 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(expectedFile.uri, actual, output); } String diff = await runDiff(expectedFile.uri, actual); onMismatch ??= expectationFileMismatch; return new Result( output, onMismatch, "$uri doesn't match ${expectedFile.uri}\n$diff", autoFixCommand: onMismatch == expectationFileMismatch ? updateExpectationsOption : null, canBeFixWithUpdateExpectations: onMismatch == expectationFileMismatch && canBeFixWithUpdateExpectations); } else { return new Result.pass(output); } } else { if (actual.isEmpty) return new Result.pass(output); if (updateExpectations) { return updateExpectationFile(expectedFile.uri, actual, output); } return new Result( output, expectationFileMissing, """ Please create file ${expectedFile.path} with this content: $actual""", autoFixCommand: updateExpectationsOption); } } Future> updateExpectationFile( 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.pass(output); } } class Print extends Step { const Print(); String get name => "print"; Future> 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 TypeCheck extends Step { const TypeCheck(); String get name => "typeCheck"; Future> 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( null, context.expectationSet["TypeCheckError"], '${errorFormatter.numberOfFailures} type errors'); } } } class MatchExpectation extends Step { final String suffix; final bool serializeFirst; final bool isLastMatchStep; /// 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, this.isLastMatchStep}) : assert(isLastMatchStep != null); String get name => "match expectations"; Future> run( ComponentResult result, MatchContext context) { Component component = result.component; Component componentToText = component; if (serializeFirst) { component.computeCanonicalNames(); List sdkLibraries = component.libraries.where((l) => !result.isUserLibrary(l)).toList(); ByteSink sink = new ByteSink(); Component writeMe = new Component( libraries: component.libraries.where(result.isUserLibrary).toList()) ..setMainMethodAndMode(null, false, component.mode); writeMe.uriToSource.addAll(component.uriToSource); if (component.problemsAsJson != null) { writeMe.problemsAsJson = new List.from(component.problemsAsJson); } BinaryPrinter binaryPrinter = new BinaryPrinter(sink); binaryPrinter.writeComponentFile(writeMe); List bytes = sink.builder.takeBytes(); BinaryBuilder binaryBuilder = new BinaryBuilder(bytes); componentToText = new Component(libraries: sdkLibraries); binaryBuilder.readComponent(componentToText); component.adoptChildren(); } Uri uri = result.description.uri; Iterable libraries = componentToText.libraries.where(result.isUserLibrary); Uri base = uri.resolve("."); Uri dartBase = Uri.base; StringBuffer buffer = new StringBuffer(); List> errors = result.compilationSetup.errors; Set reportedErrors = {}; for (Iterable message in errors) { reportedErrors.add(message.join('\n')); } Set problemsAsJson = {}; void addProblemsAsJson(List problems) { if (problems != null) { for (String jsonString in problems) { DiagnosticMessage message = new DiagnosticMessageFromJson.fromJson(jsonString); problemsAsJson.add(message.plainTextFormatted.join('\n')); } } } addProblemsAsJson(componentToText.problemsAsJson); libraries.forEach((Library library) { addProblemsAsJson(library.problemsAsJson); }); bool hasProblemsOutsideComponent = false; for (String reportedError in reportedErrors) { if (!problemsAsJson.contains(reportedError)) { if (!hasProblemsOutsideComponent) { buffer.writeln('//'); buffer.writeln('// Problems outside component:'); } buffer.writeln('//'); buffer.writeln('// ${reportedError.split('\n').join('\n// ')}'); hasProblemsOutsideComponent = true; } } if (hasProblemsOutsideComponent) { buffer.writeln('//'); } if (isLastMatchStep) { // Clear errors only in the last match step. This is needed to verify // problems reported outside the component in both the serialized and // non-serialized step. errors.clear(); } Printer printer = new Printer(buffer) ..writeProblemsAsJson( "Problems in component", componentToText.problemsAsJson); libraries.forEach((Library library) { printer.writeLibraryFile(library); printer.endLine(); }); printer.writeConstantTable(componentToText); if (result.extraConstantStrings.isNotEmpty) { buffer.writeln(""); buffer.writeln("Extra constant evaluation status:"); for (String extraConstantString in result.extraConstantStrings) { buffer.writeln(extraConstantString); } } bool printedConstantCoverageHeader = false; for (Source source in result.component.uriToSource.values) { if (!result.isUserLibraryImportUri(source.importUri)) continue; if (source.constantCoverageConstructors != null && source.constantCoverageConstructors.isNotEmpty) { if (!printedConstantCoverageHeader) { buffer.writeln(""); buffer.writeln(""); buffer.writeln("Constructor coverage from constants:"); printedConstantCoverageHeader = true; } buffer.writeln("${source.fileUri}:"); for (Reference reference in source.constantCoverageConstructors) { buffer .writeln("- ${reference.node} (from ${reference.node.location})"); } buffer.writeln(""); } } 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(suffix, actual, uri, result, onMismatch: serializeFirst ? context.expectationFileMismatchSerialized : context.expectationFileMismatch, overwriteUpdateExpectationsWith: serializeFirst ? false : null); } } class KernelTextSerialization extends Step { static const bool writeRoundTripStatus = bool.fromEnvironment( "text_serialization.writeRoundTripStatus", defaultValue: false); static const String suffix = ".roundtrip"; const KernelTextSerialization(); String get name => "kernel text serialization"; Future> 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 { component.computeCanonicalNames(); compilerContext.uriToSource.addAll(component.uriToSource); TextSerializationVerifier verifier = new TextSerializationVerifier(root: component.root); for (Library library in component.libraries) { if (library.importUri.scheme != "dart" && library.importUri.scheme != "package") { verifier.verify(library); } } List failures = verifier.failures; for (RoundTripStatus failure in failures) { LocatedMessage message = templateUnspecified .withArguments("\n${failure}") .withLocation(failure.uri, failure.offset, 1); options.report(message, message.code.severity); } if (writeRoundTripStatus) { Uri uri = component.uriToSource.keys .firstWhere((uri) => uri?.scheme == "file"); String filename = "${uri.toFilePath()}${suffix}"; uri = new File(filename).uri; StringBuffer buffer = new StringBuffer(); for (RoundTripStatus status in verifier.takeStatus()) { status.printOn(buffer); } await openWrite(uri, (IOSink sink) { sink.write(buffer.toString()); }); } if (failures.isNotEmpty) { return new Result(null, context.expectationSet["TextSerializationFailure"], "$messages"); } return pass(result); }); } } class WriteDill extends Step { const WriteDill(); String get name => "write .dill"; Future> 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, result.compilationSetup, result.sourceTarget, 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 { const ReadDill(); String get name => "read .dill"; Future> run(Uri uri, _) async { try { loadComponentFromBinary(uri.toFilePath()); } catch (e, s) { return fail(uri, e, s); } return pass(uri); } } class BytesCollector implements Sink> { final List> lists = >[]; int length = 0; void add(List data) { lists.add(data); length += data.length; } Uint8List collect() { Uint8List result = new Uint8List(length); int offset = 0; for (List list in lists) { result.setRange(offset, offset += list.length, list); } lists.clear(); length = 0; return result; } void close() {} } Future 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", [ "diff", "--no-index", "-u", expected.toFilePath(), uri.toFilePath() ], runInShell: true); file.deleteSync(); tempDirectory.deleteSync(); return process.output; } else { StdioProcess process = await StdioProcess.run( "git", ["diff", "--no-index", "-u", expected.toFilePath(), "-"], input: actual, runInShell: true); return process.output; } } Future 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 userLibraries; final Uri outputUri; final CompilationSetup compilationSetup; final KernelTarget sourceTarget; final List extraConstantStrings = []; ComponentResult(this.description, this.component, this.userLibraries, this.compilationSetup, this.sourceTarget, [this.outputUri]); bool isUserLibrary(Library library) { return isUserLibraryImportUri(library.importUri); } bool isUserLibraryImportUri(Uri importUri) { return userLibraries.contains(importUri); } ProcessedOptions get options => compilationSetup.options; }