| // Copyright 2014 The Flutter Authors. 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 'dart:math' as math; | |
| import 'package:collection/collection.dart'; | |
| import 'package:test/test.dart'; | |
| import '../utils.dart'; | |
| export 'package:test/test.dart' hide isInstanceOf; | |
| /// A matcher that compares the type of the actual value to the type argument T. | |
| TypeMatcher<T> isInstanceOf<T>() => isA<T>(); | |
| void tryToDelete(Directory directory) { | |
| // This should not be necessary, but it turns out that | |
| // on Windows it's common for deletions to fail due to | |
| // bogus (we think) "access denied" errors. | |
| try { | |
| directory.deleteSync(recursive: true); | |
| } on FileSystemException catch (error) { | |
| print('Failed to delete ${directory.path}: $error'); | |
| } | |
| } | |
| Matcher throwsExceptionWith(String messageSubString) { | |
| return throwsA( | |
| isA<Exception>().having( | |
| (Exception e) => e.toString(), | |
| 'description', | |
| contains(messageSubString), | |
| ), | |
| ); | |
| } | |
| /// A matcher that matches error messages specified in the given `fixture` [File]. | |
| /// | |
| /// This matcher allows analyzer tests to specify the expected error messages | |
| /// in the test fixture file, eliminating the need to hard code line numbers in | |
| /// the test. | |
| /// | |
| /// The error messages must be printed using the [foundError] function. Each | |
| /// error must start with the path to the file where the error resides, line | |
| /// number (1-based instead of 0-based) of the error, and a short description, | |
| /// delimited by colons (`:`). In the test fixture one could add the following | |
| /// comment on the line that would produce the error, to tell matcher what to | |
| /// expect: | |
| /// `// ERROR: <error message without the leading path and line number>`. | |
| Matcher matchesErrorsInFile(File fixture, {List<String> endsWith = const <Never>[]}) => | |
| _ErrorMatcher(fixture, endsWith); | |
| class _ErrorMatcher extends Matcher { | |
| _ErrorMatcher(this.file, this.endsWith) : bodyMatcher = _ErrorsInFileMatcher(file); | |
| static const String mismatchDescriptionKey = 'mismatchDescription'; | |
| static final int _errorBoxWidth = math.max(15, (hasColor ? stdout.terminalColumns : 80) - 1); | |
| static const String _title = 'ERROR #1'; | |
| static final String _firstLine = '╔═╡$_title╞═${"═" * (_errorBoxWidth - 4 - _title.length)}'; | |
| static final String _lastLine = '╚${"═" * _errorBoxWidth}'; | |
| static const String _linePrefix = '║ '; | |
| static bool mismatch(String mismatchDescription, Map<dynamic, dynamic> matchState) { | |
| matchState[mismatchDescriptionKey] = mismatchDescription; | |
| return false; | |
| } | |
| final List<String> endsWith; | |
| final File file; | |
| final _ErrorsInFileMatcher bodyMatcher; | |
| @override | |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { | |
| if (item is! String) { | |
| return mismatch('expected a String, got $item', matchState); | |
| } | |
| final List<String> lines = item.split('\n'); | |
| if (lines.isEmpty) { | |
| return mismatch('the actual error message is empty', matchState); | |
| } | |
| if (lines.first != _firstLine) { | |
| return mismatch( | |
| 'the first line of the error message must be $_firstLine, got ${lines.first}', | |
| matchState, | |
| ); | |
| } | |
| if (lines.last.isNotEmpty) { | |
| return mismatch( | |
| 'missing newline at the end of the error message, got ${lines.last}', | |
| matchState, | |
| ); | |
| } | |
| if (lines[lines.length - 2] != _lastLine) { | |
| return mismatch( | |
| 'the last line of the error message must be $_lastLine, got ${lines[lines.length - 2]}', | |
| matchState, | |
| ); | |
| } | |
| final List<String> body = lines.sublist(1, lines.length - 2); | |
| final String? noprefix = body.firstWhereOrNull((String line) => !line.startsWith(_linePrefix)); | |
| if (noprefix != null) { | |
| return mismatch( | |
| 'Line "$noprefix" should start with a prefix $_linePrefix..\n$lines', | |
| matchState, | |
| ); | |
| } | |
| final List<String> bodyWithoutPrefix = body | |
| .map((String s) => s.substring(_linePrefix.length)) | |
| .toList(growable: false); | |
| final bool hasTailMismatch = IterableZip<String>(<Iterable<String>>[ | |
| bodyWithoutPrefix.reversed, | |
| endsWith.reversed, | |
| ]).any((List<String> ss) => ss[0] != ss[1]); | |
| if (bodyWithoutPrefix.length < endsWith.length || hasTailMismatch) { | |
| return mismatch( | |
| 'The error message should end with $endsWith.\n' | |
| 'Actual error(s): $item', | |
| matchState, | |
| ); | |
| } | |
| return bodyMatcher.matches( | |
| bodyWithoutPrefix.sublist(0, bodyWithoutPrefix.length - endsWith.length), | |
| matchState, | |
| ); | |
| } | |
| @override | |
| Description describe(Description description) { | |
| return description.add('file ${file.path} contains the expected analyze errors.'); | |
| } | |
| @override | |
| Description describeMismatch( | |
| dynamic item, | |
| Description mismatchDescription, | |
| Map<dynamic, dynamic> matchState, | |
| bool verbose, | |
| ) { | |
| final description = matchState[mismatchDescriptionKey] as String?; | |
| return description != null | |
| ? mismatchDescription.add(description) | |
| : mismatchDescription.add('$matchState'); | |
| } | |
| } | |
| class _ErrorsInFileMatcher extends Matcher { | |
| _ErrorsInFileMatcher(this.file); | |
| final File file; | |
| static final RegExp expectationMatcher = RegExp(r'// ERROR: (?<expectations>.+)$'); | |
| static bool mismatch(String mismatchDescription, Map<dynamic, dynamic> matchState) { | |
| return _ErrorMatcher.mismatch(mismatchDescription, matchState); | |
| } | |
| List<(int, String)> _expectedErrorMessagesFromFile(Map<dynamic, dynamic> matchState) { | |
| final returnValue = <(int, String)>[]; | |
| for (final (int index, String line) in file.readAsLinesSync().indexed) { | |
| final List<String> expectations = | |
| expectationMatcher.firstMatch(line)?.namedGroup('expectations')?.split(' // ERROR: ') ?? | |
| <String>[]; | |
| for (final expectation in expectations) { | |
| returnValue.add((index + 1, expectation)); | |
| } | |
| } | |
| return returnValue; | |
| } | |
| @override | |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { | |
| final actualErrors = item as List<String>; | |
| final List<(int, String)> expectedErrors = _expectedErrorMessagesFromFile(matchState); | |
| if (expectedErrors.length != actualErrors.length) { | |
| return mismatch( | |
| 'expected ${expectedErrors.length} error(s), got ${actualErrors.length}.\n' | |
| 'expected lines with errors: ${expectedErrors.map(((int, String) x) => x.$1).toList()}\n' | |
| 'actual error(s): \n>${actualErrors.join('\n>')}', | |
| matchState, | |
| ); | |
| } | |
| for (var i = 0; i < actualErrors.length; ++i) { | |
| final String actualError = actualErrors[i]; | |
| final (int lineNumber, String expectedError) = expectedErrors[i]; | |
| switch (actualError.split(':')) { | |
| case [final String _]: | |
| return mismatch('No colons (":") found in the error message "$actualError".', matchState); | |
| case [final String path, final String line, ...final List<String> rest]: | |
| if (!path.endsWith(file.uri.pathSegments.last)) { | |
| return mismatch('"$path" does not match the file name of the source file.', matchState); | |
| } | |
| if (lineNumber.toString() != line) { | |
| return mismatch( | |
| 'could not find the expected error "$expectedError" at line $lineNumber', | |
| matchState, | |
| ); | |
| } | |
| final String actualMessage = rest.join(':').trimLeft(); | |
| if (actualMessage != expectedError) { | |
| return mismatch( | |
| 'expected \n"$expectedError"\n at line $lineNumber, got \n"$actualMessage"', | |
| matchState, | |
| ); | |
| } | |
| case _: | |
| return mismatch( | |
| 'failed to recognize a valid path from the error message "$actualError".', | |
| matchState, | |
| ); | |
| } | |
| } | |
| return true; | |
| } | |
| @override | |
| Description describe(Description description) => description; | |
| } |