Fix MemoryFile.readAsLines to behave more like File.readAsLines (#147)

* Fix MemoryFile.readAsLines to behave more like File.readAsLines

Fix `MemoryFile.readAsLines` to behave more like `File.readAsLines`.
A final newline should not add an empty string as the last element of
the returned `List`.

Fixes https://github.com/google/file.dart/issues/142.

* Make RecordingFile.readAsLines/readAsLinesSync always record a final newline

Fixes https://github.com/google/file.dart/issues/146.
diff --git a/packages/file/CHANGELOG.md b/packages/file/CHANGELOG.md
index 6b4f893..b2c3ae7 100644
--- a/packages/file/CHANGELOG.md
+++ b/packages/file/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 * systemTemp directories created by `MemoryFileSystem` will allot names
   based on the file system instance instead of globally.
+* `MemoryFile.readAsLines()`/`readAsLinesSync()` no longer treat a final newline
+  in the file as the start of a new, empty line.
+* `RecordingFile.readAsLine()`/`readAsLinesSync()` now always record a final
+  newline.
 
 #### 5.2.0
 
diff --git a/packages/file/lib/src/backends/memory/memory_file.dart b/packages/file/lib/src/backends/memory/memory_file.dart
index 94ce447..8df0a0c 100644
--- a/packages/file/lib/src/backends/memory/memory_file.dart
+++ b/packages/file/lib/src/backends/memory/memory_file.dart
@@ -234,7 +234,18 @@
   @override
   List<String> readAsLinesSync({Encoding encoding = utf8}) {
     String str = readAsStringSync(encoding: encoding);
-    return str.isEmpty ? <String>[] : str.split('\n');
+
+    if (str.isEmpty) {
+      return <String>[];
+    }
+
+    final List<String> lines = str.split('\n');
+    if (str.endsWith('\n')) {
+      // A final newline should not create an additional line.
+      lines.removeLast();
+    }
+
+    return lines;
   }
 
   @override
diff --git a/packages/file/lib/src/backends/record_replay/recording_file.dart b/packages/file/lib/src/backends/record_replay/recording_file.dart
index 6cfb178..3114f1e 100644
--- a/packages/file/lib/src/backends/record_replay/recording_file.dart
+++ b/packages/file/lib/src/backends/record_replay/recording_file.dart
@@ -153,7 +153,7 @@
       file: _newRecordingFile(),
       future: delegate.readAsLines(encoding: encoding),
       writer: (File file, List<String> lines) async {
-        await file.writeAsString(lines.join('\n'), flush: true);
+        await file.writeAsString(_joinLines(lines), flush: true);
       },
     );
   }
@@ -163,7 +163,7 @@
       file: _newRecordingFile(),
       value: delegate.readAsLinesSync(encoding: encoding),
       writer: (File file, List<String> lines) {
-        file.writeAsStringSync(lines.join('\n'), flush: true);
+        file.writeAsStringSync(_joinLines(lines), flush: true);
       },
     );
   }
@@ -259,3 +259,10 @@
   @override
   String get serializedValue => '!${_file.basename}';
 }
+
+/// Flattens a list of lines into a single, newline-delimited string.
+///
+/// Each element of [lines] is assumed to represent a complete line and will
+/// be end with a newline in the resulting string.
+String _joinLines(List<String> lines) =>
+    lines.isEmpty ? '' : (lines.join('\n') + '\n');
diff --git a/packages/file/test/common_tests.dart b/packages/file/test/common_tests.dart
index 923cf61..b99f3aa 100644
--- a/packages/file/test/common_tests.dart
+++ b/packages/file/test/common_tests.dart
@@ -2465,6 +2465,13 @@
       });
 
       group('readAsLines', () {
+        const String testString = 'Hello world\nHow are you?\nI am fine';
+        final List<String> expectedLines = <String>[
+          'Hello world',
+          'How are you?',
+          'I am fine',
+        ];
+
         test('throwsIfDoesntExist', () {
           expectFileSystemException(ErrorCodes.ENOENT, () {
             fs.file(ns('/foo')).readAsLinesSync();
@@ -2488,29 +2495,33 @@
 
         test('succeedsIfExistsAsFile', () {
           File f = fs.file(ns('/foo'))..createSync();
-          f.writeAsStringSync('Hello world\nHow are you?\nI am fine');
-          expect(f.readAsLinesSync(), <String>[
-            'Hello world',
-            'How are you?',
-            'I am fine',
-          ]);
+          f.writeAsStringSync(testString);
+          expect(f.readAsLinesSync(), expectedLines);
         });
 
         test('succeedsIfExistsAsLinkToFile', () {
           File f = fs.file(ns('/foo'))..createSync();
           fs.link(ns('/bar')).createSync(ns('/foo'));
-          f.writeAsStringSync('Hello world\nHow are you?\nI am fine');
-          expect(f.readAsLinesSync(), <String>[
-            'Hello world',
-            'How are you?',
-            'I am fine',
-          ]);
+          f.writeAsStringSync(testString);
+          expect(f.readAsLinesSync(), expectedLines);
         });
 
         test('returnsEmptyListForZeroByteFile', () {
           File f = fs.file(ns('/foo'))..createSync();
           expect(f.readAsLinesSync(), isEmpty);
         });
+
+        test('isTrailingNewlineAgnostic', () {
+          File f = fs.file(ns('/foo'))..createSync();
+          f.writeAsStringSync(testString + '\n');
+          expect(f.readAsLinesSync(), expectedLines);
+
+          f.writeAsStringSync('\n');
+          expect(f.readAsLinesSync(), <String>['']);
+
+          f.writeAsStringSync('\n\n');
+          expect(f.readAsLinesSync(), <String>['', '']);
+        });
       });
 
       group('writeAsBytes', () {
diff --git a/packages/file/test/recording_test.dart b/packages/file/test/recording_test.dart
index ea08d70..3c04437 100644
--- a/packages/file/test/recording_test.dart
+++ b/packages/file/test/recording_test.dart
@@ -763,7 +763,9 @@
         });
 
         test('readAsLines', () async {
-          String content = 'Hello\nWorld';
+          // [readAsLines] is appropriate only for text files, and POSIX
+          // requires that valid text files end with a terminating newline.
+          String content = 'Hello\nWorld\n';
           await delegate.file('/foo').writeAsString(content, flush: true);
           await fs.file('/foo').readAsLines();
           expect(
@@ -792,7 +794,9 @@
         });
 
         test('readAsLinesSync', () async {
-          String content = 'Hello\nWorld';
+          // [readAsLinesSync] is appropriate only for text files, and POSIX
+          // requires that valid text files end with a terminating newline.
+          String content = 'Hello\nWorld\n';
           await delegate.file('/foo').writeAsString(content, flush: true);
           fs.file('/foo').readAsLinesSync();
           expect(