| // 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:io'; | |
| import 'annotated_code_helper.dart'; | |
| import 'id.dart'; | |
| import 'id_generation.dart'; | |
| import '../util/colors.dart' as colors; | |
| import '../util/options.dart'; | |
| const String cfeMarker = 'cfe'; | |
| const String dart2jsMarker = 'dart2js'; | |
| const String analyzerMarker = 'analyzer'; | |
| const String ddcMarker = 'ddc'; | |
| /// Markers used in annotated tests shared by CFE, analyzer and dart2js. | |
| const List<String> sharedMarkers = [cfeMarker, dart2jsMarker, analyzerMarker]; | |
| /// Markers used in annotated tests shared by CFE and analyzer. | |
| const List<String> cfeAnalyzerMarkers = [cfeMarker, analyzerMarker]; | |
| /// `true` if ANSI colors are supported by stdout. | |
| bool useColors = stdout.supportsAnsiEscapes; | |
| /// Colorize a message [text], if ANSI colors are supported. | |
| String colorizeMessage(String text) { | |
| if (useColors) { | |
| return '${colors.YELLOW_COLOR}${text}${colors.DEFAULT_COLOR}'; | |
| } else { | |
| return text; | |
| } | |
| } | |
| /// Colorize a matching annotation [text], if ANSI colors are supported. | |
| String colorizeMatch(String text) { | |
| if (useColors) { | |
| return '${colors.BLUE_COLOR}${text}${colors.DEFAULT_COLOR}'; | |
| } else { | |
| return text; | |
| } | |
| } | |
| /// Colorize a single annotation [text], if ANSI colors are supported. | |
| String colorizeSingle(String text) { | |
| if (useColors) { | |
| return '${colors.GREEN_COLOR}${text}${colors.DEFAULT_COLOR}'; | |
| } else { | |
| return text; | |
| } | |
| } | |
| /// Colorize the actual annotation [text], if ANSI colors are supported. | |
| String colorizeActual(String text) { | |
| if (useColors) { | |
| return '${colors.RED_COLOR}${text}${colors.DEFAULT_COLOR}'; | |
| } else { | |
| return text; | |
| } | |
| } | |
| /// Colorize an expected annotation [text], if ANSI colors are supported. | |
| String colorizeExpected(String text) { | |
| if (useColors) { | |
| return '${colors.GREEN_COLOR}${text}${colors.DEFAULT_COLOR}'; | |
| } else { | |
| return text; | |
| } | |
| } | |
| /// Colorize delimiter [text], if ANSI colors are supported. | |
| String colorizeDelimiter(String text) { | |
| if (useColors) { | |
| return '${colors.YELLOW_COLOR}${text}${colors.DEFAULT_COLOR}'; | |
| } else { | |
| return text; | |
| } | |
| } | |
| /// Colorize diffs [expected] and [actual] and [delimiter], if ANSI colors are | |
| /// supported. | |
| String colorizeDiff(String expected, String delimiter, String actual) { | |
| return '${colorizeExpected(expected)}' | |
| '${colorizeDelimiter(delimiter)}${colorizeActual(actual)}'; | |
| } | |
| /// Colorize annotation delimiters [start] and [end] surrounding [text], if | |
| /// ANSI colors are supported. | |
| String colorizeAnnotation(String start, String text, String end) { | |
| return '${colorizeDelimiter(start)}$text${colorizeDelimiter(end)}'; | |
| } | |
| /// Creates an annotation that shows the difference between [expected] and | |
| /// [actual]. | |
| Annotation? createAnnotationsDiff(Annotation? expected, Annotation? actual) { | |
| if (identical(expected, actual)) return null; | |
| if (expected != null && actual != null) { | |
| return new Annotation( | |
| expected.index, | |
| expected.lineNo, | |
| expected.columnNo, | |
| expected.offset, | |
| expected.prefix, | |
| '${colorizeExpected(expected.text)}' | |
| '${colorizeDelimiter(' | ')}' | |
| '${colorizeActual(actual.text)}', | |
| expected.suffix, | |
| ); | |
| } else if (expected != null) { | |
| return new Annotation( | |
| expected.index, | |
| expected.lineNo, | |
| expected.columnNo, | |
| expected.offset, | |
| expected.prefix, | |
| '${colorizeExpected(expected.text)}' | |
| '${colorizeDelimiter(' | ')}' | |
| '${colorizeActual('---')}', | |
| expected.suffix, | |
| ); | |
| } else if (actual != null) { | |
| return new Annotation( | |
| actual.index, | |
| actual.lineNo, | |
| actual.columnNo, | |
| actual.offset, | |
| actual.prefix, | |
| '${colorizeExpected('---')}' | |
| '${colorizeDelimiter(' | ')}' | |
| '${colorizeActual(actual.text)}', | |
| actual.suffix, | |
| ); | |
| } else { | |
| return null; | |
| } | |
| } | |
| /// Encapsulates the member data computed for each source file of interest. | |
| /// It's a glorified wrapper around a map of maps, but written this way to | |
| /// provide a little more information about what it's doing. [DataType] refers | |
| /// to the type this map is holding -- it is either [IdValue] or [ActualData]. | |
| class MemberAnnotations<DataType> { | |
| /// For each Uri, we create a map associating an element id with its | |
| /// corresponding annotations. | |
| final Map<Uri, Map<Id, DataType>> _computedDataForEachFile = | |
| new Map<Uri, Map<Id, DataType>>(); | |
| /// Member or class annotations that don't refer to any of the user files. | |
| final Map<Id, DataType> globalData = <Id, DataType>{}; | |
| void operator []=(Uri file, Map<Id, DataType> computedData) { | |
| _computedDataForEachFile[file] = computedData; | |
| } | |
| void forEach(void f(Uri file, Map<Id, DataType> computedData)) { | |
| _computedDataForEachFile.forEach(f); | |
| } | |
| Map<Id, DataType>? operator [](Uri file) { | |
| if (!_computedDataForEachFile.containsKey(file)) { | |
| _computedDataForEachFile[file] = <Id, DataType>{}; | |
| } | |
| return _computedDataForEachFile[file]; | |
| } | |
| @override | |
| String toString() { | |
| StringBuffer sb = new StringBuffer(); | |
| sb.write('MemberAnnotations('); | |
| String comma = ''; | |
| if (_computedDataForEachFile.isNotEmpty && | |
| (_computedDataForEachFile.length > 1 || | |
| _computedDataForEachFile.values.single.isNotEmpty)) { | |
| sb.write('data:{'); | |
| _computedDataForEachFile.forEach((Uri uri, Map<Id, DataType> data) { | |
| sb.write(comma); | |
| sb.write('$uri:'); | |
| sb.write(data); | |
| comma = ','; | |
| }); | |
| sb.write('}'); | |
| } | |
| if (globalData.isNotEmpty) { | |
| sb.write(comma); | |
| sb.write('global:'); | |
| sb.write(globalData); | |
| } | |
| sb.write(')'); | |
| return sb.toString(); | |
| } | |
| } | |
| /// Compute a [MemberAnnotations] object from [code] for each marker in [maps] | |
| /// specifying the expected annotations. | |
| /// | |
| /// If an annotation starts with a marker, it is only expected for the | |
| /// corresponding test configuration. Otherwise it is expected for all | |
| /// configurations. | |
| // TODO(johnniwinther): Support an empty marker set. | |
| void computeExpectedMap( | |
| Uri sourceUri, | |
| String filename, | |
| AnnotatedCode code, | |
| Map<String, MemberAnnotations<IdValue>> maps, { | |
| required void onFailure(String message), | |
| bool preserveWhitespaceInAnnotations = false, | |
| bool preserveInfixWhitespaceInAnnotations = false, | |
| }) { | |
| List<String> mapKeys = maps.keys.toList(); | |
| Map<String, AnnotatedCode> split = splitByPrefixes(code, mapKeys); | |
| split.forEach((String marker, AnnotatedCode code) { | |
| MemberAnnotations<IdValue> fileAnnotations = maps[marker]!; | |
| Map<Id, IdValue> expectedValues = fileAnnotations[sourceUri]!; | |
| for (Annotation annotation in code.annotations) { | |
| String text = annotation.text; | |
| IdValue idValue = IdValue.decode( | |
| sourceUri, | |
| annotation, | |
| text, | |
| preserveWhitespaceInAnnotations: preserveWhitespaceInAnnotations, | |
| preserveInfixWhitespace: preserveInfixWhitespaceInAnnotations, | |
| ); | |
| if (idValue.id.isGlobal) { | |
| if (fileAnnotations.globalData.containsKey(idValue.id)) { | |
| onFailure( | |
| "Error in test '$filename': " | |
| "Duplicate annotations for ${idValue.id} in $marker: " | |
| "${idValue} and ${fileAnnotations.globalData[idValue.id]}.", | |
| ); | |
| } | |
| fileAnnotations.globalData[idValue.id] = idValue; | |
| } else { | |
| if (expectedValues.containsKey(idValue.id)) { | |
| onFailure( | |
| "Error in test '$filename': " | |
| "Duplicate annotations for ${idValue.id} in $marker: " | |
| "${idValue} and ${expectedValues[idValue.id]}.", | |
| ); | |
| } | |
| expectedValues[idValue.id] = idValue; | |
| } | |
| } | |
| }); | |
| } | |
| /// Creates a [TestData] object for the annotated test in [testFile]. | |
| /// | |
| /// If [testFile] is a file, use that directly. If it's a directory include | |
| /// everything in that directory. | |
| TestData computeTestData( | |
| FileSystemEntity testFile, { | |
| required Iterable<String> supportedMarkers, | |
| required Uri createTestUri(Uri uri, String fileName), | |
| required void onFailure(String message), | |
| String Function(String) preprocessFile = _noProcessing, | |
| bool preserveWhitespaceInAnnotations = false, | |
| bool preserveInfixWhitespaceInAnnotations = false, | |
| }) { | |
| Uri? entryPoint; | |
| String testName; | |
| File? mainTestFile; | |
| Uri testFileUri = testFile.uri; | |
| Map<String, File>? additionalFiles; | |
| if (testFile is File) { | |
| testName = testFileUri.pathSegments.last; | |
| mainTestFile = testFile; | |
| entryPoint = createTestUri(mainTestFile.uri, 'main.dart'); | |
| } else if (testFile is Directory) { | |
| testName = testFileUri.pathSegments[testFileUri.pathSegments.length - 2]; | |
| additionalFiles = new Map<String, File>(); | |
| for (FileSystemEntity entry in testFile | |
| .listSync(recursive: true) | |
| .where((entity) => !entity.path.endsWith('~'))) { | |
| if (entry is! File) continue; | |
| if (entry.uri.pathSegments.last == "main.dart") { | |
| mainTestFile = entry; | |
| entryPoint = createTestUri(mainTestFile.uri, 'main.dart'); | |
| } else { | |
| additionalFiles[entry.uri.path.substring(testFile.uri.path.length)] = | |
| entry; | |
| } | |
| } | |
| assert( | |
| mainTestFile != null, | |
| "No 'main.dart' test file found for $testFile.", | |
| ); | |
| } else { | |
| throw new UnimplementedError(); | |
| } | |
| String annotatedCode = preprocessFile( | |
| new File.fromUri(mainTestFile!.uri).readAsStringSync(), | |
| ); | |
| Map<Uri, AnnotatedCode> code = { | |
| entryPoint!: new AnnotatedCode.fromText( | |
| annotatedCode, | |
| commentStart, | |
| commentEnd, | |
| ), | |
| }; | |
| Map<String, MemberAnnotations<IdValue>> expectedMaps = {}; | |
| for (String testMarker in supportedMarkers) { | |
| expectedMaps[testMarker] = new MemberAnnotations<IdValue>(); | |
| } | |
| computeExpectedMap( | |
| entryPoint, | |
| testFile.uri.pathSegments.last, | |
| code[entryPoint]!, | |
| expectedMaps, | |
| onFailure: onFailure, | |
| preserveWhitespaceInAnnotations: preserveWhitespaceInAnnotations, | |
| preserveInfixWhitespaceInAnnotations: preserveInfixWhitespaceInAnnotations, | |
| ); | |
| Map<String, String> memorySourceFiles = { | |
| entryPoint.path: code[entryPoint]!.sourceCode, | |
| }; | |
| if (additionalFiles != null) { | |
| for (MapEntry<String, File> additionalFileData in additionalFiles.entries) { | |
| String libFileName = additionalFileData.key; | |
| File libEntity = additionalFileData.value; | |
| Uri libFileUri = createTestUri(libEntity.uri, libFileName); | |
| String libCode = preprocessFile(libEntity.readAsStringSync()); | |
| AnnotatedCode annotatedLibCode = new AnnotatedCode.fromText( | |
| libCode, | |
| commentStart, | |
| commentEnd, | |
| ); | |
| memorySourceFiles[libFileUri.path] = annotatedLibCode.sourceCode; | |
| code[libFileUri] = annotatedLibCode; | |
| computeExpectedMap( | |
| libFileUri, | |
| libFileName, | |
| annotatedLibCode, | |
| expectedMaps, | |
| onFailure: onFailure, | |
| preserveWhitespaceInAnnotations: preserveWhitespaceInAnnotations, | |
| preserveInfixWhitespaceInAnnotations: | |
| preserveInfixWhitespaceInAnnotations, | |
| ); | |
| } | |
| } | |
| return new TestData( | |
| testName, | |
| testFileUri, | |
| entryPoint, | |
| memorySourceFiles, | |
| code, | |
| expectedMaps, | |
| ); | |
| } | |
| /// Data for an annotated test. | |
| class TestData { | |
| final String name; | |
| final Uri testFileUri; | |
| final Uri entryPoint; | |
| final Map<String, String> memorySourceFiles; | |
| final Map<Uri, AnnotatedCode> code; | |
| final Map<String, MemberAnnotations<IdValue>> expectedMaps; | |
| TestData( | |
| this.name, | |
| this.testFileUri, | |
| this.entryPoint, | |
| this.memorySourceFiles, | |
| this.code, | |
| this.expectedMaps, | |
| ); | |
| } | |
| /// The results for running a test on a single configuration. | |
| class TestResult<T> { | |
| /// `true` if the [compiledData] didn't match the expected annotations. | |
| final bool hasMismatches; | |
| /// `true` if the test couldn't be run due to errors in the test setup. | |
| final bool isErroneous; | |
| /// The data interpreter used to verify the [compiledData]. | |
| final DataInterpreter<T>? interpreter; | |
| /// The actual data computed for the test. | |
| final CompiledData<T>? compiledData; | |
| TestResult(this.interpreter, this.compiledData, this.hasMismatches) | |
| : isErroneous = false; | |
| TestResult.erroneous() | |
| : isErroneous = true, | |
| hasMismatches = false, | |
| interpreter = null, | |
| compiledData = null; | |
| bool get hasFailures => hasMismatches || isErroneous; | |
| } | |
| /// The actual result computed for an annotated test. | |
| abstract class CompiledData<T> { | |
| final Uri mainUri; | |
| /// For each Uri, a map associating an element id with the instrumentation | |
| /// data we've collected for it. | |
| final Map<Uri, Map<Id, ActualData<T>>> actualMaps; | |
| /// Collected instrumentation data that doesn't refer to any of the user | |
| /// files. (E.g. information the test has collected about files in | |
| /// `dart:core`). | |
| final Map<Id, ActualData<T>> globalData; | |
| CompiledData(this.mainUri, this.actualMaps, this.globalData); | |
| Map<int, List<String>> computeAnnotations(Uri uri) { | |
| Map<Id, ActualData<T>> actualMap = actualMaps[uri]!; | |
| Map<int, List<String>> annotations = <int, List<String>>{}; | |
| actualMap.forEach((Id id, ActualData<T> data) { | |
| String value1 = '${data.value}'; | |
| annotations | |
| .putIfAbsent(data.offset, () => []) | |
| .add(colorizeActual(value1)); | |
| }); | |
| return annotations; | |
| } | |
| Map<int, List<String>> computeDiffAnnotationsAgainst( | |
| Map<Id, ActualData<T>> thisMap, | |
| Map<Id, ActualData<T>> otherMap, | |
| Uri uri, { | |
| bool includeMatches = false, | |
| }) { | |
| Map<int, List<String>> annotations = <int, List<String>>{}; | |
| thisMap.forEach((Id id, ActualData<T> thisData) { | |
| ActualData<T>? otherData = otherMap[id]; | |
| String thisValue = '${thisData.value}'; | |
| if (thisData.value != otherData?.value) { | |
| String otherValue = '${otherData?.value ?? '---'}'; | |
| annotations | |
| .putIfAbsent(thisData.offset, () => []) | |
| .add(colorizeDiff(thisValue, ' | ', otherValue)); | |
| } else if (includeMatches) { | |
| annotations | |
| .putIfAbsent(thisData.offset, () => []) | |
| .add(colorizeMatch(thisValue)); | |
| } | |
| }); | |
| otherMap.forEach((Id id, ActualData<T> otherData) { | |
| if (!thisMap.containsKey(id)) { | |
| String thisValue = '---'; | |
| String otherValue = '${otherData.value}'; | |
| annotations | |
| .putIfAbsent(otherData.offset, () => []) | |
| .add(colorizeDiff(thisValue, ' | ', otherValue)); | |
| } | |
| }); | |
| return annotations; | |
| } | |
| int getOffsetFromId(Id id, Uri uri); | |
| void reportError( | |
| Uri uri, | |
| int offset, | |
| String message, { | |
| bool succinct = false, | |
| }); | |
| } | |
| /// Interface used for interpreting annotations. | |
| abstract class DataInterpreter<T> { | |
| /// Returns `null` if [actualData] satisfies the [expectedData] annotation. | |
| /// Otherwise, a message is returned contain the information about the | |
| /// problems found. | |
| String? isAsExpected(T actualData, String? expectedData); | |
| /// Returns `true` if [actualData] corresponds to empty data. | |
| bool isEmpty(T actualData); | |
| /// Returns a textual representation of [actualData]. | |
| /// | |
| /// If [indentation] is provided a multiline pretty printing can be returned | |
| /// using [indentation] for additional lines. | |
| String getText(T actualData, [String? indentation]); | |
| } | |
| /// Default data interpreter for string data. | |
| class StringDataInterpreter implements DataInterpreter<String> { | |
| const StringDataInterpreter(); | |
| @override | |
| String? isAsExpected(String actualData, String? expectedData) { | |
| expectedData ??= ''; | |
| if (actualData != expectedData) { | |
| return "Expected $expectedData, found $actualData"; | |
| } | |
| return null; | |
| } | |
| @override | |
| bool isEmpty(String actualData) { | |
| return actualData == ''; | |
| } | |
| @override | |
| String getText(String actualData, [String? indentation]) { | |
| return actualData; | |
| } | |
| } | |
| String withAnnotations(String sourceCode, Map<int, List<String>> annotations) { | |
| StringBuffer sb = new StringBuffer(); | |
| int end = 0; | |
| for (int offset in annotations.keys.toList()..sort()) { | |
| if (offset >= sourceCode.length) { | |
| sb.write('...'); | |
| return sb.toString(); | |
| } | |
| if (offset > end) { | |
| sb.write(sourceCode.substring(end, offset)); | |
| } | |
| for (String annotation in annotations[offset]!) { | |
| sb.write(colorizeAnnotation('/*', annotation, '*/')); | |
| } | |
| end = offset; | |
| } | |
| if (end < sourceCode.length) { | |
| sb.write(sourceCode.substring(end)); | |
| } | |
| return sb.toString(); | |
| } | |
| /// Checks [compiledData] against the expected data in [expectedMaps] derived | |
| /// from [testData].code. | |
| Future<TestResult<T>> checkCode<T>( | |
| MarkerOptions markerOptions, | |
| String marker, | |
| String modeName, | |
| TestData testData, | |
| MemberAnnotations<IdValue> expectedMaps, | |
| CompiledData<T> compiledData, | |
| DataInterpreter<T> dataInterpreter, { | |
| bool filterActualData(IdValue? expected, ActualData<T> actualData)?, | |
| bool fatalErrors = true, | |
| bool succinct = false, | |
| required void onFailure(String message), | |
| }) async { | |
| String testName = testData.name; | |
| Map<Uri, AnnotatedCode> code = testData.code; | |
| bool hasFailure = false; | |
| Set<Uri> neededDiffs = new Set<Uri>(); | |
| void checkActualMap( | |
| Map<Id, ActualData<T>> actualMap, | |
| Map<Id, IdValue>? expectedMap, | |
| Uri uri, | |
| ) { | |
| expectedMap ??= {}; | |
| bool hasLocalFailure = false; | |
| actualMap.forEach((Id id, ActualData<T> actualData) { | |
| T actual = actualData.value; | |
| String actualText = dataInterpreter.getText(actual); | |
| if (!expectedMap!.containsKey(id)) { | |
| if (!dataInterpreter.isEmpty(actual)) { | |
| String actualValueText = IdValue.idToString(id, actualText); | |
| compiledData.reportError( | |
| actualData.uri, | |
| actualData.offset, | |
| succinct | |
| ? 'EXTRA $modeName DATA for ${id.descriptor}' | |
| : 'EXTRA $modeName DATA for ${id.descriptor}:\n ' | |
| 'object : ${actualData.objectText}\n ' | |
| 'actual : ${colorizeActual(actualValueText)}\n ' | |
| 'Data was expected for these ids: ${expectedMap.keys}', | |
| succinct: succinct, | |
| ); | |
| if (filterActualData == null || filterActualData(null, actualData)) { | |
| hasLocalFailure = true; | |
| } | |
| } | |
| } else { | |
| IdValue expected = expectedMap[id]!; | |
| String? unexpectedMessage = dataInterpreter.isAsExpected( | |
| actual, | |
| expected.value, | |
| ); | |
| if (unexpectedMessage != null) { | |
| String actualValueText = IdValue.idToString(id, actualText); | |
| compiledData.reportError( | |
| actualData.uri, | |
| actualData.offset, | |
| succinct | |
| ? 'UNEXPECTED $modeName DATA for ${id.descriptor}' | |
| : 'UNEXPECTED $modeName DATA for ${id.descriptor}:\n ' | |
| 'detail : ${colorizeMessage(unexpectedMessage)}\n ' | |
| 'object : ${actualData.objectText}\n ' | |
| 'expected: ${colorizeExpected('$expected')}\n ' | |
| 'actual : ${colorizeActual(actualValueText)}', | |
| succinct: succinct, | |
| ); | |
| if (filterActualData == null || | |
| filterActualData(expected, actualData)) { | |
| hasLocalFailure = true; | |
| } | |
| } | |
| } | |
| }); | |
| if (hasLocalFailure) { | |
| hasFailure = true; | |
| neededDiffs.add(uri); | |
| } | |
| } | |
| compiledData.actualMaps.forEach((Uri uri, Map<Id, ActualData<T>> actualMap) { | |
| checkActualMap(actualMap, expectedMaps[uri], uri); | |
| }); | |
| checkActualMap( | |
| compiledData.globalData, | |
| expectedMaps.globalData, | |
| Uri.parse("global:data"), | |
| ); | |
| Set<Id> missingIds = new Set<Id>(); | |
| void checkMissing( | |
| Map<Id, IdValue>? expectedMap, | |
| Map<Id, ActualData<T>>? actualMap, [ | |
| Uri? uri, | |
| ]) { | |
| actualMap ??= {}; | |
| expectedMap?.forEach((Id id, IdValue expected) { | |
| if (!actualMap!.containsKey(id)) { | |
| missingIds.add(id); | |
| String message = | |
| 'MISSING $modeName DATA for ${id.descriptor}: ' | |
| 'Expected ${colorizeExpected('$expected')}'; | |
| if (uri != null) { | |
| compiledData.reportError( | |
| uri, | |
| compiledData.getOffsetFromId(id, uri), | |
| message, | |
| succinct: succinct, | |
| ); | |
| } else { | |
| print(message); | |
| } | |
| } | |
| }); | |
| if (missingIds.isNotEmpty && uri != null) { | |
| neededDiffs.add(uri); | |
| } | |
| } | |
| expectedMaps.forEach((Uri uri, Map<Id, IdValue> expectedMap) { | |
| checkMissing(expectedMap, compiledData.actualMaps[uri], uri); | |
| }); | |
| checkMissing(expectedMaps.globalData, compiledData.globalData); | |
| if (!succinct) { | |
| if (neededDiffs.isNotEmpty) { | |
| Map<Uri, List<Annotation>> annotations = computeAnnotationsPerUri( | |
| code, | |
| {'dummyMarker': expectedMaps}, | |
| compiledData.mainUri, | |
| {'dummyMarker': compiledData.actualMaps}, | |
| dataInterpreter, | |
| createDiff: createAnnotationsDiff, | |
| ); | |
| for (Uri uri in neededDiffs) { | |
| print('--annotations diff [${uri.pathSegments.last}]-------------'); | |
| AnnotatedCode? annotatedCode = code[uri]; | |
| print( | |
| new AnnotatedCode( | |
| annotatedCode?.annotatedCode ?? "", | |
| annotatedCode?.sourceCode ?? "", | |
| annotations[uri] ?? const [], | |
| ).toText(), | |
| ); | |
| print('----------------------------------------------------------'); | |
| } | |
| } | |
| } | |
| if (missingIds.isNotEmpty) { | |
| print("MISSING ids: ${missingIds}."); | |
| hasFailure = true; | |
| } | |
| if (hasFailure && fatalErrors) { | |
| onFailure( | |
| generateErrorMessage( | |
| markerOptions, | |
| mismatches: { | |
| testName: {marker}, | |
| }, | |
| ), | |
| ); | |
| } | |
| return new TestResult<T>(dataInterpreter, compiledData, hasFailure); | |
| } | |
| typedef Future<Map<String, TestResult<T>>> RunTestFunction<T>( | |
| MarkerOptions markerOptions, | |
| TestData testData, { | |
| required bool testAfterFailures, | |
| required bool verbose, | |
| required bool succinct, | |
| required bool printCode, | |
| Map<String, List<String>>? skipMap, | |
| required Uri nullUri, | |
| }); | |
| /// Compute the file: URI of the file located at `path`, where `path` is | |
| /// relative to the root of the SDK repository. | |
| /// | |
| /// We find the root of the SDK repository by looking for the parent of the | |
| /// directory named `pkg`. | |
| Uri _fileUriFromSdkRoot(String path) { | |
| Uri uri = Platform.script; | |
| List<String> pathSegments = uri.pathSegments; | |
| return uri.replace( | |
| pathSegments: [ | |
| ...pathSegments.sublist(0, pathSegments.lastIndexOf('pkg')), | |
| ...path.split('/'), | |
| ], | |
| ); | |
| } | |
| /// Description of a tester for a specific marker. | |
| /// | |
| /// A tester is the runnable dart script used to run the tests for a certain | |
| /// configurations. For instance | |
| /// `pkg/front_end/test/id_tests/constant_test.dart` is used to run the test | |
| /// data in `pkg/_fe_analyzer_shared/test/constants/data/` using the front end | |
| /// and `pkg/analyzer/test/id_tests/constant_test.dart` is used to run the same | |
| /// test data using the analyzer. | |
| class MarkerTester { | |
| /// The marker supported by this tester, for instance 'cfe' for front end | |
| /// testing. | |
| final String marker; | |
| /// The path of the tester relative to the sdk repo root, for instance | |
| /// `pkg/front_end/test/id_tests/constant_test.dart`. | |
| final String path; | |
| /// The resolved file uri for the test. | |
| final Uri uri; | |
| MarkerTester(this.marker, this.path, this.uri); | |
| @override | |
| String toString() => 'MarkerTester($marker,$path,$uri)'; | |
| } | |
| /// A model of the testers defined in a `marker.options` file, located in the | |
| /// data folder for id tests. | |
| class MarkerOptions { | |
| /// The supported markers and their corresponding testers. | |
| final Map<String, MarkerTester> markers; | |
| MarkerOptions.internal(this.markers); | |
| factory MarkerOptions.fromDataDir( | |
| Directory dataDir, { | |
| bool shouldFindScript = true, | |
| }) { | |
| File file = new File.fromUri(dataDir.uri.resolve('marker.options')); | |
| File script = new File.fromUri(Platform.script); | |
| if (!file.existsSync()) { | |
| throw new ArgumentError( | |
| "Marker option file '${file.uri}' doesn't exist.", | |
| ); | |
| } | |
| Map<String, MarkerTester> markers = {}; | |
| String text = file.readAsStringSync(); | |
| bool isScriptFound = false; | |
| for (String line in text.split('\n')) { | |
| line = line.trim(); | |
| if (line.isEmpty || line.startsWith('#')) continue; | |
| int eqPos = line.indexOf('='); | |
| if (eqPos == -1) { | |
| throw new ArgumentError( | |
| "Unsupported marker option '$line' in ${file.uri}", | |
| ); | |
| } | |
| String marker = line.substring(0, eqPos); | |
| String tester = line.substring(eqPos + 1); | |
| File testerFile = new File.fromUri(_fileUriFromSdkRoot(tester)); | |
| if (!testerFile.existsSync()) { | |
| throw new ArgumentError( | |
| "Tester '$tester' does not exist for marker '$marker' in " | |
| "${file.uri}", | |
| ); | |
| } | |
| if (markers.containsKey(marker)) { | |
| throw new ArgumentError("Duplicate marker '$marker' in ${file.uri}"); | |
| } | |
| markers[marker] = new MarkerTester(marker, tester, testerFile.uri); | |
| if (testerFile.absolute.uri == script.absolute.uri) { | |
| isScriptFound = true; | |
| } | |
| } | |
| if (shouldFindScript && !isScriptFound) { | |
| throw new ArgumentError( | |
| "Script '${script.uri}' not found in ${file.uri}", | |
| ); | |
| } | |
| return new MarkerOptions.internal(markers); | |
| } | |
| Iterable<String> get supportedMarkers => markers.keys; | |
| Future<void> runAll(List<String> args) async { | |
| bool assertsEnabled = true; | |
| try { | |
| assert(false); | |
| assertsEnabled = false; | |
| } catch (_) {} | |
| Set<MarkerTester> testers = markers.values.toSet(); | |
| int errorsCount = 0; | |
| for (MarkerTester tester in testers) { | |
| print('================================================================'); | |
| print('Running tester: ${tester.path} ${args.join(' ')}'); | |
| print('================================================================'); | |
| Process process = await Process.start(Platform.resolvedExecutable, [ | |
| if (assertsEnabled) '--enable_asserts', | |
| tester.uri.toString(), | |
| ...args, | |
| ], mode: ProcessStartMode.inheritStdio); | |
| if (await process.exitCode != 0) { | |
| errorsCount++; | |
| } | |
| } | |
| if (errorsCount != 0) { | |
| throw "Error${errorsCount == 1 ? "" : "s"} occurred."; | |
| } | |
| } | |
| } | |
| String getTestName(FileSystemEntity entity) { | |
| if (entity is Directory) { | |
| return entity.uri.pathSegments[entity.uri.pathSegments.length - 2]; | |
| } else { | |
| return entity.uri.pathSegments.last; | |
| } | |
| } | |
| // TODO(johnniwinther): Support --show to show actual data for an input. | |
| class Options { | |
| static const Option<bool> runAll = const Option<bool>( | |
| '--run-all', | |
| const BoolValue(false), | |
| ); | |
| static const Option<bool> verbose = const Option<bool>( | |
| '--verbose', | |
| const BoolValue(false), | |
| aliases: ['-v'], | |
| ); | |
| static const Option<bool> succinct = const Option<bool>( | |
| '--succinct', | |
| const BoolValue(false), | |
| aliases: ['-s'], | |
| ); | |
| static const Option<bool> shouldContinue = const Option<bool>( | |
| '--continue', | |
| const BoolValue(false), | |
| aliases: ['-c'], | |
| ); | |
| static const Option<bool> stopAfterFailures = const Option<bool>( | |
| '--stop-after-failures', | |
| const BoolValue(false), | |
| aliases: ['-a'], | |
| ); | |
| static const Option<bool> printCode = const Option<bool>( | |
| '--print-code', | |
| const BoolValue(false), | |
| aliases: ['-p'], | |
| ); | |
| static const Option<bool> generateAnnotations = const Option<bool>( | |
| '--generate', | |
| const BoolValue(false), | |
| aliases: ['-g'], | |
| ); | |
| static const Option<bool> forceUpdate = const Option<bool>( | |
| '--force-update', | |
| const BoolValue(false), | |
| aliases: ['-f'], | |
| ); | |
| } | |
| const List<Option> idTestOptions = [ | |
| Options.runAll, | |
| Options.verbose, | |
| Options.succinct, | |
| Options.shouldContinue, | |
| Options.stopAfterFailures, | |
| Options.printCode, | |
| Options.generateAnnotations, | |
| Options.forceUpdate, | |
| ]; | |
| String _noProcessing(String s) => s; | |
| /// Check code for all tests in [dataDir] using [runTest]. | |
| Future<void> runTests<T>( | |
| Directory dataDir, { | |
| List<String> args = const <String>[], | |
| int shards = 1, | |
| int shardIndex = 0, | |
| void onTest(Uri uri)?, | |
| required Uri createUriForFileName(String fileName), | |
| required void onFailure(String message), | |
| required RunTestFunction<T> runTest, | |
| List<String>? skipList, | |
| Map<String, List<String>>? skipMap, | |
| bool preserveWhitespaceInAnnotations = false, | |
| bool preserveInfixWhitespaceInAnnotations = false, | |
| String Function(String) preProcessFile = _noProcessing, | |
| String Function(String) postProcessFile = _noProcessing, | |
| }) async { | |
| ParsedOptions parsedOptions = ParsedOptions.parse(args, idTestOptions); | |
| MarkerOptions markerOptions = new MarkerOptions.fromDataDir( | |
| dataDir, | |
| shouldFindScript: shards == 1, | |
| ); | |
| if (Options.runAll.read(parsedOptions)) { | |
| await markerOptions.runAll(Options.runAll.remove(args.toList())); | |
| return; | |
| } | |
| bool verbose = Options.verbose.read(parsedOptions); | |
| bool succinct = Options.succinct.read(parsedOptions); | |
| bool shouldContinue = Options.shouldContinue.read(parsedOptions); | |
| bool stopAfterFailures = Options.stopAfterFailures.read(parsedOptions); | |
| bool printCode = Options.printCode.read(parsedOptions); | |
| bool generateAnnotations = Options.generateAnnotations.read(parsedOptions); | |
| bool forceUpdate = Options.forceUpdate.read(parsedOptions); | |
| List<String> arguments = parsedOptions.arguments; | |
| bool continued = false; | |
| Map<String, Set<String>> mismatches = {}; | |
| Map<String, Set<String>> errors = {}; | |
| String relativeDir = dataDir.uri.path.replaceAll(Uri.base.path, ''); | |
| print('Data dir: ${relativeDir}'); | |
| List<FileSystemEntity> entities = | |
| dataDir | |
| .listSync() | |
| .where( | |
| (entity) => | |
| !entity.path.endsWith('~') && | |
| !entity.path.endsWith('marker.options') && | |
| !entity.path.endsWith('.expect'), | |
| ) | |
| .toList(); | |
| if (shards > 1) { | |
| entities.sort((a, b) => getTestName(a).compareTo(getTestName(b))); | |
| int start = entities.length * shardIndex ~/ shards; | |
| int end = entities.length * (shardIndex + 1) ~/ shards; | |
| entities = entities.sublist(start, end); | |
| } | |
| int testCount = 0; | |
| for (FileSystemEntity entity in entities) { | |
| String testName = getTestName(entity); | |
| if (arguments.isNotEmpty && !arguments.contains(testName) && !continued) { | |
| continue; | |
| } | |
| if (shouldContinue) continued = true; | |
| testCount++; | |
| if (skipList != null && | |
| skipList.contains(testName) && | |
| !arguments.contains(testName)) { | |
| print('Skip: ${testName}'); | |
| continue; | |
| } | |
| if (onTest != null) { | |
| onTest(entity.uri); | |
| } | |
| print('----------------------------------------------------------------'); | |
| Map<Uri, Uri> testToFileUri = {}; | |
| Uri createTestUri(Uri fileUri, String fileName) { | |
| Uri testUri = createUriForFileName(fileName); | |
| testToFileUri[testUri] = fileUri; | |
| return testUri; | |
| } | |
| TestData testData = computeTestData( | |
| entity, | |
| supportedMarkers: markerOptions.supportedMarkers, | |
| createTestUri: createTestUri, | |
| onFailure: onFailure, | |
| preserveWhitespaceInAnnotations: preserveWhitespaceInAnnotations, | |
| preserveInfixWhitespaceInAnnotations: | |
| preserveInfixWhitespaceInAnnotations, | |
| preprocessFile: preProcessFile, | |
| ); | |
| print('Test: ${testData.testFileUri}'); | |
| Map<String, TestResult<T>> results = await runTest( | |
| markerOptions, | |
| testData, | |
| testAfterFailures: !stopAfterFailures || generateAnnotations, | |
| verbose: verbose, | |
| succinct: succinct, | |
| printCode: printCode, | |
| skipMap: skipMap, | |
| nullUri: createTestUri(entity.uri.resolve("null"), "null"), | |
| ); | |
| Set<String> mismatchMarkers = {}; | |
| Set<String> errorMarkers = {}; | |
| results.forEach((String marker, TestResult<T> result) { | |
| if (result.hasMismatches) { | |
| mismatchMarkers.add(marker); | |
| } else if (result.isErroneous) { | |
| errorMarkers.add(marker); | |
| } | |
| }); | |
| if (errorMarkers.isNotEmpty) { | |
| // Cannot generate annotations for erroneous tests. | |
| if (mismatchMarkers.isNotEmpty) { | |
| mismatches[testName] = mismatchMarkers; | |
| } | |
| errors[testName] = errorMarkers; | |
| } else if (mismatchMarkers.isNotEmpty || | |
| (forceUpdate && generateAnnotations)) { | |
| if (generateAnnotations) { | |
| DataInterpreter? dataInterpreter; | |
| Map<String, Map<Uri, Map<Id, ActualData<T>>>> actualData = {}; | |
| results.forEach((String marker, TestResult<T> result) { | |
| dataInterpreter ??= result.interpreter; | |
| Map<Uri, Map<Id, ActualData<T>>> actualDataPerUri = | |
| actualData[marker] = {}; | |
| void addActualData(Uri uri, Map<Id, ActualData<T>> actualData) { | |
| assert( | |
| testData.code.containsKey(uri) || actualData.isEmpty, | |
| "Unexpected data ${actualData} for $uri", | |
| ); | |
| if (actualData.isEmpty) { | |
| // TODO(johnniwinther): Avoid collecting data without | |
| // invalid uris. | |
| return; | |
| } | |
| Map<Id, ActualData<T>> actualDataPerId = | |
| actualDataPerUri[uri] ??= {}; | |
| actualDataPerId.addAll(actualData); | |
| } | |
| result.compiledData!.actualMaps.forEach(addActualData); | |
| addActualData( | |
| result.compiledData!.mainUri, | |
| result.compiledData!.globalData, | |
| ); | |
| }); | |
| Map<Uri, List<Annotation>> annotations = computeAnnotationsPerUri( | |
| testData.code, | |
| testData.expectedMaps, | |
| testData.entryPoint, | |
| actualData, | |
| dataInterpreter!, | |
| forceUpdate: forceUpdate, | |
| ); | |
| annotations.forEach((Uri uri, List<Annotation> annotations) { | |
| AnnotatedCode? code = testData.code[uri]; | |
| assert( | |
| code != null, | |
| "No annotated code for $uri with annotations: $annotations", | |
| ); | |
| AnnotatedCode generated = new AnnotatedCode( | |
| code?.annotatedCode ?? "", | |
| code?.sourceCode ?? "", | |
| annotations, | |
| ); | |
| Uri fileUri = testToFileUri[uri]!; | |
| new File.fromUri( | |
| fileUri, | |
| ).writeAsStringSync(postProcessFile(generated.toText())); | |
| print('Generated annotations for ${fileUri}'); | |
| }); | |
| } else { | |
| mismatches[testName] = mismatchMarkers; | |
| } | |
| } | |
| } | |
| if (mismatches.isNotEmpty || errors.isNotEmpty) { | |
| onFailure( | |
| generateErrorMessage( | |
| markerOptions, | |
| mismatches: mismatches, | |
| errors: errors, | |
| ), | |
| ); | |
| } | |
| if (testCount == 0) { | |
| onFailure("No files were tested."); | |
| } | |
| } | |
| const String _dartLocationMessage = """ | |
| * Note that the 'dart' executable *must* be the one | |
| located in the 'dart-sdk/bin' folder of the build | |
| directory, for instance | |
| out/ReleaseX64/dart-sdk/bin/dart"""; | |
| /// Generates an error message for the [mismatches] and [errors] using | |
| /// [markerOptions] to provide a re-run/re-generation command. | |
| /// | |
| /// [mismatches] and [errors] are maps from the test names of the failing tests | |
| /// to the markers for the configurations in which they failed. | |
| String generateErrorMessage( | |
| MarkerOptions markerOptions, { | |
| Map<String, Set<String>> mismatches = const {}, | |
| Map<String, Set<String>> errors = const {}, | |
| }) { | |
| Set<String> testNames = {...mismatches.keys, ...errors.keys}; | |
| StringBuffer message = new StringBuffer(); | |
| message.writeln(); | |
| message.writeln('=========================================================='); | |
| message.writeln(); | |
| if (testNames.length > 1) { | |
| message.writeln("Errors found in tests:"); | |
| message.writeln(); | |
| for (String testName in testNames.toList()..sort()) { | |
| message.writeln(" ${testName}"); | |
| } | |
| } else { | |
| message.writeln("Errors found in test '${testNames.single}'"); | |
| } | |
| if (mismatches.isNotEmpty) { | |
| Map<MarkerTester, Set<String>> regenerations = {}; | |
| mismatches.forEach((String testName, Set<String> markers) { | |
| for (String marker in markers) { | |
| MarkerTester tester = markerOptions.markers[marker]!; | |
| (regenerations[tester] ??= {}).add(testName); | |
| } | |
| }); | |
| message.writeln(); | |
| if (regenerations.length > 1) { | |
| message.writeln('Run these commands to generate new expectations:'); | |
| } else { | |
| message.writeln('Run this command to generate new expectations:'); | |
| } | |
| message.writeln(); | |
| regenerations.forEach((MarkerTester tester, Set<String> testNames) { | |
| message.write(' dart ${tester.path} -g'); | |
| for (String testName in testNames) { | |
| message.write(' ${testName}'); | |
| } | |
| message.writeln(); | |
| }); | |
| message.writeln(); | |
| message.writeln(_dartLocationMessage); | |
| } | |
| if (errors.isNotEmpty) { | |
| Map<MarkerTester, Set<String>> erroneous = {}; | |
| errors.forEach((String testName, Set<String> markers) { | |
| for (String marker in markers) { | |
| MarkerTester tester = markerOptions.markers[marker]!; | |
| (erroneous[tester] ??= {}).add(testName); | |
| } | |
| }); | |
| message.writeln(); | |
| if (erroneous.length > 1) { | |
| message.writeln('Run these commands to re-run the failing tests:'); | |
| } else { | |
| message.writeln('Run this command to re-run the failing test(s):'); | |
| } | |
| message.writeln(); | |
| erroneous.forEach((MarkerTester tester, Set<String> testNames) { | |
| message.write(' dart ${tester.path}'); | |
| for (String testName in testNames) { | |
| message.write(' ${testName}'); | |
| } | |
| message.writeln(); | |
| }); | |
| message.writeln(_dartLocationMessage); | |
| } | |
| message.writeln(); | |
| message.writeln('=========================================================='); | |
| return message.toString(); | |
| } | |
| /// Returns `true` if [testName] is marked as skipped in [skipMap] for | |
| /// the given [configMarker]. | |
| bool skipForConfig( | |
| String testName, | |
| String configMarker, | |
| Map<String, List<String>>? skipMap, | |
| ) { | |
| if (skipMap != null) { | |
| List<String>? skipList = skipMap[configMarker]; | |
| if (skipList != null && skipList.contains(testName)) { | |
| print("Skip: ${testName} for config '${configMarker}'"); | |
| return true; | |
| } | |
| skipList = skipMap[null]; | |
| if (skipList != null && skipList.contains(testName)) { | |
| print("Skip: ${testName} for config '${configMarker}'"); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Updates all id tests in [relativeTestPaths]. | |
| /// | |
| /// This assumes that the current working directory is the repository root. | |
| Future<void> updateAllTests(List<String> relativeTestPaths) async { | |
| for (String testPath in relativeTestPaths) { | |
| List<String> arguments = []; | |
| if (Platform.packageConfig != null) { | |
| arguments.add('--packages=${Platform.packageConfig}'); | |
| } | |
| arguments.addAll([testPath, '-g', '--run-all']); | |
| print('Running: ${Platform.resolvedExecutable} ${arguments.join(' ')}'); | |
| Process process = await Process.start( | |
| Platform.resolvedExecutable, | |
| arguments, | |
| mode: ProcessStartMode.inheritStdio, | |
| ); | |
| await process.exitCode; | |
| } | |
| } |