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(