Run common tests for ReplayFileSystem. (#31)

diff --git a/test/common_tests.dart b/test/common_tests.dart
index c3c2abf..21312e3 100644
--- a/test/common_tests.dart
+++ b/test/common_tests.dart
@@ -10,12 +10,20 @@
 import 'package:file/file.dart';
 import 'package:file/testing.dart';
 import 'package:test/test.dart';
-import 'package:test/test.dart' as testpkg show group, test;
+import 'package:test/test.dart' as testpkg show group, test, setUp;
 
 /// Callback used in [runCommonTests] to produce the root folder in which all
 /// file system entities will be created.
 typedef String RootPathGenerator();
 
+/// Callback used in [runCommonTests] to create the file system under test.
+/// It must return either a [FileSystem] or a [Future] that completes with a
+/// [FileSystem].
+typedef dynamic FileSystemGenerator();
+
+/// A function to run before tests (passed to [setUp]).
+typedef dynamic SetUpCallback();
+
 /// Runs a suite of tests common to all file system implementations. All file
 /// system implementations should run *at least* these tests to ensure
 /// compliance with file system API.
@@ -29,14 +37,24 @@
 /// not yet fully complete). The format of each entry in the list is:
 /// `$group1Description > $group2Description > ... > $testDescription`.
 /// Entries may use regular expression syntax.
+///
+/// If [replay] is specified, each test (and its setup callbacks) will run
+/// twice - once as a "setup" pass with the file system returned by
+/// [createFileSystem], and again as the "test" pass with the file system
+/// returned by [replay]. This is intended for use with `ReplayFileSystem`,
+/// where in order for the file system to behave as expected, a recording of
+/// the invocation(s) must first be made.
 void runCommonTests(
-  FileSystem createFileSystem(), {
+  FileSystemGenerator createFileSystem, {
   RootPathGenerator root,
   List<String> skip: const <String>[],
+  FileSystemGenerator replay,
 }) {
   RootPathGenerator rootfn = root;
 
   group('common', () {
+    FileSystemGenerator createFs;
+    List<SetUpCallback> setUps;
     FileSystem fs;
     String root;
 
@@ -52,11 +70,36 @@
       stack.removeLast();
     }
 
+    testpkg.setUp(() async {
+      createFs = createFileSystem;
+      setUps = <SetUpCallback>[];
+      fs = null;
+      root = null;
+    });
+
+    void setUp(callback()) {
+      testpkg.setUp(replay == null ? callback : () => setUps.add(callback));
+    }
+
     void group(String description, body()) =>
         skipIfNecessary(description, () => testpkg.group(description, body));
 
-    void test(String description, body()) =>
-        skipIfNecessary(description, () => testpkg.test(description, body));
+    void test(String description, body()) => skipIfNecessary(description, () {
+          if (replay == null) {
+            testpkg.test(description, body);
+          } else {
+            group('rerun', () {
+              testpkg.setUp(() async {
+                await Future.forEach(setUps, (SetUpCallback setUp) => setUp());
+                await body();
+                createFs = replay;
+                await Future.forEach(setUps, (SetUpCallback setUp) => setUp());
+              });
+
+              testpkg.test(description, body);
+            });
+          }
+        });
 
     /// Returns [path] prefixed by the [root] namespace.
     /// This is only intended for absolute paths.
@@ -68,10 +111,10 @@
       return root == '/' ? path : (path == '/' ? root : '$root$path');
     }
 
-    setUp(() {
+    setUp(() async {
       root = rootfn != null ? rootfn() : '/';
       assert(root.startsWith('/') && (root == '/' || !root.endsWith('/')));
-      fs = createFileSystem();
+      fs = await createFs();
     });
 
     group('FileSystem', () {
@@ -588,7 +631,7 @@
           expect(fs.link(ns('/baz')).targetSync(), ns('/foo'));
         });
 
-        test('succeedsIfDestinationIsLinkToNotFound', () {
+        test('throwsIfDestinationIsLinkToNotFound', () {
           Directory src = fs.directory(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/baz'));
           expectFileSystemException('Not a directory', () {
diff --git a/test/replay_test.dart b/test/replay_test.dart
index 517c553..65d317b 100644
--- a/test/replay_test.dart
+++ b/test/replay_test.dart
@@ -11,6 +11,8 @@
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
 
+import 'common_tests.dart';
+
 void main() {
   group('Replay', () {
     RecordingFileSystem recordingFileSystem;
@@ -33,6 +35,21 @@
       return new ReplayFileSystem(recording: recording.destination);
     }
 
+    runCommonTests(
+      () => recordingFileSystem,
+      replay: replay,
+      skip: <String>[
+        // ReplayFileSystem does not yet replay exceptions
+        '.*(disallows|throws).*',
+
+        // TODO(tvolkert): Enable when ReplayFileSystem is complete.
+        'FileSystem',
+        'Directory',
+        'File',
+        'Link',
+      ],
+    );
+
     group('ReplayFileSystem', () {
       test('directory', () async {
         recordingFileSystem.directory('/foo');