blob: 046a77972841237d88df9fce3037b0006563ffae [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:convert';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'codecs.dart';
import 'common.dart';
import 'errors.dart';
import 'recording_file_system.dart';
import 'replay_proxy_mixin.dart';
/// A file system that replays invocations from a prior recording for use
/// in tests.
/// This will replay all invocations (methods, property getters, and property
/// setters) that occur on it, based on an opaque recording that was generated
/// in [RecordingFileSystem]. All activity in the [File], [Directory], [Link],
/// [IOSink], and [RandomAccessFile] instances returned from this API will also
/// be replayed from the same recording.
/// Once an invocation has been replayed once, it is marked as such and will
/// not be eligible for further replay. If an eligible invocation cannot be
/// found that matches an incoming invocation, a [NoMatchingInvocationError]
/// will be thrown.
/// This class is intended for use in tests, where you would otherwise have to
/// set up complex mocks or fake file systems. With this class, the process is
/// as follows:
/// - You record the file system activity during a real run of your program
/// by injecting a `RecordingFileSystem` that delegates to your real file
/// system.
/// - You serialize that recording to disk as your program finishes.
/// - You use that recording in tests to create a mock file system that knows
/// how to respond to the exact invocations your program makes. Any
/// invocations that aren't in the recording will throw, and you can make
/// assertions in your tests about which methods were invoked and in what
/// order.
/// *Implementation note*: this class uses [noSuchMethod] to dynamically handle
/// invocations. As a result, method references on objects herein will not pass
/// `is` checks or checked-mode checks on type. For example:
/// ```dart
/// typedef FileStat StatSync(String path);
/// FileSystem fs = new ReplayFileSystem(directory);
/// StatSync method = fs.statSync; // Will fail in checked-mode
/// fs.statSync is StatSync // Will return false
/// fs.statSync is Function // Will return false
/// dynamic method2 = fs.statSync; // OK
/// FileStat stat = method2('/path'); // OK
/// ```
/// See also:
/// - [RecordingFileSystem]
abstract class ReplayFileSystem extends FileSystem {
/// Creates a new `ReplayFileSystem`.
/// Recording data will be loaded from the specified [recording] location.
/// This location must have been created by [RecordingFileSystem], or an
/// [ArgumentError] will be thrown.
factory ReplayFileSystem({
@required Directory recording,
}) {
String dirname = recording.path;
String path = recording.fileSystem.path.join(dirname, kManifestName);
File manifestFile = recording.fileSystem.file(path);
if (!manifestFile.existsSync()) {
throw new ArgumentError('Not a valid recording directory: $dirname');
List<Map<String, dynamic>> manifest =
new JsonDecoder().convert(manifestFile.readAsStringSync());
return new ReplayFileSystemImpl(recording, manifest);
/// Non-exported implementation class for `ReplayFileSystem`.
class ReplayFileSystemImpl extends FileSystem
with ReplayProxyMixin
implements ReplayFileSystem, ReplayAware {
/// Creates a new `ReplayFileSystemImpl`.
ReplayFileSystemImpl(this.recording, this.manifest) {
Converter<String, Directory> reviveDirectory = new ReviveDirectory(this);
ToFuture<FileSystemEntityType> toFutureType =
const ToFuture<FileSystemEntityType>();
methods.addAll(<Symbol, Converter<dynamic, dynamic>>{
#directory: reviveDirectory,
#file: new ReviveFile(this),
#link: new ReviveLink(this),
#stat: FileStatCodec.deserialize.fuse(const ToFuture<FileStat>()),
#statSync: FileStatCodec.deserialize,
#identical: const Passthrough<bool>().fuse(const ToFuture<bool>()),
#identicalSync: const Passthrough<bool>(),
#type: EntityTypeCodec.deserialize.fuse(toFutureType),
#typeSync: EntityTypeCodec.deserialize,
properties.addAll(<Symbol, Converter<dynamic, dynamic>>{
#path: PathContextCodec.deserialize,
#systemTempDirectory: reviveDirectory,
#currentDirectory: reviveDirectory,
const Symbol('currentDirectory='): const Passthrough<Null>(),
#isWatchSupported: const Passthrough<bool>(),
/// The location of the recording that's driving this file system
final Directory recording;
String get identifier => kFileSystemEncodedValue;
final List<Map<String, dynamic>> manifest;