Add copy and open operation handles to inject memory file exceptions without mocking (#184)

diff --git a/packages/file/CHANGELOG.md b/packages/file/CHANGELOG.md
index 4bf571e..69175de 100644
--- a/packages/file/CHANGELOG.md
+++ b/packages/file/CHANGELOG.md
@@ -1,8 +1,12 @@
+#### 6.1.1
+
+* `MemoryFile` now provides `opHandle`s for copy and open operations.
+
 #### 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`.
+* `MemoryFileSystem` now creates the temporary directory before returning in `createTemp`/`createTempSync`.
 
 #### 6.0.1
 
diff --git a/packages/file/lib/src/backends/memory/memory_file.dart b/packages/file/lib/src/backends/memory/memory_file.dart
index a3ce724..988ffd2 100644
--- a/packages/file/lib/src/backends/memory/memory_file.dart
+++ b/packages/file/lib/src/backends/memory/memory_file.dart
@@ -98,6 +98,7 @@
 
   @override
   File copySync(String newPath) {
+    fileSystem.opHandle(path, FileSystemOp.copy);
     FileNode sourceNode = resolvedBacking as FileNode;
     fileSystem.findNode(
       newPath,
@@ -180,6 +181,7 @@
 
   @override
   io.RandomAccessFile openSync({io.FileMode mode = io.FileMode.read}) {
+    fileSystem.opHandle(path, FileSystemOp.open);
     if (utils.isWriteMode(mode) && !existsSync()) {
       // [resolvedBacking] requires that the file already exists, so we must
       // create it here first.
@@ -191,6 +193,7 @@
 
   @override
   Stream<Uint8List> openRead([int? start, int? end]) {
+    fileSystem.opHandle(path, FileSystemOp.open);
     try {
       FileNode node = resolvedBacking as FileNode;
       Uint8List content = node.content;
@@ -210,6 +213,7 @@
     io.FileMode mode = io.FileMode.write,
     Encoding encoding = utf8,
   }) {
+    fileSystem.opHandle(path, FileSystemOp.open);
     if (!utils.isWriteMode(mode)) {
       throw ArgumentError.value(mode, 'mode',
           'Must be either WRITE, APPEND, WRITE_ONLY, or WRITE_ONLY_APPEND');
diff --git a/packages/file/lib/src/backends/memory/operations.dart b/packages/file/lib/src/backends/memory/operations.dart
index a3e47f4..e0bf55f 100644
--- a/packages/file/lib/src/backends/memory/operations.dart
+++ b/packages/file/lib/src/backends/memory/operations.dart
@@ -42,6 +42,20 @@
   /// * [FileSystemEntity.createSync]
   static const FileSystemOp create = FileSystemOp._(3);
 
+  /// A file operation used for all open methods.
+  ///
+  /// * [File.open]
+  /// * [File.openSync]
+  /// * [File.openRead]
+  /// * [File.openWrite]
+  static const FileSystemOp open = FileSystemOp._(4);
+
+  /// A file operation used for all copy methods.
+  ///
+  /// * [File.copy]
+  /// * [File.copySync]
+  static const FileSystemOp copy = FileSystemOp._(5);
+
   @override
   String toString() {
     switch (_value) {
@@ -53,6 +67,10 @@
         return 'FileSystemOp.delete';
       case 3:
         return 'FileSystemOp.create';
+      case 4:
+        return 'FileSystemOp.open';
+      case 5:
+        return 'FileSystemOp.copy';
       default:
         throw StateError('Invalid FileSytemOp type: $this');
     }
diff --git a/packages/file/pubspec.yaml b/packages/file/pubspec.yaml
index a627171..14ac96a 100644
--- a/packages/file/pubspec.yaml
+++ b/packages/file/pubspec.yaml
@@ -1,5 +1,5 @@
 name: file
-version: 6.1.0
+version: 6.1.1
 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
index e878317..2efa569 100644
--- a/packages/file/test/memory_operations_test.dart
+++ b/packages/file/test/memory_operations_test.dart
@@ -139,6 +139,54 @@
     ]);
   });
 
+  test('Open operations invoke opHandle', () async {
+    List<String> contexts = <String>[];
+    List<FileSystemOp> operations = <FileSystemOp>[];
+    MemoryFileSystem fs = MemoryFileSystem.test(
+        opHandle: (String context, FileSystemOp operation) {
+      if (operation == FileSystemOp.open) {
+        contexts.add(context);
+        operations.add(operation);
+      }
+    });
+    final File file = fs.file('test')..createSync();
+
+    await file.open();
+    file.openSync();
+    file.openRead();
+    file.openWrite();
+
+    expect(contexts, <String>['test', 'test', 'test', 'test']);
+    expect(operations, <FileSystemOp>[
+      FileSystemOp.open,
+      FileSystemOp.open,
+      FileSystemOp.open,
+      FileSystemOp.open,
+    ]);
+  });
+
+  test('Copy operations invoke opHandle', () async {
+    List<String> contexts = <String>[];
+    List<FileSystemOp> operations = <FileSystemOp>[];
+    MemoryFileSystem fs = MemoryFileSystem.test(
+        opHandle: (String context, FileSystemOp operation) {
+      if (operation == FileSystemOp.copy) {
+        contexts.add(context);
+        operations.add(operation);
+      }
+    });
+    final File file = fs.file('test')..createSync();
+
+    await file.copy('A');
+    file.copySync('B');
+
+    expect(contexts, <String>['test', 'test']);
+    expect(operations, <FileSystemOp>[
+      FileSystemOp.copy,
+      FileSystemOp.copy,
+    ]);
+  });
+
   test('FileSystemOp toString', () {
     expect(FileSystemOp.create.toString(), 'FileSystemOp.create');
     expect(FileSystemOp.delete.toString(), 'FileSystemOp.delete');