| // 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 'package:_fe_analyzer_shared/src/testing/id_testing.dart'; |
| |
| /// Utility class for annotated testing representing a set of features. |
| class Features { |
| Map<String, Object> _features = {}; |
| Set<String> _unsorted = new Set<String>(); |
| |
| Features(); |
| |
| /// Creates a [Features] registering each key in [featuresMap] as a features |
| /// with the corresponding value(s) in the map. Note: values are expected to |
| /// be either a single `String` value or a `List<String>`. |
| factory Features.fromMap(Map<String, dynamic> featuresMap) { |
| Features features = new Features(); |
| featuresMap.forEach((key, value) { |
| if (value is List) { |
| for (dynamic v in value) { |
| features.addElement(key, v); |
| } |
| } else { |
| features.add(key, value: value); |
| } |
| }); |
| return features; |
| } |
| |
| /// Mark the feature [key] as existing. If [value] is provided, the feature |
| /// [key] is set to have this value. |
| void add(String key, {var value: ''}) { |
| _features[key] = value.toString(); |
| } |
| |
| /// Add [value] as an element of the list values of feature [key]. |
| void addElement(String key, [var value]) { |
| List<String> list = |
| _features.putIfAbsent(key, () => <String>[]) as List<String>; |
| if (value != null) { |
| list.add(value.toString()); |
| } |
| } |
| |
| /// Marks list values of [key] as unsorted. This prevents the [getText] |
| /// representation from automatically sorting the values. |
| void markAsUnsorted(String key) { |
| _unsorted.add(key); |
| } |
| |
| /// Returns `true` if feature [key] exists. |
| bool containsKey(String key) { |
| return _features.containsKey(key); |
| } |
| |
| /// Set the feature [key] to exist with the [value]. |
| void operator []=(String key, String value) { |
| _features[key] = value; |
| } |
| |
| /// Returns the value set for feature [key]. |
| Object? operator [](String key) => _features[key]; |
| |
| /// Removes the value set for feature [key]. Returns the existing value. |
| Object? remove(String key) => _features.remove(key); |
| |
| /// Returns `true` if this feature set is empty. |
| bool get isEmpty => _features.isEmpty; |
| |
| /// Returns `true` if this feature set is non-empty. |
| bool get isNotEmpty => _features.isNotEmpty; |
| |
| /// Call [f] for each feature in this feature set with its corresponding |
| /// value. |
| void forEach(void Function(String, Object) f) { |
| _features.forEach(f); |
| } |
| |
| /// Returns a string containing all features in a comma-separated list sorted |
| /// by feature names. |
| String getText([String? indent]) { |
| if (indent == null) { |
| StringBuffer sb = new StringBuffer(); |
| bool needsComma = false; |
| for (String name in _features.keys.toList()..sort()) { |
| dynamic value = _features[name]; |
| if (value != null) { |
| if (needsComma) { |
| sb.write(','); |
| } |
| sb.write(name); |
| if (value is List<String>) { |
| if (_unsorted.contains(name)) { |
| value = '[${value.join(',')}]'; |
| } else { |
| value = '[${(value..sort()).join(',')}]'; |
| } |
| } |
| if (value != '') { |
| sb.write('='); |
| sb.write(value); |
| } |
| needsComma = true; |
| } |
| } |
| return sb.toString(); |
| } else { |
| StringBuffer sb = new StringBuffer(); |
| Map<String, dynamic> values = {}; |
| for (String name in _features.keys.toList()..sort()) { |
| dynamic value = _features[name]; |
| if (value != null) { |
| values[name] = value; |
| } |
| } |
| String comma = ''; |
| if (values.length > 1) { |
| comma = '\n$indent '; |
| } |
| values.forEach((String name, dynamic value) { |
| sb.write(comma); |
| sb.write(name); |
| if (value is List<String>) { |
| if (value.length > 1) { |
| if (_unsorted.contains(name)) { |
| value = '[\n$indent ${value.join(',\n$indent ')}]'; |
| } else { |
| value = '[\n$indent ${(value..sort()).join(',\n$indent ')}]'; |
| } |
| } else { |
| if (_unsorted.contains(name)) { |
| value = '[${value.join(',')}]'; |
| } else { |
| value = '[${(value..sort()).join(',')}]'; |
| } |
| } |
| } |
| if (value != '') { |
| sb.write('='); |
| sb.write(value); |
| } |
| comma = ',\n$indent '; |
| }); |
| if (values.length > 1) { |
| sb.write('\n$indent'); |
| } |
| return sb.toString(); |
| } |
| } |
| |
| @override |
| String toString() => 'Features(${getText()})'; |
| |
| /// Creates a [Features] object by parse the [text] encoding. |
| /// |
| /// Single features will be parsed as strings and list features (features |
| /// encoded in `[...]` will be parsed as lists of strings. |
| static Features fromText(String? text) { |
| Features features = new Features(); |
| if (text == null) return features; |
| int index = 0; |
| while (index < text.length) { |
| int eqPos = text.indexOf('=', index); |
| int commaPos = text.indexOf(',', index); |
| String name; |
| bool hasValue = false; |
| if (eqPos != -1 && commaPos != -1) { |
| if (eqPos < commaPos) { |
| name = text.substring(index, eqPos); |
| hasValue = true; |
| index = eqPos + 1; |
| } else { |
| name = text.substring(index, commaPos); |
| index = commaPos + 1; |
| } |
| } else if (eqPos != -1) { |
| name = text.substring(index, eqPos); |
| hasValue = true; |
| index = eqPos + 1; |
| } else if (commaPos != -1) { |
| name = text.substring(index, commaPos); |
| index = commaPos + 1; |
| } else { |
| name = text.substring(index); |
| index = text.length; |
| } |
| if (hasValue) { |
| const Map<String, String> delimiters = const { |
| '[': ']', |
| '{': '}', |
| '(': ')', |
| '<': '>' |
| }; |
| List<String> endDelimiters = <String>[]; |
| bool isList = index < text.length && text.startsWith('[', index); |
| if (isList) { |
| features.addElement(name); |
| endDelimiters.add(']'); |
| index++; |
| } |
| int valueStart = index; |
| while (index < text.length) { |
| String char = text.substring(index, index + 1); |
| if (endDelimiters.isNotEmpty && endDelimiters.last == char) { |
| endDelimiters.removeLast(); |
| index++; |
| } else { |
| String? endDelimiter = delimiters[char]; |
| if (endDelimiter != null) { |
| endDelimiters.add(endDelimiter); |
| index++; |
| } else if (char == ',') { |
| if (endDelimiters.isEmpty) { |
| break; |
| } else if (endDelimiters.length == 1 && isList) { |
| String value = text.substring(valueStart, index); |
| features.addElement(name, value); |
| index++; |
| valueStart = index; |
| } else { |
| index++; |
| } |
| } else { |
| index++; |
| } |
| } |
| } |
| if (isList) { |
| String value = text.substring(valueStart, index - 1); |
| if (value.isNotEmpty) { |
| features.addElement(name, value); |
| } |
| } else { |
| String value = text.substring(valueStart, index); |
| features.add(name, value: value); |
| } |
| index++; |
| } else { |
| features.add(name); |
| } |
| } |
| return features; |
| } |
| } |
| |
| class FeaturesDataInterpreter implements DataInterpreter<Features> { |
| final String? wildcard; |
| |
| const FeaturesDataInterpreter({this.wildcard}); |
| |
| @override |
| String? isAsExpected(Features actualFeatures, String? expectedData) { |
| if (wildcard != null && expectedData == wildcard) { |
| return null; |
| } else if (expectedData == '') { |
| return actualFeatures.isNotEmpty ? "Expected empty data." : null; |
| } else { |
| List<String> errorsFound = []; |
| Features expectedFeatures = Features.fromText(expectedData); |
| Set<String> validatedFeatures = new Set<String>(); |
| expectedFeatures.forEach((String key, Object expectedValue) { |
| bool expectMatch = true; |
| if (key.startsWith('!')) { |
| key = key.substring(1); |
| expectMatch = false; |
| } |
| validatedFeatures.add(key); |
| Object? actualValue = actualFeatures[key]; |
| if (!expectMatch) { |
| if (actualFeatures.containsKey(key)) { |
| errorsFound.add('Unexpected data found for $key=$actualValue'); |
| } |
| } else if (!actualFeatures.containsKey(key)) { |
| errorsFound.add('No data found for $key'); |
| } else if (expectedValue == '') { |
| if (actualValue != '') { |
| errorsFound.add('Non-empty data found for $key'); |
| } |
| } else if (wildcard != null && expectedValue == wildcard) { |
| return; |
| } else if (expectedValue is List) { |
| if (actualValue is List) { |
| List actualList = actualValue.toList(); |
| for (Object expectedObject in expectedValue) { |
| String expectedText = '$expectedObject'; |
| bool matchFound = false; |
| if (wildcard != null && expectedText.endsWith(wildcard!)) { |
| // Wildcard matcher. |
| String prefix = |
| expectedText.substring(0, expectedText.indexOf(wildcard!)); |
| List matches = []; |
| for (Object actualObject in actualList) { |
| if ('$actualObject'.startsWith(prefix)) { |
| matches.add(actualObject); |
| matchFound = true; |
| } |
| } |
| for (Object match in matches) { |
| actualList.remove(match); |
| } |
| } else { |
| for (Object actualObject in actualList) { |
| if (expectedText == '$actualObject') { |
| actualList.remove(actualObject); |
| matchFound = true; |
| break; |
| } |
| } |
| } |
| if (!matchFound) { |
| errorsFound.add("No match found for $key=[$expectedText]"); |
| } |
| } |
| if (actualList.isNotEmpty) { |
| errorsFound |
| .add("Extra data found $key=[${actualList.join(',')}]"); |
| } |
| } else { |
| errorsFound.add("List data expected for $key: " |
| "expected '$expectedValue', found '${actualValue}'"); |
| } |
| } else if (expectedValue != actualValue) { |
| errorsFound.add("Mismatch for $key: expected '$expectedValue', " |
| "found '${actualValue}'"); |
| } |
| }); |
| actualFeatures.forEach((String key, Object value) { |
| if (!validatedFeatures.contains(key)) { |
| if (value == '') { |
| errorsFound.add("Extra data found '$key'"); |
| } else { |
| errorsFound.add("Extra data found $key=$value"); |
| } |
| } |
| }); |
| return errorsFound.isNotEmpty ? errorsFound.join('\n ') : null; |
| } |
| } |
| |
| @override |
| String getText(Features actualData, [String? indentation]) { |
| return actualData.getText(indentation); |
| } |
| |
| @override |
| bool isEmpty(Features? actualData) { |
| return actualData == null || actualData.isEmpty; |
| } |
| } |