Update API per new dart:io methods (#39)

diff --git a/.analysis_options b/.analysis_options
index b0163e6..bf6018c 100644
--- a/.analysis_options
+++ b/.analysis_options
@@ -4,6 +4,8 @@
     enableStrictCallChecks: true
     enableSuperMixins: true
   errors:
+    # treat missing required parameters as a warning (not a hint)
+    missing_required_param: warning
     # Allow having TODOs in the code
     todo: ignore
 
@@ -60,7 +62,7 @@
     - super_goes_last
     - type_annotate_public_apis
     - type_init_formals
-    - unawaited_futures
+    # TODO - unawaited_futures (https://github.com/dart-lang/linter/issues/419)
     - unnecessary_brace_in_string_interp
     - unnecessary_getters_setters
 
diff --git a/lib/src/backends/chroot/chroot_file.dart b/lib/src/backends/chroot/chroot_file.dart
index 244b7fa..74d8cc9 100644
--- a/lib/src/backends/chroot/chroot_file.dart
+++ b/lib/src/backends/chroot/chroot_file.dart
@@ -209,6 +209,22 @@
   int lengthSync() => getDelegate(followLinks: true).lengthSync();
 
   @override
+  Future<DateTime> lastAccessed() =>
+      getDelegate(followLinks: true).lastAccessed();
+
+  @override
+  DateTime lastAccessedSync() =>
+      getDelegate(followLinks: true).lastAccessedSync();
+
+  @override
+  Future<dynamic> setLastAccessed(DateTime time) =>
+      getDelegate(followLinks: true).setLastAccessed(time);
+
+  @override
+  void setLastAccessedSync(DateTime time) =>
+      getDelegate(followLinks: true).setLastAccessedSync(time);
+
+  @override
   Future<DateTime> lastModified() =>
       getDelegate(followLinks: true).lastModified();
 
@@ -217,6 +233,14 @@
       getDelegate(followLinks: true).lastModifiedSync();
 
   @override
+  Future<dynamic> setLastModified(DateTime time) =>
+      getDelegate(followLinks: true).setLastModified(time);
+
+  @override
+  void setLastModifiedSync(DateTime time) =>
+      getDelegate(followLinks: true).setLastModifiedSync(time);
+
+  @override
   Future<RandomAccessFile> open({
     FileMode mode: FileMode.READ,
   }) async =>
diff --git a/lib/src/backends/memory/memory_file.dart b/lib/src/backends/memory/memory_file.dart
index 66e5abc..ded6bfa 100644
--- a/lib/src/backends/memory/memory_file.dart
+++ b/lib/src/backends/memory/memory_file.dart
@@ -122,12 +122,38 @@
   File get absolute => super.absolute;
 
   @override
+  Future<DateTime> lastAccessed() async => lastAccessedSync();
+
+  @override
+  DateTime lastAccessedSync() => (_resolvedBacking as _FileNode).stat.accessed;
+
+  @override
+  Future<dynamic> setLastAccessed(DateTime time) async =>
+      setLastAccessedSync(time);
+
+  @override
+  void setLastAccessedSync(DateTime time) {
+    _FileNode node = _resolvedBacking;
+    node.accessed = time.millisecondsSinceEpoch;
+  }
+
+  @override
   Future<DateTime> lastModified() async => lastModifiedSync();
 
   @override
   DateTime lastModifiedSync() => (_resolvedBacking as _FileNode).stat.modified;
 
   @override
+  Future<dynamic> setLastModified(DateTime time) async =>
+      setLastModifiedSync(time);
+
+  @override
+  void setLastModifiedSync(DateTime time) {
+    _FileNode node = _resolvedBacking;
+    node.modified = time.millisecondsSinceEpoch;
+  }
+
+  @override
   Future<io.RandomAccessFile> open(
           {io.FileMode mode: io.FileMode.READ}) async =>
       openSync(mode: mode);
diff --git a/lib/src/backends/record_replay/recording_file.dart b/lib/src/backends/record_replay/recording_file.dart
index 1453b7e..4a9ea43 100644
--- a/lib/src/backends/record_replay/recording_file.dart
+++ b/lib/src/backends/record_replay/recording_file.dart
@@ -43,8 +43,14 @@
       #copySync: _copySync,
       #length: delegate.length,
       #lengthSync: delegate.lengthSync,
+      #lastAccessed: delegate.lastAccessed,
+      #lastAccessedSync: delegate.lastAccessedSync,
+      #setLastAccessed: delegate.setLastAccessed,
+      #setLastAccessedSync: delegate.setLastAccessedSync,
       #lastModified: delegate.lastModified,
       #lastModifiedSync: delegate.lastModifiedSync,
+      #setLastModified: delegate.setLastModified,
+      #setLastModifiedSync: delegate.setLastModifiedSync,
       #open: _open,
       #openSync: _openSync,
       #openRead: _openRead,
diff --git a/lib/src/backends/record_replay/replay_file.dart b/lib/src/backends/record_replay/replay_file.dart
index 1995f31..9b26a2c 100644
--- a/lib/src/backends/record_replay/replay_file.dart
+++ b/lib/src/backends/record_replay/replay_file.dart
@@ -42,8 +42,14 @@
       #copySync: reviveFile,
       #length: const ToFuture<int>(),
       #lengthSync: const Passthrough<int>(),
+      #lastAccessed: DateTimeCodec.deserialize.fuse(const ToFuture<DateTime>()),
+      #lastAccessedSync: DateTimeCodec.deserialize,
+      #setLastAccessed: const ToFuture<dynamic>(),
+      #setLastAccessedSync: const Passthrough<Null>(),
       #lastModified: DateTimeCodec.deserialize.fuse(const ToFuture<DateTime>()),
       #lastModifiedSync: DateTimeCodec.deserialize,
+      #setLastModified: const ToFuture<dynamic>(),
+      #setLastModifiedSync: const Passthrough<Null>(),
       #open: reviveRandomAccessFile.fuse(const ToFuture<RandomAccessFile>()),
       #openSync: reviveRandomAccessFile,
       #openRead: blobToByteStream,
diff --git a/lib/src/forwarding/forwarding_file.dart b/lib/src/forwarding/forwarding_file.dart
index 500642d..19dc3fb 100644
--- a/lib/src/forwarding/forwarding_file.dart
+++ b/lib/src/forwarding/forwarding_file.dart
@@ -31,12 +31,32 @@
   int lengthSync() => delegate.lengthSync();
 
   @override
+  Future<DateTime> lastAccessed() => delegate.lastAccessed();
+
+  @override
+  DateTime lastAccessedSync() => delegate.lastAccessedSync();
+
+  @override
+  Future<dynamic> setLastAccessed(DateTime time) =>
+      delegate.setLastAccessed(time);
+
+  @override
+  void setLastAccessedSync(DateTime time) => delegate.setLastAccessedSync(time);
+
+  @override
   Future<DateTime> lastModified() => delegate.lastModified();
 
   @override
   DateTime lastModifiedSync() => delegate.lastModifiedSync();
 
   @override
+  Future<dynamic> setLastModified(DateTime time) =>
+      delegate.setLastModified(time);
+
+  @override
+  void setLastModifiedSync(DateTime time) => delegate.setLastModifiedSync(time);
+
+  @override
   Future<RandomAccessFile> open({
     FileMode mode: FileMode.READ,
   }) async =>
diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart
index 46777aa..78bb899 100644
--- a/lib/src/io/interface.dart
+++ b/lib/src/io/interface.dart
@@ -67,12 +67,30 @@
   File get absolute;
 
   // ignore: public_member_api_docs
+  Future<DateTime> lastAccessed();
+
+  // ignore: public_member_api_docs
+  DateTime lastAccessedSync();
+
+  // ignore: public_member_api_docs
+  Future<dynamic> setLastAccessed(DateTime time);
+
+  // ignore: public_member_api_docs
+  void setLastAccessedSync(DateTime time);
+
+  // ignore: public_member_api_docs
   Future<DateTime> lastModified();
 
   // ignore: public_member_api_docs
   DateTime lastModifiedSync();
 
   // ignore: public_member_api_docs
+  Future<dynamic> setLastModified(DateTime time);
+
+  // ignore: public_member_api_docs
+  void setLastModifiedSync(DateTime time);
+
+  // ignore: public_member_api_docs
   Future<RandomAccessFile> open({FileMode mode: FileMode.READ});
 
   // ignore: public_member_api_docs
diff --git a/test/chroot_test.dart b/test/chroot_test.dart
index 995d08b..f9e0d62 100644
--- a/test/chroot_test.dart
+++ b/test/chroot_test.dart
@@ -48,6 +48,11 @@
       runCommonTests(
         () => fs,
         skip: <String>[
+          // API doesn't exit in dart:io until Dart 1.23
+          'File > lastAccessed',
+          'File > setLastAccessed',
+          'File > setLastModified',
+
           // https://github.com/dart-lang/sdk/issues/28170
           'File > create > throwsIfAlreadyExistsAsDirectory',
           'File > create > throwsIfAlreadyExistsAsLinkToDirectory',
diff --git a/test/common_tests.dart b/test/common_tests.dart
index 630af33..867ba95 100644
--- a/test/common_tests.dart
+++ b/test/common_tests.dart
@@ -12,6 +12,8 @@
 import 'package:test/test.dart';
 import 'package:test/test.dart' as testpkg show group, setUp, tearDown, test;
 
+import 'utils.dart';
+
 /// Callback used in [runCommonTests] to produce the root folder in which all
 /// file system entities will be created.
 typedef String RootPathGenerator();
@@ -1483,11 +1485,78 @@
         });
       });
 
+      group('lastAccessed', () {
+        test('isNowForNewlyCreatedFile', () {
+          DateTime before = floor();
+          File f = fs.file(ns('/foo'))..createSync();
+          DateTime after = ceil();
+          DateTime accessed = f.lastAccessedSync();
+          expect(before, isSameOrBefore(accessed));
+          expect(after, isSameOrAfter(accessed));
+        });
+
+        test('throwsIfDoesntExist', () {
+          expectFileSystemException('No such file or directory', () {
+            fs.file(ns('/foo')).lastAccessedSync();
+          });
+        });
+
+        test('throwsIfExistsAsDirectory', () {
+          fs.directory(ns('/foo')).createSync();
+          expectFileSystemException('Is a directory', () {
+            fs.file(ns('/foo')).lastAccessedSync();
+          });
+        });
+
+        test('succeedsIfExistsAsLinkToFile', () {
+          DateTime before = floor();
+          fs.file(ns('/foo')).createSync();
+          fs.link(ns('/bar')).createSync(ns('/foo'));
+          DateTime after = ceil();
+          DateTime accessed = fs.file(ns('/bar')).lastAccessedSync();
+          expect(before, isSameOrBefore(accessed));
+          expect(after, isSameOrAfter(accessed));
+        });
+      });
+
+      group('setLastAccessed', () {
+        final DateTime time = new DateTime(1999);
+
+        test('throwsIfDoesntExist', () {
+          expectFileSystemException('No such file or directory', () {
+            fs.file(ns('/foo')).setLastAccessedSync(time);
+          });
+        });
+
+        test('throwsIfExistsAsDirectory', () {
+          fs.directory(ns('/foo')).createSync();
+          expectFileSystemException('Is a directory', () {
+            fs.file(ns('/foo')).setLastAccessedSync(time);
+          });
+        });
+
+        test('succeedsIfExistsAsFile', () {
+          File f = fs.file(ns('/foo'))..createSync();
+          f.setLastAccessedSync(time);
+          expect(fs.file(ns('/foo')).lastAccessedSync(), time);
+        });
+
+        test('succeedsIfExistsAsLinkToFile', () {
+          File f = fs.file(ns('/foo'))..createSync();
+          fs.link(ns('/bar')).createSync(ns('/foo'));
+          f.setLastAccessedSync(time);
+          expect(fs.file(ns('/bar')).lastAccessedSync(), time);
+        });
+      });
+
       group('lastModified', () {
         test('isNowForNewlyCreatedFile', () {
+          DateTime before = floor();
           File f = fs.file(ns('/foo'))..createSync();
-          expect(new DateTime.now().difference(f.lastModifiedSync()).abs(),
-              lessThan(new Duration(seconds: 2)));
+          DateTime after = ceil();
+          DateTime modified = f.lastModifiedSync();
+          expect(before, isSameOrBefore(modified));
+          expect(after, isSameOrAfter(modified));
         });
 
         test('throwsIfDoesntExist', () {
@@ -1504,14 +1573,43 @@
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
+          DateTime before = floor();
           fs.file(ns('/foo')).createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          expect(
-            new DateTime.now()
-                .difference(fs.file(ns('/bar')).lastModifiedSync())
-                .abs(),
-            lessThan(new Duration(seconds: 2)),
-          );
+          DateTime after = ceil();
+          DateTime modified = fs.file(ns('/bar')).lastModifiedSync();
+          expect(before, isSameOrBefore(modified));
+          expect(after, isSameOrAfter(modified));
+        });
+      });
+
+      group('setLastModified', () {
+        final DateTime time = new DateTime(1999);
+
+        test('throwsIfDoesntExist', () {
+          expectFileSystemException('No such file or directory', () {
+            fs.file(ns('/foo')).setLastModifiedSync(time);
+          });
+        });
+
+        test('throwsIfExistsAsDirectory', () {
+          fs.directory(ns('/foo')).createSync();
+          expectFileSystemException('Is a directory', () {
+            fs.file(ns('/foo')).setLastModifiedSync(time);
+          });
+        });
+
+        test('succeedsIfExistsAsFile', () {
+          File f = fs.file(ns('/foo'))..createSync();
+          f.setLastModifiedSync(time);
+          expect(fs.file(ns('/foo')).lastModifiedSync(), time);
+        });
+
+        test('succeedsIfExistsAsLinkToFile', () {
+          File f = fs.file(ns('/foo'))..createSync();
+          fs.link(ns('/bar')).createSync(ns('/foo'));
+          f.setLastModifiedSync(time);
+          expect(fs.file(ns('/bar')).lastModifiedSync(), time);
         });
       });
 
diff --git a/test/local_test.dart b/test/local_test.dart
index 0e893ab..028b4e5 100644
--- a/test/local_test.dart
+++ b/test/local_test.dart
@@ -33,6 +33,11 @@
       () => fs,
       root: () => tmp.path,
       skip: <String>[
+        // API doesn't exit in dart:io until Dart 1.23
+        'File > lastAccessed',
+        'File > setLastAccessed',
+        'File > setLastModified',
+
         // https://github.com/dart-lang/sdk/issues/28170
         'File > create > throwsIfAlreadyExistsAsDirectory',
         'File > create > throwsIfAlreadyExistsAsLinkToDirectory',
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..40e7b3b
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,95 @@
+// 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:meta/meta.dart';
+import 'package:test/test.dart';
+
+/// Returns a [DateTime] with an exact second-precision by removing the
+/// milliseconds and microseconds from the specified [time].
+///
+/// If [time] is not specified, it will default to the current time.
+DateTime floor([DateTime time]) {
+  time ??= new DateTime.now();
+  return time.subtract(new Duration(
+    milliseconds: time.millisecond,
+    microseconds: time.microsecond,
+  ));
+}
+
+/// Returns a [DateTime] with an exact second precision by adding just enough
+/// milliseconds and microseconds to the specified [time] to reach the next
+/// second.
+///
+/// If [time] is not specified, it will default to the current time.
+DateTime ceil([DateTime time]) {
+  time ??= new DateTime.now();
+  int microseconds = (1000 * time.millisecond) + time.microsecond;
+  return time.add(new Duration(microseconds: 1000000 - microseconds));
+}
+
+/// Successfully matches against a [DateTime] that is the same moment or before
+/// the specified [time].
+Matcher isSameOrBefore(DateTime time) => new _IsSameOrBefore(time);
+
+/// Successfully matches against a [DateTime] that is the same moment or after
+/// the specified [time].
+Matcher isSameOrAfter(DateTime time) => new _IsSameOrAfter(time);
+
+abstract class _CompareDateTime extends Matcher {
+  final DateTime _time;
+  final Matcher _matcher;
+
+  const _CompareDateTime(this._time, this._matcher);
+
+  @override
+  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
+    return item is DateTime &&
+        _matcher.matches(item.compareTo(_time), <dynamic, dynamic>{});
+  }
+
+  @protected
+  String get descriptionOperator;
+
+  @override
+  Description describe(Description description) =>
+      description.add('a DateTime $descriptionOperator $_time');
+
+  @protected
+  String get mismatchAdjective;
+
+  @override
+  Description describeMismatch(
+    dynamic item,
+    Description description,
+    Map<dynamic, dynamic> matchState,
+    bool verbose,
+  ) {
+    if (item is DateTime) {
+      Duration diff = item.difference(_time).abs();
+      return description.add('is $mismatchAdjective $_time by $diff');
+    } else {
+      return description.add('is not a DateTime');
+    }
+  }
+}
+
+class _IsSameOrBefore extends _CompareDateTime {
+  const _IsSameOrBefore(DateTime time) : super(time, isNonPositive);
+
+  @override
+  String get descriptionOperator => '<=';
+
+  @override
+  String get mismatchAdjective => 'after';
+}
+
+class _IsSameOrAfter extends _CompareDateTime {
+  const _IsSameOrAfter(DateTime time) : super(time, isNonNegative);
+
+  @override
+  String get descriptionOperator => '>=';
+
+  @override
+  String get mismatchAdjective => 'before';
+}
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..fb6f079
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,35 @@
+// 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:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  test('floorAndCeilProduceExactSecondDateTime', () {
+    DateTime time = new DateTime.fromMicrosecondsSinceEpoch(1001);
+    DateTime lower = floor(time);
+    DateTime upper = ceil(time);
+    expect(lower.millisecond, 0);
+    expect(upper.millisecond, 0);
+    expect(lower.microsecond, 0);
+    expect(upper.microsecond, 0);
+  });
+
+  test('floorAndCeilWorkWithNow', () {
+    DateTime time = new DateTime.now();
+    int lower = time.difference(floor(time)).inMicroseconds;
+    int upper = ceil(time).difference(time).inMicroseconds;
+    expect(lower, lessThan(1000000));
+    expect(upper, lessThanOrEqualTo(1000000));
+  });
+
+  test('floorAndCeilWorkWithExactSecondDateTime', () {
+    DateTime time = DateTime.parse('1999-12-31 23:59:59');
+    int lower = time.difference(floor(time)).inMicroseconds;
+    int upper = ceil(time).difference(time).inMicroseconds;
+    expect(lower, 0);
+    expect(upper, lessThanOrEqualTo(1000000));
+  });
+}