// Copyright 2024 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:devtools_shared/devtools_shared.dart'; import 'package:path/path.dart' as path; typedef TestDtdConnectionInfo = ({ DtdInfo? info, Process? process, }); /// Helper method to start DTD for the purpose of testing. Future startDtd() async { const dtdConnectTimeout = Duration(seconds: 10); final completer = Completer(); Process? dtdProcess; StreamSubscription? dtdStoutSubscription; TestDtdConnectionInfo onFailure() => (info: null, process: dtdProcess); try { dtdProcess = await Process.start( Platform.resolvedExecutable, ['tooling-daemon', '--machine'], ); dtdStoutSubscription = dtdProcess.stdout.listen((List data) { try { final decoded = utf8.decode(data); final json = jsonDecode(decoded) as Map; if (json case { 'tooling_daemon_details': { 'uri': final String uri, 'trusted_client_secret': final String secret, } }) { completer.complete( ( info: DtdInfo(Uri.parse(uri), secret: secret), process: dtdProcess, ), ); } else { completer.complete(onFailure()); } } catch (e) { completer.complete(onFailure()); } }); return completer.future .timeout(dtdConnectTimeout, onTimeout: onFailure) .then((value) async { await dtdStoutSubscription?.cancel(); return value; }); } catch (e) { await dtdStoutSubscription?.cancel(); return onFailure(); } } class TestDartApp { static final dartVMServiceRegExp = RegExp( r'The Dart VM service is listening on (http://127.0.0.1:.*)', ); final directory = Directory('tmp/test_app'); Process? process; Future start() async { await _initTestApp(); process = await Process.start( Platform.resolvedExecutable, ['--observe=0', 'run', 'bin/main.dart'], workingDirectory: directory.path, ); final serviceUriCompleter = Completer(); late StreamSubscription sub; sub = process!.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) async { if (line.contains(dartVMServiceRegExp)) { await sub.cancel(); serviceUriCompleter.complete( dartVMServiceRegExp.firstMatch(line)!.group(1), ); } }); return await serviceUriCompleter.future.timeout( const Duration(seconds: 5), onTimeout: () async { await sub.cancel(); return ''; }, ); } Future kill() async { process?.kill(); await process?.exitCode; process = null; await deleteDirectoryWithRetry(directory); } Future _initTestApp() async { await deleteDirectoryWithRetry(directory); directory.createSync(recursive: true); final mainFile = File(path.join(directory.path, 'bin', 'main.dart')) ..createSync(recursive: true); mainFile.writeAsStringSync(''' import 'dart:async'; void main() async { for (int i = 0; i < 10000; i++) { await Future.delayed(const Duration(seconds: 2)); } } '''); } } /// Deletes [directory] and retries if the delete operation fails. /// /// Deletes will be retried if they fail for a period to avoid failing due to /// Windows being slow to unlock files after processes terminate. Future deleteDirectoryWithRetry(Directory directory) async { // On Windows, trying to delete a directory immediately after the // test completes may fail with a file locking error. To avoid this, retry // the delete a few times before failing. // // On DanTup's Windows PC, it can take ~5s for the delete to work sometimes // and this will probably be slower on bots. Allow a reasonable time because // taking 10s to delete is better than failing the tests for a non-bug. await runWithRetry( callback: () => directory.deleteSync(recursive: true), maxRetries: 20, retryDelay: const Duration(milliseconds: 500), stopCondition: () => !directory.existsSync(), onRetry: (attempt) => // ignore: avoid_print, deliberate print to monitor delete failures print('Failed to delete directory on attempt $attempt. Retrying...'), ); }