| // 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; |
| } |
| } |