Monotonic clock version of MemoryFileSystem (#129)

diff --git a/packages/file/lib/src/backends/memory/clock.dart b/packages/file/lib/src/backends/memory/clock.dart
new file mode 100644
index 0000000..12549aa
--- /dev/null
+++ b/packages/file/lib/src/backends/memory/clock.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2018, 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.
+
+/// Interface describing clocks used by the [MemoryFileSystem].
+///
+/// The [MemoryFileSystem] uses a clock to determine the modification times of
+/// files that are created in that file system.
+abstract class Clock {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const Clock();
+
+  /// A real-time clock.
+  ///
+  /// Uses [DateTime.now] to reflect the actual time reported by the operating
+  /// system.
+  const factory Clock.realTime() = _RealtimeClock;
+
+  /// A monotonically-increasing test clock.
+  ///
+  /// Each time [now] is called, the time increases by one minute.
+  ///
+  /// The `start` argument can be used to set the seed time for the clock.
+  /// The first value will be that time plus one minute.
+  /// By default, `start` is midnight on the first of January, 2000.
+  factory Clock.monotonicTest() = _MonotonicTestClock;
+
+  /// Returns the value of the clock.
+  DateTime get now;
+}
+
+class _RealtimeClock extends Clock {
+  const _RealtimeClock();
+
+  @override
+  DateTime get now => DateTime.now();
+}
+
+class _MonotonicTestClock extends Clock {
+  _MonotonicTestClock({
+    DateTime start,
+  }) : _current = start ?? DateTime(2000);
+
+  DateTime _current;
+
+  @override
+  DateTime get now {
+    _current = _current.add(const Duration(minutes: 1));
+    return _current;
+  }
+}
diff --git a/packages/file/lib/src/backends/memory/memory_file_system.dart b/packages/file/lib/src/backends/memory/memory_file_system.dart
index e5e09b8..38f818e 100644
--- a/packages/file/lib/src/backends/memory/memory_file_system.dart
+++ b/packages/file/lib/src/backends/memory/memory_file_system.dart
@@ -6,8 +6,10 @@
 
 import 'package:file/file.dart';
 import 'package:file/src/io.dart' as io;
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 
+import 'clock.dart';
 import 'common.dart';
 import 'memory_directory.dart';
 import 'memory_file.dart';
@@ -35,16 +37,46 @@
   /// The file system will be empty, and the current directory will be the
   /// root directory.
   ///
+  /// The clock will be a real-time clock; file modification times will
+  /// reflect the real time as reported by the operating system.
+  ///
   /// If [style] is specified, the file system will use the specified path
   /// style. The default is [FileSystemStyle.posix].
-  factory MemoryFileSystem({FileSystemStyle style}) = _MemoryFileSystem;
+  factory MemoryFileSystem({
+    FileSystemStyle style = FileSystemStyle.posix,
+  }) =>
+      _MemoryFileSystem(
+        style: style,
+        clock: const Clock.realTime(),
+      );
+
+  /// Creates a new `MemoryFileSystem` that has a fake clock.
+  ///
+  /// The file system will be empty, and the current directory will be the
+  /// root directory.
+  ///
+  /// The clock will increase monotonically each time it is used, disconnected
+  /// from any real-world clock.
+  ///
+  /// If [style] is specified, the file system will use the specified path
+  /// style. The default is [FileSystemStyle.posix].
+  factory MemoryFileSystem.test({
+    FileSystemStyle style = FileSystemStyle.posix,
+  }) =>
+      _MemoryFileSystem(
+        style: style,
+        clock: Clock.monotonicTest(),
+      );
 }
 
 /// Internal implementation of [MemoryFileSystem].
 class _MemoryFileSystem extends FileSystem
     implements MemoryFileSystem, NodeBasedFileSystem {
-  _MemoryFileSystem({this.style = FileSystemStyle.posix})
-      : assert(style != null) {
+  _MemoryFileSystem({
+    this.style = FileSystemStyle.posix,
+    @required this.clock,
+  })  : assert(style != null),
+        assert(clock != null) {
     _root = RootNode(this);
     _context = style.contextFor(style.root);
   }
@@ -54,6 +86,9 @@
   p.Context _context;
 
   @override
+  final Clock clock;
+
+  @override
   final FileSystemStyle style;
 
   @override
diff --git a/packages/file/lib/src/backends/memory/node.dart b/packages/file/lib/src/backends/memory/node.dart
index e053aec..68c4cc9 100644
--- a/packages/file/lib/src/backends/memory/node.dart
+++ b/packages/file/lib/src/backends/memory/node.dart
@@ -7,6 +7,7 @@
 import 'package:file/file.dart';
 import 'package:file/src/io.dart' as io;
 
+import 'clock.dart';
 import 'common.dart';
 import 'memory_file_stat.dart';
 import 'style.dart';
@@ -46,6 +47,10 @@
   /// The path of the current working directory.
   String get cwd;
 
+  /// The clock to use when finding the current time (e.g. to set the creation
+  /// time of a new node).
+  Clock get clock;
+
   /// Gets the backing node of the entity at the specified path. If the tail
   /// element of the path does not exist, this will return null. If the tail
   /// element cannot be reached because its directory does not exist, a
@@ -142,12 +147,14 @@
 abstract class RealNode extends Node {
   /// Constructs a new [RealNode] as a child of the specified [parent].
   RealNode(DirectoryNode parent) : super(parent) {
-    int now = DateTime.now().millisecondsSinceEpoch;
+    int now = clock.now.millisecondsSinceEpoch;
     changed = now;
     modified = now;
     accessed = now;
   }
 
+  Clock get clock => parent.clock;
+
   /// Last changed time in milliseconds since the Epoch.
   int changed;
 
@@ -177,7 +184,7 @@
 
   /// Updates the last modified time of the node.
   void touch() {
-    modified = DateTime.now().millisecondsSinceEpoch;
+    modified = clock.now.millisecondsSinceEpoch;
   }
 }
 
@@ -211,6 +218,9 @@
   final NodeBasedFileSystem fs;
 
   @override
+  Clock get clock => fs.clock;
+
+  @override
   DirectoryNode get parent => this;
 
   @override
@@ -253,7 +263,7 @@
   /// fields will be reset as opposed to copied to indicate that this file
   /// has been modified and changed.
   void copyFrom(FileNode source) {
-    modified = changed = DateTime.now().millisecondsSinceEpoch;
+    modified = changed = clock.now.millisecondsSinceEpoch;
     accessed = source.accessed;
     mode = source.mode;
     _content = Uint8List.fromList(source.content);
diff --git a/packages/file/test/memory_test.dart b/packages/file/test/memory_test.dart
index ed2c9b2..dfa76bf 100644
--- a/packages/file/test/memory_test.dart
+++ b/packages/file/test/memory_test.dart
@@ -67,4 +67,33 @@
       });
     });
   });
+
+  test('MemoryFileSystem.test', () {
+    final MemoryFileSystem fs =
+        MemoryFileSystem.test(); // creates root directory
+    fs.file('/test1.txt').createSync(); // creates file
+    fs.file('/test2.txt').createSync(); // creates file
+    expect(fs.directory('/').statSync().modified, DateTime(2000, 1, 1, 0, 1));
+    expect(
+        fs.file('/test1.txt').statSync().modified, DateTime(2000, 1, 1, 0, 2));
+    expect(
+        fs.file('/test2.txt').statSync().modified, DateTime(2000, 1, 1, 0, 3));
+    fs.file('/test1.txt').createSync();
+    fs.file('/test2.txt').createSync();
+    expect(fs.file('/test1.txt').statSync().modified,
+        DateTime(2000, 1, 1, 0, 2)); // file already existed
+    expect(fs.file('/test2.txt').statSync().modified,
+        DateTime(2000, 1, 1, 0, 3)); // file already existed
+    fs.file('/test1.txt').writeAsStringSync('test'); // touches file
+    expect(
+        fs.file('/test1.txt').statSync().modified, DateTime(2000, 1, 1, 0, 4));
+    expect(fs.file('/test2.txt').statSync().modified,
+        DateTime(2000, 1, 1, 0, 3)); // didn't touch it
+    fs.file('/test1.txt').copySync(
+        '/test2.txt'); // creates file, then mutates file (so time changes twice)
+    expect(fs.file('/test1.txt').statSync().modified,
+        DateTime(2000, 1, 1, 0, 4)); // didn't touch it
+    expect(
+        fs.file('/test2.txt').statSync().modified, DateTime(2000, 1, 1, 0, 6));
+  });
 }