blob: 6c956b8cc1842662b79140ce59dbc0f40e0a4e8c [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 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'common.dart';
import 'mutable_recording.dart';
import 'recording.dart';
import 'recording_directory.dart';
import 'recording_file.dart';
import 'recording_link.dart';
import 'recording_proxy_mixin.dart';
import 'replay_file_system.dart';
/// File system that records invocations for later playback in tests.
///
/// This will record all invocations (methods, property getters, and property
/// setters) that occur on it, in an opaque format that can later be used in
/// [ReplayFileSystem]. All activity in the [File], [Directory], [Link],
/// [IOSink], and [RandomAccessFile] instances returned from this API will also
/// be recorded.
///
/// 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 RecordingFileSystem(delegate: delegate, destination: dir);
///
/// 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:
/// - [ReplayFileSystem]
abstract class RecordingFileSystem extends FileSystem {
/// Creates a new `RecordingFileSystem`.
///
/// Invocations will be recorded and forwarded to the specified [delegate]
/// file system.
///
/// The recording will be serialized to the specified [destination] directory
/// (only when `flush` is called on this file system's [recording]).
///
/// If [stopwatch] is specified, it will be assumed to have already been
/// started by the caller, and it will be used to record timestamps on each
/// recorded invocation. If `stopwatch` is unspecified (or `null`), a new
/// stopwatch will be created and started immediately to record these
/// timestamps.
factory RecordingFileSystem({
@required FileSystem delegate,
@required Directory destination,
Stopwatch stopwatch,
}) =>
new RecordingFileSystemImpl(delegate, destination, stopwatch);
/// The file system to which invocations will be forwarded upon recording.
FileSystem get delegate;
/// The recording generated by invocations on this file system.
///
/// The recording provides access to the invocation events that have been
/// recorded thus far, as well as the ability to flush them to disk.
LiveRecording get recording;
/// The stopwatch used to record timestamps on invocation events.
///
/// Timestamps will be recorded before the delegate is invoked (not after
/// the delegate returns).
Stopwatch get stopwatch;
}
/// Non-exported implementation class for `RecordingFileSystem`.
class RecordingFileSystemImpl extends FileSystem
with RecordingProxyMixin
implements RecordingFileSystem {
/// Creates a new `RecordingFileSystemImpl`.
RecordingFileSystemImpl(
this.delegate, Directory destination, Stopwatch recordingStopwatch)
: recording = new MutableRecording(destination),
stopwatch = recordingStopwatch ?? new Stopwatch() {
if (recordingStopwatch == null) {
// We instantiated our own stopwatch, so start it ourselves.
stopwatch.start();
}
methods.addAll(<Symbol, Function>{
#directory: _directory,
#file: _file,
#link: _link,
#stat: delegate.stat,
#statSync: delegate.statSync,
#identical: delegate.identical,
#identicalSync: delegate.identicalSync,
#type: delegate.type,
#typeSync: delegate.typeSync,
});
properties.addAll(<Symbol, Function>{
#path: () => delegate.path,
#systemTempDirectory: _getSystemTempDirectory,
#currentDirectory: _getCurrentDirectory,
const Symbol('currentDirectory='): _setCurrentDirectory,
#isWatchSupported: () => delegate.isWatchSupported,
});
}
@override
String get identifier => kFileSystemEncodedValue;
/// The file system to which invocations will be forwarded upon recording.
@override
final FileSystem delegate;
/// The recording generated by invocations on this file system.
@override
final MutableRecording recording;
/// The stopwatch used to record timestamps on invocation events.
@override
final Stopwatch stopwatch;
Directory _directory(dynamic path) =>
new RecordingDirectory(this, delegate.directory(path));
File _file(dynamic path) => new RecordingFile(this, delegate.file(path));
Link _link(dynamic path) => new RecordingLink(this, delegate.link(path));
Directory _getSystemTempDirectory() =>
new RecordingDirectory(this, delegate.systemTempDirectory);
Directory _getCurrentDirectory() =>
new RecordingDirectory(this, delegate.currentDirectory);
void _setCurrentDirectory(dynamic value) {
delegate.currentDirectory = value;
}
}