| // 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 file. | |
| import 'dart:async'; | |
| import 'dart:io' show Directory, File, IOSink, Platform; | |
| import 'dart:typed_data'; | |
| 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/util/relativize.dart' | |
| show isWindows, relativizeUri; | |
| import 'package:front_end/src/api_prototype/compiler_options.dart' | |
| show CfeDiagnosticMessage; | |
| import 'package:front_end/src/base/compiler_context.dart' show CompilerContext; | |
| import 'package:front_end/src/base/messages.dart' | |
| show DiagnosticMessageFromJson; | |
| 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/kernel/kernel_target.dart' show KernelTarget; | |
| import 'package:front_end/src/kernel/utils.dart' show ByteSink; | |
| import 'package:kernel/ast.dart' | |
| show | |
| Block, | |
| Component, | |
| Library, | |
| LibraryPart, | |
| Procedure, | |
| ReturnStatement, | |
| Source, | |
| Statement; | |
| 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:testing/testing.dart' | |
| show | |
| ChainContext, | |
| Expectation, | |
| ExpectationSet, | |
| Result, | |
| StdioProcess, | |
| Step, | |
| TestDescription; | |
| import '../test_utils.dart'; | |
| import '../testing/suite.dart' show CompilationSetup, CompileMode; | |
| final Uri platformBinariesLocation = computePlatformBinariesLocation(); | |
| mixin MatchContext implements ChainContext { | |
| bool get updateExpectations; | |
| String get updateExpectationsOption; | |
| bool get canBeFixWithUpdateExpectations; | |
| @override | |
| 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, | |
| 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.replaceAll("\r\n", "\n") != 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", | |
| autoFixCommand: onMismatch == expectationFileMismatch | |
| ? updateExpectationsOption | |
| : null, | |
| canBeFixWithUpdateExpectations: | |
| onMismatch == expectationFileMismatch && | |
| canBeFixWithUpdateExpectations, | |
| ); | |
| } 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""", autoFixCommand: updateExpectationsOption); | |
| } | |
| } | |
| 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 ErrorCommentChecker | |
| extends Step<ComponentResult, ComponentResult, ChainContext> { | |
| final CompileMode compileMode; | |
| const new(this.compileMode); | |
| static const bool throwOnNoMatch = false; | |
| @override | |
| String get name => "ErrorCommentChecker"; | |
| static const Set<String> ignoreBecauseOfFailures = { | |
| "extension_types/conflicting_static_and_instance", | |
| "extension_types/implements_conflicts", | |
| "general/bounds_enums", | |
| "general/bounds_type_parameters", | |
| "general/covariant_equals", | |
| "general/getter_vs_setter_type", | |
| "general/nested_variance", | |
| "general/new_as_selector", | |
| "general/top_level_variance", | |
| "general/type_variable_uses", | |
| "generic_metadata/alias_from_opt_in", | |
| "inference/block_bodied_lambdas_infer_bottom_sync", | |
| "inference/future_then_conditional", | |
| "inference/future_then_ifNull", | |
| "inference/generic_methods_infer_js_builtin", | |
| "inference/instantiate_to_bounds_generic2_has_bound_defined_after", | |
| "inference/instantiate_to_bounds_generic2_has_bound_defined_before", | |
| "inference/void_return_type_subtypes_dynamic", | |
| "nnbd_mixed/hierarchy/in_dill_out_in/in_out_in", | |
| "nnbd_mixed/hierarchy/in_out_dill_in/in_out_in", | |
| "nnbd/getter_vs_setter_type_nnbd", | |
| "nnbd/getter_vs_setter_type", | |
| "nnbd/null_access", | |
| "patterns/exhaustiveness/bool_switch", | |
| "patterns/exhaustiveness/enum_switch", | |
| "patterns/for_final_variable", | |
| "patterns/pattern_types", | |
| "records/type_record_as_supertype", | |
| }; | |
| @override | |
| Future<Result<ComponentResult>> run( | |
| ComponentResult result, | |
| ChainContext context, | |
| ) { | |
| if (compileMode != CompileMode.full) return Future.value(pass(result)); | |
| // TODO(jensj): Delete this once failures are fixed. | |
| if (ignoreBecauseOfFailures.contains(result.description.shortName)) { | |
| return Future.value(pass(result)); | |
| } | |
| Component component = result.component; | |
| for (Library lib in component.libraries) { | |
| if (!result.userLibraries.contains(lib.importUri)) continue; | |
| if (!lib.fileUri.isScheme("file")) continue; | |
| List<Uri> uris = []; | |
| List<String> filenames = []; | |
| for (LibraryPart part in lib.parts) { | |
| // This is a bit simplistic but will probably work for our use here. | |
| uris.add(lib.fileUri.resolve(part.partUri)); | |
| filenames.add(uris.last.pathSegments.last); | |
| } | |
| uris.add(lib.fileUri); | |
| filenames.add(uris.last.pathSegments.last); | |
| Set<String> expectProblemOn = {}; | |
| Set<String> expectNoProblemOn = {}; | |
| for (Uri uri in uris) { | |
| Set<int> expectErrorOnLines = {}; | |
| Set<int> expectNoErrorOnLines = {}; | |
| Map<int, List<CommentToken>> linesToComments = extractCommentsFromLines( | |
| uri, | |
| ); | |
| categorizeCommentLines( | |
| linesToComments, | |
| expectErrorOnLines, | |
| expectNoErrorOnLines, | |
| ); | |
| for (int line in expectErrorOnLines) { | |
| expectProblemOn.add("${uri.pathSegments.last}:$line"); | |
| } | |
| for (int line in expectNoErrorOnLines) { | |
| expectNoProblemOn.add("${uri.pathSegments.last}:$line"); | |
| } | |
| } | |
| // Now check. | |
| if (expectProblemOn.isNotEmpty || expectNoProblemOn.isNotEmpty) { | |
| List<String> failures = []; | |
| Set<String> notYetSeen = new Set<String>.of(expectProblemOn); | |
| // Sanity check: No overlap between error and no-error expectations. | |
| Set<String> overlap = expectProblemOn.intersection(expectNoProblemOn); | |
| if (overlap.isNotEmpty) { | |
| for (String fileAndLine in overlap) { | |
| failures.add( | |
| "Test error: " | |
| "$fileAndLine is marked as both error and no error.", | |
| ); | |
| } | |
| } | |
| List<String>? libraryProblems = lib.problemsAsJson; | |
| RegExp extractLineRegExp = RegExp( | |
| "(${filenames.map((e) => RegExp.escape(e)).join("|")}):(\\d+)", | |
| ); | |
| if (libraryProblems != null) { | |
| for (String jsonString in libraryProblems) { | |
| DiagnosticMessageFromJson message = | |
| new DiagnosticMessageFromJson.fromJson(jsonString); | |
| // By taking all of these we accept it if it's just mentioned in a | |
| // context message too. Is that the precision we want? | |
| for (String plainTextProblem in message.plainTextFormatted) { | |
| List<RegExpMatch> matches = extractLineRegExp | |
| .allMatches(plainTextProblem) | |
| .toList(); | |
| if (matches.isEmpty && throwOnNoMatch) { | |
| throw "Couldn't extract any offsets from " | |
| "'$plainTextProblem' with '$extractLineRegExp'"; | |
| } | |
| for (RegExpMatch match in matches) { | |
| String lineString = match[0]!; | |
| notYetSeen.remove(lineString); | |
| if (expectNoProblemOn.contains(lineString)) { | |
| failures.add( | |
| "Found error at $lineString " | |
| "but didn't expect any:\n" | |
| "$plainTextProblem", | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| for (String line in notYetSeen) { | |
| failures.add("Expected error on $line but didn't find any."); | |
| } | |
| if (failures.isNotEmpty) { | |
| return new Future.value( | |
| new Result<ComponentResult>( | |
| result, | |
| context.expectationSet["ErrorCommentCheckFailure"], | |
| "Found ${failures.length} failures:\n\n * " | |
| "${failures.join("\n\n * ")}\n", | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| return Future.value(pass(result)); | |
| } | |
| void categorizeCommentLines( | |
| Map<int, List<CommentToken>> linesToComments, | |
| Set<int> expectErrorOnLines, | |
| Set<int> expectNoErrorOnLines, | |
| ) { | |
| for (MapEntry<int, List<CommentToken>> entry in linesToComments.entries) { | |
| for (CommentToken comment in entry.value) { | |
| String message = comment.lexeme.trim().toLowerCase(); | |
| while (message.startsWith("//") || message.startsWith("/*")) { | |
| message = message.substring(2).trim(); | |
| } | |
| // TODO(jensj): Possibly reduce these cases by updating tests. | |
| // See discussion in | |
| // https://dart-review.googlesource.com/c/sdk/+/346301. | |
| if (message == "error" || | |
| message == "error." || | |
| message == "error */" || | |
| message == "error*/" || | |
| message.startsWith("error in strong mode") || | |
| message.startsWith("error:") || | |
| message.startsWith("error,") || | |
| message.startsWith("error.") || | |
| message.startsWith("error - ") || | |
| message.startsWith("error (") || | |
| message.startsWith("error on ") || | |
| message.startsWith("error in ") || | |
| message.startsWith("error since ") || | |
| message.startsWith("error because ") || | |
| message.startsWith("not ok.") || | |
| message.startsWith("note: illegal ") || | |
| message.startsWith("parse error:") || | |
| message.startsWith("parse error,") || | |
| message.startsWith("compile-time error") || | |
| message.endsWith(" compile-time error")) { | |
| expectErrorOnLines.add(entry.key); | |
| } else if (message == "ok" || | |
| message == "ok." || | |
| message == "ok," || | |
| message == "ok */" || | |
| message.startsWith("ok: ") || | |
| message.startsWith("ok, ") || | |
| message.startsWith("ok (") || | |
| message.startsWith("ok because ") || | |
| message.startsWith("ok to ") || | |
| message.startsWith("now ok") || | |
| message.startsWith("no error.") || | |
| message.startsWith("no error:") || | |
| message.startsWith("no error ") || | |
| message.startsWith("not a compile time error") || | |
| message.startsWith("not an error") || | |
| message == "shouldn't result in a compile-time error.") { | |
| expectNoErrorOnLines.add(entry.key); | |
| } | |
| } | |
| } | |
| } | |
| Map<int, List<CommentToken>> extractCommentsFromLines(Uri uri) { | |
| if (!uri.isScheme("file")) return const {}; | |
| File f = new File.fromUri(uri); | |
| if (!f.existsSync()) return const {}; | |
| Uint8List rawBytes = f.readAsBytesSync(); | |
| Utf8BytesScanner scanner = new Utf8BytesScanner( | |
| rawBytes, | |
| includeComments: true, | |
| languageVersionChanged: (scanner, languageVersion) { | |
| // Nothing - but don't overwrite the previous settings. | |
| }, | |
| ); | |
| Token firstToken = scanner.tokenize(); | |
| List<int> lineStarts = scanner.lineStarts; | |
| Token? token = firstToken; | |
| Token? previousToken; | |
| Source lineStartsHelper = new Source.emptySource(lineStarts, null, null); | |
| Map<int, List<CommentToken>> linesToComments = {}; | |
| while (token != null && !token.isEof) { | |
| CommentToken? precedingComments = token.precedingComments; | |
| while (precedingComments != null) { | |
| int commentLine = lineStartsHelper | |
| .getLocation(Uri.base /* dummy */, precedingComments.offset) | |
| .line; | |
| int tokenLine = lineStartsHelper | |
| .getLocation(Uri.base /* dummy */, token.offset) | |
| .line; | |
| int likelyAboutLine = tokenLine; | |
| if (previousToken != null) { | |
| int previousTokenLine = lineStartsHelper | |
| .getLocation(Uri.base /* dummy */, previousToken.offset) | |
| .line; | |
| if (previousTokenLine == commentLine) likelyAboutLine = commentLine; | |
| } | |
| if (!precedingComments.lexeme.startsWith("// Copyright (c)") && | |
| !precedingComments.lexeme.startsWith( | |
| "// for details. All rights reserved.", | |
| ) && | |
| !precedingComments.lexeme.startsWith("// BSD-style license")) { | |
| (linesToComments[likelyAboutLine] ??= []).add(precedingComments); | |
| } | |
| Token? next = precedingComments.next; | |
| if (next is CommentToken) { | |
| precedingComments = next; | |
| } else { | |
| precedingComments = null; | |
| } | |
| } | |
| previousToken = token; | |
| token = token.next; | |
| } | |
| return linesToComments; | |
| } | |
| } | |
| class Print extends Step<ComponentResult, ComponentResult, ChainContext> { | |
| const new(); | |
| @override | |
| String get name => "print"; | |
| @override | |
| 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, | |
| showOffsets: result.compilationSetup.folderOptions.showOffsets, | |
| ); | |
| 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<ComponentResult, ComponentResult, ChainContext> { | |
| const new(); | |
| @override | |
| String get name => "typeCheck"; | |
| @override | |
| Future<Result<ComponentResult>> run( | |
| ComponentResult result, | |
| ChainContext context, | |
| ) { | |
| Component component = result.component; | |
| ErrorFormatter errorFormatter = new ErrorFormatter(); | |
| NaiveTypeChecker checker = new NaiveTypeChecker( | |
| errorFormatter, | |
| component, | |
| ignoreSdk: true, | |
| ); | |
| checker.checkComponent(component); | |
| if (errorFormatter.numberOfFailures == 0) { | |
| return new Future.value(pass(result)); | |
| } else { | |
| errorFormatter.failures.forEach(print); | |
| print('------- Found ${errorFormatter.numberOfFailures} errors -------'); | |
| return new Future.value( | |
| new Result<ComponentResult>( | |
| null, | |
| context.expectationSet["TypeCheckError"], | |
| '${errorFormatter.numberOfFailures} type errors', | |
| ), | |
| ); | |
| } | |
| } | |
| } | |
| class MatchExpectation | |
| extends Step<ComponentResult, ComponentResult, MatchContext> { | |
| 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 new( | |
| this.suffix, { | |
| this.serializeFirst = false, | |
| required this.isLastMatchStep, | |
| }); | |
| @override | |
| String get name => "match expectations"; | |
| @override | |
| Future<Result<ComponentResult>> run( | |
| ComponentResult result, | |
| MatchContext context, | |
| ) { | |
| Component component = result.component; | |
| Component componentToText = component; | |
| if (serializeFirst) { | |
| if (result.compilationSetup.folderOptions.showOffsets) { | |
| // Not all offsets are serialized so the output won't match and there is | |
| // currently no reason to check this. | |
| // TODO(johnniwinther): Find a way to avoid or verify the discrepancies. | |
| return new Future.value(new Result<ComponentResult>.pass(result)); | |
| } | |
| // TODO(johnniwinther): Use library filter instead. | |
| 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(), | |
| )..setMainMethodAndMode(null, false); | |
| 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); | |
| Uint8List 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<Library> libraries = componentToText.libraries.where( | |
| result.isUserLibrary, | |
| ); | |
| StringBuffer buffer = new StringBuffer(); | |
| List<Iterable<String>> errors = result.compilationSetup.errors; | |
| Set<String> reportedErrors = <String>{}; | |
| for (Iterable<String> message in errors) { | |
| reportedErrors.add(message.join('\n')); | |
| } | |
| Set<String> problemsAsJson = <String>{}; | |
| void addProblemsAsJson(List<String>? problems) { | |
| if (problems != null) { | |
| for (String jsonString in problems) { | |
| CfeDiagnosticMessage 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, | |
| showOffsets: result.compilationSetup.folderOptions.showOffsets, | |
| )..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); | |
| } | |
| } | |
| addConstantCoverageToExpectation( | |
| result.component, | |
| buffer, | |
| skipImportUri: (Uri? importUri) => | |
| !result.isUserLibraryImportUri(importUri), | |
| ); | |
| String actual = replacePathsInExpectText( | |
| buffer.toString(), | |
| uri.resolve("."), | |
| ); | |
| return context.match<ComponentResult>( | |
| suffix, | |
| actual, | |
| uri, | |
| result, | |
| onMismatch: serializeFirst | |
| ? context.expectationFileMismatchSerialized | |
| : context.expectationFileMismatch, | |
| overwriteUpdateExpectationsWith: serializeFirst ? false : null, | |
| ); | |
| } | |
| } | |
| class WriteDill extends Step<ComponentResult, ComponentResult, ChainContext> { | |
| final bool skipVm; | |
| const new({required this.skipVm}); | |
| @override | |
| String get name => "write .dill"; | |
| @override | |
| Future<Result<ComponentResult>> run(ComponentResult result, _) async { | |
| Component component = result.component; | |
| Procedure? mainMethod = component.mainMethod; | |
| bool writeToFile = !skipVm; | |
| if (mainMethod == null) { | |
| writeToFile = false; | |
| } else { | |
| Statement? mainBody = mainMethod.function.body; | |
| if (mainBody is Block && mainBody.statements.isEmpty || | |
| mainBody is ReturnStatement && mainBody.expression == null) { | |
| writeToFile = false; | |
| } | |
| } | |
| ByteSink sink = new ByteSink(); | |
| bool good = false; | |
| try { | |
| // TODO(johnniwinther): Use library filter instead. | |
| // Avoid serializing the sdk. | |
| Component userCode = new Component( | |
| nameRoot: component.root, | |
| uriToSource: new Map<Uri, Source>.from(component.uriToSource), | |
| ); | |
| userCode.setMainMethodAndMode(component.mainMethodName, true); | |
| List<Library> auxiliaryLibraries = []; | |
| for (Library library in component.libraries) { | |
| bool includeLibrary; | |
| if (library.importUri.isScheme("dart")) { | |
| if (result.isUserLibrary(library)) { | |
| // dart:test, test:extra etc as used will say yes to being a user | |
| // library. | |
| includeLibrary = true; | |
| } else if (library.isSynthetic) { | |
| // OK --- serialize that. | |
| includeLibrary = true; | |
| } else { | |
| // Skip serialization of "real" platform libraries. | |
| includeLibrary = false; | |
| } | |
| } else if (result.isUserLibrary(library)) { | |
| includeLibrary = true; | |
| } else { | |
| // This library is neither part of the user libraries nor part of the | |
| // platform libraries. To run this, we need to include it in the | |
| // dill. | |
| auxiliaryLibraries.add(library); | |
| includeLibrary = false; | |
| } | |
| if (includeLibrary) { | |
| userCode.libraries.add(library); | |
| } | |
| } | |
| // We first ensure that we can serialize with possible references to | |
| // libraries that aren't included in the serialization. | |
| new BinaryPrinter(sink).writeComponentFile(userCode); | |
| // We then serialize with any such libraries to | |
| // a) ensure that we can do that too, and that | |
| // b) the output is complete (modulo the platform) so that the VM can | |
| // actually run it. | |
| if (auxiliaryLibraries.isNotEmpty) { | |
| userCode.libraries.addAll(auxiliaryLibraries); | |
| sink = new ByteSink(); | |
| new BinaryPrinter(sink).writeComponentFile(userCode); | |
| } | |
| good = true; | |
| } catch (e, s) { | |
| return fail(result, e, s); | |
| } finally { | |
| if (good && writeToFile) { | |
| Directory tmp = await Directory.systemTemp.createTemp(); | |
| Uri uri = tmp.uri.resolve("generated.dill"); | |
| File generated = new File.fromUri(uri); | |
| IOSink ioSink = generated.openWrite(); | |
| ioSink.add(sink.builder.takeBytes()); | |
| await ioSink.close(); | |
| result = new ComponentResult( | |
| result.description, | |
| result.component, | |
| result.userLibraries, | |
| result.compilationSetup, | |
| result.sourceTarget, | |
| uri, | |
| ); | |
| print("Wrote component to `${generated.path}`."); | |
| } else { | |
| print("Wrote component to memory."); | |
| } | |
| } | |
| return pass(result); | |
| } | |
| } | |
| class ReadDill extends Step<Uri, Uri, ChainContext> { | |
| const new(); | |
| @override | |
| String get name => "read .dill"; | |
| @override | |
| Future<Result<Uri>> run(Uri uri, _) { | |
| try { | |
| loadComponentFromBinary(uri.toFilePath()); | |
| } catch (e, s) { | |
| return new Future.value(fail(uri, e, s)); | |
| } | |
| return new Future.value(pass(uri)); | |
| } | |
| } | |
| 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; | |
| final CompilationSetup compilationSetup; | |
| final KernelTarget sourceTarget; | |
| final List<String> extraConstantStrings = []; | |
| new( | |
| this.description, | |
| this.component, | |
| this.userLibraries, | |
| this.compilationSetup, | |
| this.sourceTarget, [ | |
| this.outputUri, | |
| ]); | |
| bool isUserLibrary(Library library) { | |
| return isUserLibraryImportUri(library.importUri); | |
| } | |
| bool isUserLibraryImportUri(Uri? importUri) { | |
| // TODO(johnniwinther): Support patch libraries user libraries. | |
| return userLibraries.contains(importUri); | |
| } | |
| ProcessedOptions get options => compilationSetup.options; | |
| } | |
| /// Replace SDK locations starting with [path] with [replacement] and '*' | |
| /// instead of the line/column. | |
| /// | |
| /// For instance replacing | |
| /// | |
| /// out/ReleaseX64/dart-sdk/lib/core/enum.dart:101:13 | |
| /// | |
| /// with | |
| /// | |
| /// sdk/lib/core/enum.dart:* | |
| /// | |
| /// This is done to avoid expectations to depend on the actual location | |
| /// of the SDK or the position within the SDK file. | |
| String _replaceSdkLocation(String text, String path, String replacement) { | |
| // Replace path with line/column. | |
| RegExp regExp = new RegExp( | |
| '^// ${RegExp.escape(path)}([^:\r\n]*):\\d+:\\d+:', | |
| multiLine: true, | |
| ); | |
| text = text.replaceAllMapped( | |
| regExp, | |
| (Match match) => '// $replacement${match[1]}:*:', | |
| ); | |
| // Replace path with no line/column. | |
| return text.replaceAll(path, replacement); | |
| } | |
| String replacePathsInExpectText(String actual, Uri base) { | |
| String binariesPath = relativizeUri( | |
| Uri.base, | |
| platformBinariesLocation, | |
| isWindows, | |
| ); | |
| if (binariesPath.endsWith("/dart-sdk/lib/_internal/")) { | |
| // We are running from something like out/ReleaseX64/dart-sdk/bin/dart | |
| String search = binariesPath.substring( | |
| 0, | |
| binariesPath.length - "lib/_internal/".length, | |
| ); | |
| actual = _replaceSdkLocation(actual, search, "sdk/"); | |
| } else { | |
| // We are running from something like out/ReleaseX64/dart | |
| } | |
| actual = _replaceSdkLocation(actual, "sdk/", "sdk/"); | |
| actual = actual.replaceAll("$base", "org-dartlang-testcase:///"); | |
| actual = actual.replaceAll("\\n", "\n"); | |
| return actual; | |
| } |