Retry filesystem deletes (#1965)

We frequently see CI failures with exceptions pointing to various
deletes which fail because of processes still having the file or
directory open. Add a retry with some async backoff.
diff --git a/pkgs/test/lib/src/runner/browser/chrome.dart b/pkgs/test/lib/src/runner/browser/chrome.dart
index 0feb397..9835006 100644
--- a/pkgs/test/lib/src/runner/browser/chrome.dart
+++ b/pkgs/test/lib/src/runner/browser/chrome.dart
@@ -88,8 +88,8 @@
           remoteDebuggerCompleter.complete(null);
         }
 
-        unawaited(process.exitCode
-            .then((_) => Directory(dir).deleteSync(recursive: true)));
+        unawaited(
+            process.exitCode.then((_) => Directory(dir).deleteWithRetry()));
 
         return process;
       }
diff --git a/pkgs/test/lib/src/runner/browser/firefox.dart b/pkgs/test/lib/src/runner/browser/firefox.dart
index 5b68018..50fdecf 100644
--- a/pkgs/test/lib/src/runner/browser/firefox.dart
+++ b/pkgs/test/lib/src/runner/browser/firefox.dart
@@ -50,8 +50,7 @@
       'MOZ_CRASHREPORTER_DISABLE': '1'
     });
 
-    unawaited(process.exitCode
-        .then((_) => Directory(dir).deleteSync(recursive: true)));
+    unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry()));
 
     return process;
   }
diff --git a/pkgs/test/lib/src/runner/browser/platform.dart b/pkgs/test/lib/src/runner/browser/platform.dart
index a44eb7e..80fd0df 100644
--- a/pkgs/test/lib/src/runner/browser/platform.dart
+++ b/pkgs/test/lib/src/runner/browser/platform.dart
@@ -472,7 +472,7 @@
         ]);
 
         if (_config.pubServeUrl == null) {
-          Directory(_compiledDir!).deleteSync(recursive: true);
+          await Directory(_compiledDir!).deleteWithRetry();
         } else {
           _http!.close();
         }
diff --git a/pkgs/test/lib/src/runner/browser/safari.dart b/pkgs/test/lib/src/runner/browser/safari.dart
index 41cd896..6689a8b 100644
--- a/pkgs/test/lib/src/runner/browser/safari.dart
+++ b/pkgs/test/lib/src/runner/browser/safari.dart
@@ -40,8 +40,7 @@
     var process = await Process.start(
         settings.executable, settings.arguments.toList()..add(redirect));
 
-    unawaited(process.exitCode
-        .then((_) => Directory(dir).deleteSync(recursive: true)));
+    unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry()));
 
     return process;
   }
diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart
index 8d04f46..23a362a 100644
--- a/pkgs/test/lib/src/runner/node/platform.dart
+++ b/pkgs/test/lib/src/runner/node/platform.dart
@@ -298,7 +298,7 @@
         await _compilers.close();
 
         if (_config.pubServeUrl == null) {
-          Directory(_compiledDir).deleteSync(recursive: true);
+          await Directory(_compiledDir).deleteWithRetry();
         } else {
           _http!.close();
         }
diff --git a/pkgs/test/lib/src/runner/wasm/platform.dart b/pkgs/test/lib/src/runner/wasm/platform.dart
index 7ed30f5..0103171 100644
--- a/pkgs/test/lib/src/runner/wasm/platform.dart
+++ b/pkgs/test/lib/src/runner/wasm/platform.dart
@@ -358,9 +358,8 @@
             browser.then((b) => b?.close()),
           _server.close(),
           _compilers.close(),
+          Directory(_compiledDir).deleteWithRetry(),
         ]);
-
-        Directory(_compiledDir).deleteSync(recursive: true);
       });
   final _closeMemo = AsyncMemoizer<void>();
 }
diff --git a/pkgs/test_core/lib/src/runner/vm/platform.dart b/pkgs/test_core/lib/src/runner/vm/platform.dart
index 75e56dc..c260c9c 100644
--- a/pkgs/test_core/lib/src/runner/vm/platform.dart
+++ b/pkgs/test_core/lib/src/runner/vm/platform.dart
@@ -26,6 +26,7 @@
 import '../../runner/plugin/shared_platform_helpers.dart';
 import '../../runner/runner_suite.dart';
 import '../../runner/suite.dart';
+import '../../util/io.dart';
 import '../../util/package_config.dart';
 import '../package_version.dart';
 import 'environment.dart';
@@ -157,8 +158,10 @@
   }
 
   @override
-  Future close() => _closeMemo.runOnce(() =>
-      Future.wait([_compiler.dispose(), _tempDir.delete(recursive: true)]));
+  Future close() => _closeMemo.runOnce(() => Future.wait([
+        _compiler.dispose(),
+        _tempDir.deleteWithRetry(),
+      ]));
 
   Uri _absolute(String path) {
     final uri = p.toUri(path);
diff --git a/pkgs/test_core/lib/src/runner/vm/test_compiler.dart b/pkgs/test_core/lib/src/runner/vm/test_compiler.dart
index a87a70b..7ca295e 100644
--- a/pkgs/test_core/lib/src/runner/vm/test_compiler.dart
+++ b/pkgs/test_core/lib/src/runner/vm/test_compiler.dart
@@ -13,6 +13,7 @@
 import 'package:test_api/backend.dart'; // ignore: deprecated_member_use
 
 import '../../util/dart.dart';
+import '../../util/io.dart';
 import '../../util/package_config.dart';
 import '../package_version.dart';
 
@@ -182,7 +183,7 @@
         _frontendServerClient?.kill();
         _frontendServerClient = null;
         if (_outputDillDirectory.existsSync()) {
-          _outputDillDirectory.deleteSync(recursive: true);
+          await _outputDillDirectory.deleteWithRetry();
         }
       });
 }
diff --git a/pkgs/test_core/lib/src/util/io.dart b/pkgs/test_core/lib/src/util/io.dart
index 2fb0131..59e613c 100644
--- a/pkgs/test_core/lib/src/util/io.dart
+++ b/pkgs/test_core/lib/src/util/io.dart
@@ -7,6 +7,7 @@
 import 'dart:core' as core;
 import 'dart:core';
 import 'dart:io';
+import 'dart:math';
 
 import 'package:async/async.dart';
 import 'package:path/path.dart' as p;
@@ -112,7 +113,7 @@
   return Future.sync(() {
     var tempDir = createTempDir();
     return Future.sync(() => fn(tempDir))
-        .whenComplete(() => Directory(tempDir).deleteSync(recursive: true));
+        .whenComplete(() => Directory(tempDir).deleteWithRetry());
   });
 }
 
@@ -226,3 +227,19 @@
     return base;
   }
 }
+
+extension RetryDelete on FileSystemEntity {
+  Future<void> deleteWithRetry() async {
+    var attempt = 0;
+    while (true) {
+      try {
+        await delete(recursive: true);
+        return;
+      } on FileSystemException {
+        if (attempt == 2) rethrow;
+        attempt++;
+        await Future.delayed(Duration(milliseconds: pow(10, attempt).toInt()));
+      }
+    }
+  }
+}