| // Copyright (c) 2017, 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'; |
| import 'dart:math' as math; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:term_glyph/term_glyph.dart' as glyph; |
| import 'package:test/test.dart'; |
| |
| import 'descriptor.dart'; |
| import 'sandbox.dart'; |
| import 'utils.dart'; |
| |
| /// A descriptor describing a single file. |
| /// |
| /// In addition to the normal descriptor methods, this has [read] and |
| /// [readAsBytes] methods that allows its contents to be read. |
| /// |
| /// This may be extended outside this package. |
| abstract class FileDescriptor extends Descriptor { |
| /// Creates a new [FileDescriptor] with [name] and [contents]. |
| /// |
| /// The [contents] may be a `String`, a `List<int>`, or a [Matcher]. If it's a |
| /// string, [create] creates a UTF-8 file and [validate] parses the physical |
| /// file as UTF-8. If it's a [Matcher], [validate] matches it against the |
| /// physical file's contents parsed as UTF-8, and [create], [read], and |
| /// [readAsBytes] are unsupported. |
| /// |
| /// If [contents] isn't passed, [create] creates an empty file and [validate] |
| /// verifies that the file is empty. |
| /// |
| /// To match a [Matcher] against a file's binary contents, use [new |
| /// FileDescriptor.binaryMatcher] instead. |
| factory FileDescriptor(String name, contents) { |
| if (contents is String) return _StringFileDescriptor(name, contents); |
| if (contents is List) { |
| return _BinaryFileDescriptor(name, contents.cast<int>()); |
| } |
| if (contents == null) return _BinaryFileDescriptor(name, []); |
| return _MatcherFileDescriptor(name, contents as Matcher); |
| } |
| |
| /// Returns a `dart:io` [File] object that refers to this file within |
| /// [sandbox]. |
| File get io => File(p.join(sandbox, name)); |
| |
| /// Creates a new binary [FileDescriptor] with [name] that matches its binary |
| /// contents against [matcher]. |
| /// |
| /// The [create], [read], and [readAsBytes] methods are unsupported for this |
| /// descriptor. |
| factory FileDescriptor.binaryMatcher(String name, Matcher matcher) => |
| _MatcherFileDescriptor(name, matcher, isBinary: true); |
| |
| /// A protected constructor that's only intended for subclasses. |
| FileDescriptor.protected(String name) : super(name); |
| |
| @override |
| Future create([String parent]) async { |
| // Create the stream before we call [File.openWrite] because it may fail |
| // fast (e.g. if this is a matcher file). |
| var file = File(p.join(parent ?? sandbox, name)).openWrite(); |
| try { |
| await readAsBytes().listen(file.add).asFuture(); |
| } finally { |
| await file.close(); |
| } |
| } |
| |
| @override |
| Future validate([String parent]) async { |
| var fullPath = p.join(parent ?? sandbox, name); |
| var pretty = prettyPath(fullPath); |
| if (!(await File(fullPath).exists())) { |
| fail('File not found: "$pretty".'); |
| } |
| |
| await _validate(pretty, await File(fullPath).readAsBytes()); |
| } |
| |
| /// Validates that [binaryContents] matches the expected contents of |
| /// the descriptor. |
| /// |
| /// The [prettyPath] is a human-friendly representation of the path to the |
| /// descriptor. |
| Future _validate(String prettyPath, List<int> binaryContents); |
| |
| /// Reads and decodes the contents of this descriptor as a UTF-8 string. |
| /// |
| /// This isn't supported for matcher descriptors. |
| Future<String> read() => utf8.decodeStream(readAsBytes()); |
| |
| /// Reads the contents of this descriptor as a byte stream. |
| /// |
| /// This isn't supported for matcher descriptors. |
| Stream<List<int>> readAsBytes(); |
| |
| @override |
| String describe() => name; |
| } |
| |
| class _BinaryFileDescriptor extends FileDescriptor { |
| /// The contents of this descriptor's file. |
| final List<int> _contents; |
| |
| _BinaryFileDescriptor(String name, this._contents) : super.protected(name); |
| |
| @override |
| Stream<List<int>> readAsBytes() => Stream.fromIterable([_contents]); |
| |
| @override |
| Future _validate(String prettPath, List<int> actualContents) async { |
| if (const IterableEquality().equals(_contents, actualContents)) return null; |
| // TODO(nweiz): show a hex dump here if the data is small enough. |
| fail('File "$prettPath" didn\'t contain the expected binary data.'); |
| } |
| } |
| |
| class _StringFileDescriptor extends FileDescriptor { |
| /// The contents of this descriptor's file. |
| final String _contents; |
| |
| _StringFileDescriptor(String name, this._contents) : super.protected(name); |
| |
| @override |
| Future<String> read() async => _contents; |
| |
| @override |
| Stream<List<int>> readAsBytes() => |
| Stream.fromIterable([utf8.encode(_contents)]); |
| |
| @override |
| Future _validate(String prettyPath, List<int> actualContents) { |
| var actualContentsText = utf8.decode(actualContents); |
| if (_contents == actualContentsText) return null; |
| fail(_textMismatchMessage(prettyPath, _contents, actualContentsText)); |
| } |
| |
| String _textMismatchMessage( |
| String prettyPath, String expected, String actual) { |
| final expectedLines = expected.split('\n'); |
| final actualLines = actual.split('\n'); |
| |
| var results = []; |
| |
| // Compare them line by line to see which ones match. |
| var length = math.max(expectedLines.length, actualLines.length); |
| for (var i = 0; i < length; i++) { |
| if (i >= actualLines.length) { |
| // Missing output. |
| results.add('? ${expectedLines[i]}'); |
| } else if (i >= expectedLines.length) { |
| // Unexpected extra output. |
| results.add('X ${actualLines[i]}'); |
| } else { |
| var expectedLine = expectedLines[i]; |
| var actualLine = actualLines[i]; |
| |
| if (expectedLine != actualLine) { |
| // Mismatched lines. |
| results.add('X $actualLine'); |
| } else { |
| // Matched lines. |
| results.add('${glyph.verticalLine} $actualLine'); |
| } |
| } |
| } |
| |
| return 'File "$prettyPath" should contain:\n' |
| '${addBar(expected)}\n' |
| 'but actually contained:\n' |
| "${results.join('\n')}"; |
| } |
| } |
| |
| class _MatcherFileDescriptor extends FileDescriptor { |
| /// The matcher for this descriptor's contents. |
| final Matcher _matcher; |
| |
| /// Whether [_matcher] should match against the file's string or byte |
| /// contents. |
| final bool _isBinary; |
| |
| _MatcherFileDescriptor(String name, this._matcher, {bool isBinary = false}) |
| : _isBinary = isBinary, |
| super.protected(name); |
| |
| @override |
| Stream<List<int>> readAsBytes() => |
| throw UnsupportedError("Matcher files can't be created or read."); |
| |
| @override |
| Future _validate(String prettyPath, List<int> actualContents) async { |
| try { |
| expect( |
| _isBinary ? actualContents : utf8.decode(actualContents), _matcher); |
| } on TestFailure catch (error) { |
| fail('Invalid contents for file "$prettyPath":\n${error.message}'); |
| } |
| } |
| } |