blob: b9eae6e126a1749dc7aae43433cc73ec117673d4 [file] [log] [blame]
// 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, Object? 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<void> 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<void> 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.
FutureOr<void> _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<void> _validate(String prettPath, List<int> actualContents) async {
if (const IterableEquality().equals(_contents, actualContents)) return;
// 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
void _validate(String prettyPath, List<int> actualContents) {
var actualContentsText = utf8.decode(actualContents);
if (_contents == actualContentsText) return;
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<void> _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}');
}
}
}