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); + }); }