| // Copyright (c) 2019, 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:convert' show json; |
| |
| import '../ast.dart'; |
| |
| import '../text/serializer_combinators.dart'; |
| |
| import '../text/text_reader.dart' show TextIterator; |
| |
| import '../text/text_serializer.dart'; |
| |
| const Uri? noUri = null; |
| |
| const int noOffset = -1; |
| |
| abstract class RoundTripStatus implements Comparable<RoundTripStatus> { |
| /// The round-trip serialization was run on that [node]. |
| final Node node; |
| |
| /// The context of the failure. |
| /// |
| /// The [context] node is a [TreeNode] and is set either to the node that the |
| /// round-trip serialization failed on or to the closest parent with location. |
| final TreeNode context; |
| |
| RoundTripStatus(this.node, {required TreeNode context}) |
| : context = node is TreeNode && node.location != null ? node : context; |
| |
| Uri? get uri => context.location?.file; |
| |
| int get offset => context.fileOffset; |
| |
| bool get isSuccess; |
| |
| bool get isFailure => !isSuccess; |
| |
| String get nameForDebugging; |
| |
| @override |
| int compareTo(RoundTripStatus other) { |
| Node thisNode = this.node; |
| Node otherNode = other.node; |
| if (thisNode is TreeNode && otherNode is TreeNode) { |
| Uri? thisUri = thisNode.location?.file; |
| Uri? otherUri = otherNode.location?.file; |
| int thisOffset = thisNode.fileOffset; |
| int otherOffset = otherNode.fileOffset; |
| |
| int compareUri; |
| if (thisUri == null && otherUri == null) { |
| compareUri = 0; |
| } else if (thisUri == null) { |
| compareUri = 1; |
| } else if (otherUri == null) { |
| compareUri = -1; |
| } else { |
| // ignore: unnecessary_null_comparison |
| assert(thisUri != null && otherUri != null); |
| compareUri = thisUri.toString().compareTo(otherUri.toString()); |
| } |
| if (compareUri != 0) return compareUri; |
| |
| int compareOffset; |
| // ignore: unnecessary_null_comparison |
| if (thisOffset == null && otherOffset == null) { |
| compareOffset = 0; |
| // ignore: unnecessary_null_comparison |
| } else if (thisOffset == null) { |
| compareOffset = 1; |
| // ignore: unnecessary_null_comparison |
| } else if (otherOffset == null) { |
| compareOffset = -1; |
| } else { |
| compareOffset = thisOffset = otherOffset; |
| } |
| if (compareOffset != 0) return compareOffset; |
| |
| // The "success" outcome has the lowest index. Make it so that it appears |
| // last, and the failures are at the beginning and are more visible. |
| if (isFailure && other.isSuccess) { |
| return -1; |
| } |
| if (isSuccess && other.isFailure) { |
| return 1; |
| } |
| |
| return 0; |
| } else if (node is TreeNode) { |
| return -1; |
| } else { |
| return 1; |
| } |
| } |
| |
| void printOn(StringBuffer sb) { |
| sb.writeln("" |
| ";; -------------------------------------" |
| "----------------------------------------"); |
| sb.writeln("Status: ${nameForDebugging}"); |
| sb.writeln("Node type: ${node.runtimeType}"); |
| sb.writeln("Node: ${json.encode(node.leakingDebugToString())}"); |
| Node treeNode = node; |
| if (treeNode is TreeNode) { |
| if (treeNode.parent != null) { |
| sb.writeln("Parent type: ${treeNode.parent.runtimeType}"); |
| sb.writeln( |
| "Parent: ${json.encode(treeNode.parent!.leakingDebugToString())}"); |
| } |
| } |
| } |
| |
| @override |
| String toString() { |
| StringBuffer sb = new StringBuffer(); |
| printOn(sb); |
| return sb.toString(); |
| } |
| } |
| |
| class RoundTripSuccess extends RoundTripStatus { |
| final String serialized; |
| |
| RoundTripSuccess(Node node, this.serialized, {required TreeNode context}) |
| : super(node, context: context); |
| |
| @override |
| bool get isSuccess => true; |
| |
| @override |
| String get nameForDebugging => "RoundTripSuccess"; |
| |
| @override |
| void printOn(StringBuffer sb) { |
| super.printOn(sb); |
| sb.writeln("Serialized: ${serialized}"); |
| } |
| } |
| |
| class RoundTripInitialSerializationFailure extends RoundTripStatus { |
| final String message; |
| |
| RoundTripInitialSerializationFailure(Node node, this.message, |
| {required TreeNode context}) |
| : super(node, context: context); |
| |
| @override |
| bool get isSuccess => false; |
| |
| @override |
| String get nameForDebugging => "RoundTripInitialSerializationFailure"; |
| |
| @override |
| void printOn(StringBuffer sb) { |
| super.printOn(sb); |
| sb.writeln("Message: ${message}"); |
| } |
| } |
| |
| class RoundTripDeserializationFailure extends RoundTripStatus { |
| final String message; |
| |
| RoundTripDeserializationFailure(Node node, this.message, |
| {required TreeNode context}) |
| : super(node, context: context); |
| |
| @override |
| bool get isSuccess => false; |
| |
| @override |
| String get nameForDebugging => "RoundTripDeserializationFailure"; |
| |
| @override |
| void printOn(StringBuffer sb) { |
| super.printOn(sb); |
| sb.writeln("Message: ${message}"); |
| } |
| } |
| |
| class RoundTripSecondSerializationFailure extends RoundTripStatus { |
| final String initial; |
| final String serialized; |
| |
| RoundTripSecondSerializationFailure(Node node, this.initial, this.serialized, |
| {required TreeNode context}) |
| : super(node, context: context); |
| |
| @override |
| bool get isSuccess => false; |
| |
| @override |
| String get nameForDebugging => "RoundTripSecondSerializationFailure"; |
| |
| @override |
| void printOn(StringBuffer sb) { |
| super.printOn(sb); |
| sb.writeln("Initial: ${initial}"); |
| sb.writeln("Serialized: ${serialized}"); |
| } |
| } |
| |
| class TextSerializationVerifier { |
| static const bool showStackTrace = bool.fromEnvironment( |
| "text_serialization.showStackTrace", |
| defaultValue: false); |
| |
| /// List of status for all round-trip serialization attempts. |
| final List<RoundTripStatus> _status = <RoundTripStatus>[]; |
| |
| final CanonicalName root; |
| |
| TextSerializationVerifier({CanonicalName? root}) |
| : root = root ?? new CanonicalName.root() { |
| initializeSerializers(); |
| } |
| |
| /// List of errors produced during round trips on the visited nodes. |
| Iterable<RoundTripStatus> get _failures => _status.where((s) => s.isFailure); |
| |
| List<RoundTripStatus> get failures => _failures.toList()..sort(); |
| |
| void verify(Library node) { |
| makeRoundTrip<Library>(node, librarySerializer); |
| } |
| |
| T? readNode<T extends TreeNode>( |
| T node, String input, TextSerializer<T> serializer) { |
| TextIterator stream = new TextIterator(input, 0); |
| stream.moveNext(); |
| T result; |
| try { |
| result = serializer.readFrom(stream, |
| new DeserializationState(new DeserializationEnvironment(null), root)); |
| } catch (exception, stackTrace) { |
| String message = |
| showStackTrace ? "${exception}\n${stackTrace}" : "${exception}"; |
| _status.add( |
| new RoundTripDeserializationFailure(node, message, context: node)); |
| return null; |
| } |
| if (stream.moveNext()) { |
| _status.add(new RoundTripDeserializationFailure( |
| node, "unexpected trailing text", |
| context: node)); |
| } |
| // ignore: unnecessary_null_comparison |
| if (result == null) { |
| _status.add(new RoundTripDeserializationFailure( |
| node, "Deserialization of the following returned null: '${input}'", |
| context: node)); |
| } |
| return result; |
| } |
| |
| String writeNode<T extends TreeNode>(T node, TextSerializer<T> serializer) { |
| StringBuffer buffer = new StringBuffer(); |
| try { |
| serializer.writeTo(buffer, node, |
| new SerializationState(new SerializationEnvironment(null))); |
| } catch (exception, stackTrace) { |
| String message = |
| showStackTrace ? "${exception}\n${stackTrace}" : "${exception}"; |
| _status.add(new RoundTripInitialSerializationFailure(node, message, |
| context: node)); |
| } |
| return buffer.toString(); |
| } |
| |
| void makeRoundTrip<T extends TreeNode>(T node, TextSerializer<T> serializer) { |
| int failureCount = _failures.length; |
| String initial = writeNode(node, serializer); |
| if (_failures.length != failureCount) { |
| return; |
| } |
| |
| // Do the round trip. |
| T? deserialized = readNode(node, initial, serializer); |
| if (_failures.length != failureCount) { |
| return; |
| } |
| |
| if (deserialized == null) { |
| // The error is reported elsewhere for the case of null. |
| return; |
| } |
| |
| String serialized = writeNode(deserialized, serializer); |
| if (_failures.length != failureCount) { |
| return; |
| } |
| |
| if (initial != serialized) { |
| _status.add(new RoundTripSecondSerializationFailure( |
| node, initial, serialized, |
| context: node)); |
| } else { |
| _status.add(new RoundTripSuccess(node, initial, context: node)); |
| } |
| } |
| |
| List<RoundTripStatus> takeStatus() { |
| List<RoundTripStatus> result = _status.toList()..sort(); |
| _status.clear(); |
| return result; |
| } |
| } |