Add an operation handle to allow inserting FileSystemExceptions in the memory fs without mocking (#181)
diff --git a/packages/file/CHANGELOG.md b/packages/file/CHANGELOG.md
index 4537832..4bf571e 100644
--- a/packages/file/CHANGELOG.md
+++ b/packages/file/CHANGELOG.md
@@ -1,3 +1,9 @@
+#### 6.1.0
+
+* Reading invalid UTF8 with the `MemoryFileSystem` now correctly throws a `FileSystemException` instead of a `FormatError`.
+* `MemoryFileSystem` now provides an `opHandle` to inspect read/write operations.
+* `MemoryFileSystem` now creates the tempory directory before returning in `createTemp`/`createTempSync`.
+
#### 6.0.1
* Fix sound type error in memory backend when reading non-existent `MemoryDirectory`.
diff --git a/packages/file/lib/memory.dart b/packages/file/lib/memory.dart
index 31e90e7..c5705ef 100644
--- a/packages/file/lib/memory.dart
+++ b/packages/file/lib/memory.dart
@@ -5,3 +5,4 @@
/// An implementation of `FileSystem` that exists entirely in memory with an
/// internal representation loosely based on the Filesystem Hierarchy Standard.
export 'src/backends/memory.dart';
+export 'src/backends/memory/operations.dart';
diff --git a/packages/file/lib/src/backends/memory/memory_directory.dart b/packages/file/lib/src/backends/memory/memory_directory.dart
index 6b11263..c5c590e 100644
--- a/packages/file/lib/src/backends/memory/memory_directory.dart
+++ b/packages/file/lib/src/backends/memory/memory_directory.dart
@@ -12,6 +12,7 @@
import 'memory_file_system_entity.dart';
import 'memory_link.dart';
import 'node.dart';
+import 'operations.dart';
import 'style.dart';
import 'utils.dart' as utils;
@@ -47,6 +48,7 @@
@override
void createSync({bool recursive = false}) {
+ fileSystem.opHandle(path, FileSystemOp.create);
Node? node = internalCreateSync(
followTailLink: true,
visitLinks: true,
@@ -84,7 +86,8 @@
_systemTempCounter[fileSystem] = _tempCounter;
DirectoryNode tempDir = DirectoryNode(node);
node.children[name()] = tempDir;
- return MemoryDirectory(fileSystem, fileSystem.path.join(dirname, name()));
+ return MemoryDirectory(fileSystem, fileSystem.path.join(dirname, name()))
+ ..createSync();
}
@override
diff --git a/packages/file/lib/src/backends/memory/memory_file.dart b/packages/file/lib/src/backends/memory/memory_file.dart
index e285f49..a3ce724 100644
--- a/packages/file/lib/src/backends/memory/memory_file.dart
+++ b/packages/file/lib/src/backends/memory/memory_file.dart
@@ -8,6 +8,7 @@
import 'dart:typed_data';
import 'package:file/file.dart';
+import 'package:file/src/backends/memory/operations.dart';
import 'package:file/src/common.dart' as common;
import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
@@ -51,6 +52,7 @@
@override
void createSync({bool recursive = false}) {
+ fileSystem.opHandle(path, FileSystemOp.create);
_doCreate(recursive: recursive);
}
@@ -219,16 +221,23 @@
Future<Uint8List> readAsBytes() async => readAsBytesSync();
@override
- Uint8List readAsBytesSync() =>
- Uint8List.fromList((resolvedBacking as FileNode).content);
+ Uint8List readAsBytesSync() {
+ fileSystem.opHandle(path, FileSystemOp.read);
+ return Uint8List.fromList((resolvedBacking as FileNode).content);
+ }
@override
Future<String> readAsString({Encoding encoding = utf8}) async =>
readAsStringSync(encoding: encoding);
@override
- String readAsStringSync({Encoding encoding = utf8}) =>
- encoding.decode(readAsBytesSync());
+ String readAsStringSync({Encoding encoding = utf8}) {
+ try {
+ return encoding.decode(readAsBytesSync());
+ } on FormatException catch (err) {
+ throw FileSystemException(err.message, path);
+ }
+ }
@override
Future<List<String>> readAsLines({Encoding encoding = utf8}) async =>
@@ -272,6 +281,7 @@
}
FileNode node = _resolvedBackingOrCreate;
_truncateIfNecessary(node, mode);
+ fileSystem.opHandle(path, FileSystemOp.write);
node.write(bytes);
node.touch();
}
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 adb5e0a..8d63ea5 100644
--- a/packages/file/lib/src/backends/memory/memory_file_system.dart
+++ b/packages/file/lib/src/backends/memory/memory_file_system.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:file/file.dart';
+import 'package:file/src/backends/memory/operations.dart';
import 'package:file/src/io.dart' as io;
import 'package:path/path.dart' as p;
@@ -19,6 +20,8 @@
const String _thisDir = '.';
const String _parentDir = '..';
+void _defaultOpHandle(String context, FileSystemOp operation) {}
+
/// An implementation of [FileSystem] that exists entirely in memory with an
/// internal representation loosely based on the Filesystem Hierarchy Standard.
///
@@ -41,10 +44,13 @@
/// style. The default is [FileSystemStyle.posix].
factory MemoryFileSystem({
FileSystemStyle style = FileSystemStyle.posix,
+ void Function(String context, FileSystemOp operation) opHandle =
+ _defaultOpHandle,
}) =>
_MemoryFileSystem(
style: style,
clock: const Clock.realTime(),
+ opHandle: opHandle,
);
/// Creates a new `MemoryFileSystem` that has a fake clock.
@@ -59,10 +65,13 @@
/// style. The default is [FileSystemStyle.posix].
factory MemoryFileSystem.test({
FileSystemStyle style = FileSystemStyle.posix,
+ void Function(String context, FileSystemOp operation) opHandle =
+ _defaultOpHandle,
}) =>
_MemoryFileSystem(
style: style,
clock: Clock.monotonicTest(),
+ opHandle: opHandle,
);
}
@@ -72,14 +81,17 @@
_MemoryFileSystem({
this.style = FileSystemStyle.posix,
required this.clock,
- }) {
- _context = style.contextFor(style.root);
+ this.opHandle = _defaultOpHandle,
+ }) : _context = style.contextFor(style.root) {
_root = RootNode(this);
}
RootNode? _root;
String? _systemTemp;
- late p.Context _context;
+ p.Context _context;
+
+ @override
+ final Function(String context, FileSystemOp operation) opHandle;
@override
final Clock clock;
diff --git a/packages/file/lib/src/backends/memory/memory_file_system_entity.dart b/packages/file/lib/src/backends/memory/memory_file_system_entity.dart
index cdf64e3..c2f9c4d 100644
--- a/packages/file/lib/src/backends/memory/memory_file_system_entity.dart
+++ b/packages/file/lib/src/backends/memory/memory_file_system_entity.dart
@@ -10,6 +10,7 @@
import 'common.dart';
import 'memory_directory.dart';
import 'node.dart';
+import 'operations.dart';
import 'style.dart';
import 'utils.dart' as utils;
@@ -286,6 +287,7 @@
bool recursive = false,
utils.TypeChecker? checkType,
}) {
+ fileSystem.opHandle(path, FileSystemOp.delete);
Node node = backing;
if (!recursive) {
if (node is DirectoryNode && node.children.isNotEmpty) {
diff --git a/packages/file/lib/src/backends/memory/memory_link.dart b/packages/file/lib/src/backends/memory/memory_link.dart
index 31c084a..a3cba7c 100644
--- a/packages/file/lib/src/backends/memory/memory_link.dart
+++ b/packages/file/lib/src/backends/memory/memory_link.dart
@@ -9,6 +9,7 @@
import 'memory_file_system_entity.dart';
import 'node.dart';
+import 'operations.dart';
import 'utils.dart' as utils;
/// Internal implementation of [Link].
@@ -47,6 +48,7 @@
@override
void createSync(String target, {bool recursive = false}) {
bool preexisting = true;
+ fileSystem.opHandle(path, FileSystemOp.create);
internalCreateSync(
createChild: (DirectoryNode parent, bool isFinalSegment) {
if (isFinalSegment) {
diff --git a/packages/file/lib/src/backends/memory/node.dart b/packages/file/lib/src/backends/memory/node.dart
index 71c86d2..4cbb25c 100644
--- a/packages/file/lib/src/backends/memory/node.dart
+++ b/packages/file/lib/src/backends/memory/node.dart
@@ -5,6 +5,7 @@
import 'dart:typed_data';
import 'package:file/file.dart';
+import 'package:file/src/backends/memory/operations.dart';
import 'package:file/src/io.dart' as io;
import 'clock.dart';
@@ -41,6 +42,9 @@
/// A [FileSystem] whose internal structure is made up of a tree of [Node]
/// instances, rooted at a single node.
abstract class NodeBasedFileSystem implements StyleableFileSystem {
+ /// An optional handle to hook into common file system operations.
+ void Function(String context, FileSystemOp operation) get opHandle;
+
/// The root node.
RootNode? get root;
diff --git a/packages/file/lib/src/backends/memory/operations.dart b/packages/file/lib/src/backends/memory/operations.dart
new file mode 100644
index 0000000..a3e47f4
--- /dev/null
+++ b/packages/file/lib/src/backends/memory/operations.dart
@@ -0,0 +1,60 @@
+// 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.
+
+/// A file system operation used by the [MemoryFileSytem] to allow
+/// tests to insert errors for certain operations.
+///
+/// This is not implemented as an enum to allow new values to be added in a
+/// backwards compatible manner.
+class FileSystemOp {
+ const FileSystemOp._(this._value);
+
+ // This field added to ensure const values can be different.
+ // ignore: unused_field
+ final int _value;
+
+ /// A file system operation used for all read methods.
+ ///
+ /// * [FileSystemEntity.readAsString]
+ /// * [FileSystemEntity.readAsStringSync]
+ /// * [FileSystemEntity.readAsBytes]
+ /// * [FileSystemEntity.readAsBytesSync]
+ static const FileSystemOp read = FileSystemOp._(0);
+
+ /// A file system operation used for all write methods.
+ ///
+ /// * [FileSystemEntity.writeAsString]
+ /// * [FileSystemEntity.writeAsStringSync]
+ /// * [FileSystemEntity.writeAsBytes]
+ /// * [FileSystemEntity.writeAsBytesSync]
+ static const FileSystemOp write = FileSystemOp._(1);
+
+ /// A file system operation used for all delete methods.
+ ///
+ /// * [FileSystemEntity.delete]
+ /// * [FileSystemEntity.deleteSync]
+ static const FileSystemOp delete = FileSystemOp._(2);
+
+ /// A file system operation used for all create methods.
+ ///
+ /// * [FileSystemEntity.create]
+ /// * [FileSystemEntity.createSync]
+ static const FileSystemOp create = FileSystemOp._(3);
+
+ @override
+ String toString() {
+ switch (_value) {
+ case 0:
+ return 'FileSystemOp.read';
+ case 1:
+ return 'FileSystemOp.write';
+ case 2:
+ return 'FileSystemOp.delete';
+ case 3:
+ return 'FileSystemOp.create';
+ default:
+ throw StateError('Invalid FileSytemOp type: $this');
+ }
+ }
+}
diff --git a/packages/file/pubspec.yaml b/packages/file/pubspec.yaml
index f7485f1..a627171 100644
--- a/packages/file/pubspec.yaml
+++ b/packages/file/pubspec.yaml
@@ -1,5 +1,5 @@
name: file
-version: 6.0.1
+version: 6.1.0
description:
A pluggable, mockable file system abstraction for Dart. Supports local file
system access, as well as in-memory file systems, record-replay file systems,
diff --git a/packages/file/test/memory_operations_test.dart b/packages/file/test/memory_operations_test.dart
new file mode 100644
index 0000000..e878317
--- /dev/null
+++ b/packages/file/test/memory_operations_test.dart
@@ -0,0 +1,148 @@
+// 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:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:file/src/interface/file.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('Read operations invoke opHandle', () async {
+ List<String> contexts = <String>[];
+ List<FileSystemOp> operations = <FileSystemOp>[];
+ MemoryFileSystem fs = MemoryFileSystem.test(
+ opHandle: (String context, FileSystemOp operation) {
+ if (operation == FileSystemOp.read) {
+ contexts.add(context);
+ operations.add(operation);
+ }
+ });
+ final File file = fs.file('test')..createSync();
+
+ await file.readAsBytes();
+ file.readAsBytesSync();
+ await file.readAsString();
+ file.readAsStringSync();
+
+ expect(contexts, <String>['test', 'test', 'test', 'test']);
+ expect(operations, <FileSystemOp>[
+ FileSystemOp.read,
+ FileSystemOp.read,
+ FileSystemOp.read,
+ FileSystemOp.read
+ ]);
+ });
+
+ test('Write operations invoke opHandle', () async {
+ List<String> contexts = <String>[];
+ List<FileSystemOp> operations = <FileSystemOp>[];
+ MemoryFileSystem fs = MemoryFileSystem.test(
+ opHandle: (String context, FileSystemOp operation) {
+ if (operation == FileSystemOp.write) {
+ contexts.add(context);
+ operations.add(operation);
+ }
+ });
+ final File file = fs.file('test')..createSync();
+
+ await file.writeAsBytes(<int>[]);
+ file.writeAsBytesSync(<int>[]);
+ await file.writeAsString('');
+ file.writeAsStringSync('');
+
+ expect(contexts, <String>['test', 'test', 'test', 'test']);
+ expect(operations, <FileSystemOp>[
+ FileSystemOp.write,
+ FileSystemOp.write,
+ FileSystemOp.write,
+ FileSystemOp.write
+ ]);
+ });
+
+ test('Delete operations invoke opHandle', () async {
+ List<String> contexts = <String>[];
+ List<FileSystemOp> operations = <FileSystemOp>[];
+ MemoryFileSystem fs = MemoryFileSystem.test(
+ opHandle: (String context, FileSystemOp operation) {
+ if (operation == FileSystemOp.delete) {
+ contexts.add(context);
+ operations.add(operation);
+ }
+ });
+ final File file = fs.file('test')..createSync();
+ final Directory directory = fs.directory('testDir')..createSync();
+ final Link link = fs.link('testLink')..createSync('foo');
+
+ await file.delete();
+ file.createSync();
+ file.deleteSync();
+
+ await directory.delete();
+ directory.createSync();
+ directory.deleteSync();
+
+ await link.delete();
+ link.createSync('foo');
+ link.deleteSync();
+
+ expect(contexts,
+ <String>['test', 'test', 'testDir', 'testDir', 'testLink', 'testLink']);
+ expect(operations, <FileSystemOp>[
+ FileSystemOp.delete,
+ FileSystemOp.delete,
+ FileSystemOp.delete,
+ FileSystemOp.delete,
+ FileSystemOp.delete,
+ FileSystemOp.delete,
+ ]);
+ });
+
+ test('Create operations invoke opHandle', () async {
+ List<String> contexts = <String>[];
+ List<FileSystemOp> operations = <FileSystemOp>[];
+ MemoryFileSystem fs = MemoryFileSystem.test(
+ opHandle: (String context, FileSystemOp operation) {
+ if (operation == FileSystemOp.create) {
+ contexts.add(context);
+ operations.add(operation);
+ }
+ });
+ fs.file('testA').createSync();
+ await fs.file('testB').create();
+ fs.directory('testDirA').createSync();
+ await fs.directory('testDirB').create();
+ fs.link('testLinkA').createSync('foo');
+ await fs.link('testLinkB').create('foo');
+ fs.currentDirectory.createTempSync('tmp.bar');
+ await fs.currentDirectory.createTemp('tmp.bar');
+
+ expect(contexts, <dynamic>[
+ 'testA',
+ 'testB',
+ 'testDirA',
+ 'testDirB',
+ 'testLinkA',
+ 'testLinkB',
+ startsWith('/tmp.bar'),
+ startsWith('/tmp.bar'),
+ ]);
+ expect(operations, <FileSystemOp>[
+ FileSystemOp.create,
+ FileSystemOp.create,
+ FileSystemOp.create,
+ FileSystemOp.create,
+ FileSystemOp.create,
+ FileSystemOp.create,
+ FileSystemOp.create,
+ FileSystemOp.create,
+ ]);
+ });
+
+ test('FileSystemOp toString', () {
+ expect(FileSystemOp.create.toString(), 'FileSystemOp.create');
+ expect(FileSystemOp.delete.toString(), 'FileSystemOp.delete');
+ expect(FileSystemOp.read.toString(), 'FileSystemOp.read');
+ expect(FileSystemOp.write.toString(), 'FileSystemOp.write');
+ });
+}
diff --git a/packages/file/test/memory_test.dart b/packages/file/test/memory_test.dart
index cb241d2..8e358be 100644
--- a/packages/file/test/memory_test.dart
+++ b/packages/file/test/memory_test.dart
@@ -4,6 +4,7 @@
import 'dart:io' as io;
+import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file/src/backends/memory/memory_random_access_file.dart';
import 'package:test/test.dart';
@@ -131,4 +132,20 @@
expect(fooAA.path, '/.tmp_rand0/foorand0');
expect(fooBB.path, '/.tmp_rand0/foorand1');
});
+
+ test('Failed UTF8 decoding in MemoryFileSystem throws a FileSystemException',
+ () {
+ final MemoryFileSystem fileSystem = MemoryFileSystem.test();
+ final File file = fileSystem.file('foo')
+ ..writeAsBytesSync(<int>[0xFFFE]); // Invalid UTF8
+
+ expect(file.readAsStringSync, throwsA(isA<FileSystemException>()));
+ });
+
+ test('Creating a temporary directory actually creates the directory', () {
+ final MemoryFileSystem fileSystem = MemoryFileSystem.test();
+ final Directory tempDir = fileSystem.currentDirectory.createTempSync('foo');
+
+ expect(tempDir.existsSync(), true);
+ });
}