| // 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<TestDtdConnectionInfo> startDtd() async { | |
| const dtdConnectTimeout = Duration(seconds: 10); | |
| final completer = Completer<TestDtdConnectionInfo>(); | |
| 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<int> data) { | |
| try { | |
| final decoded = utf8.decode(data); | |
| final json = jsonDecode(decoded) as Map<String, Object?>; | |
| 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 await completer.future | |
| .timeout(dtdConnectTimeout, onTimeout: onFailure) | |
| .then((value) async { | |
| await dtdStoutSubscription?.cancel(); | |
| return value; | |
| }); | |
| } catch (e) { | |
| await dtdStoutSubscription?.cancel(); | |
| return onFailure(); | |
| } | |
| } | |
| class TestDartApp { | |
| TestDartApp() { | |
| directory = Directory.systemTemp.createTempSync('test_app_'); | |
| } | |
| static final dartVMServiceRegExp = RegExp( | |
| r'The Dart VM service is listening on (http://127.0.0.1:.*)', | |
| ); | |
| late final Directory directory; | |
| Process? process; | |
| StreamSubscription<String>? _stdoutSub; | |
| StreamSubscription<String>? _stderrSub; | |
| Future<String> start() async { | |
| _initTestApp(); | |
| process = await Process.start(Platform.resolvedExecutable, [ | |
| '--observe=0', | |
| 'bin/main.dart', | |
| ], workingDirectory: directory.path); | |
| final serviceUriCompleter = Completer<String>(); | |
| _stdoutSub = process!.stdout | |
| .transform(utf8.decoder) | |
| .transform(const LineSplitter()) | |
| .listen((line) { | |
| if (!serviceUriCompleter.isCompleted && | |
| line.contains(dartVMServiceRegExp)) { | |
| serviceUriCompleter.complete( | |
| dartVMServiceRegExp.firstMatch(line)!.group(1), | |
| ); | |
| } | |
| }); | |
| _stderrSub = process!.stderr | |
| .transform(utf8.decoder) | |
| .transform(const LineSplitter()) | |
| .listen((line) { | |
| // ignore: avoid_print, deliberate print to monitor errors. | |
| print('TestDartApp stderr: $line'); | |
| }); | |
| return await serviceUriCompleter.future.timeout( | |
| const Duration(seconds: 5), | |
| onTimeout: () { | |
| return ''; | |
| }, | |
| ); | |
| } | |
| Future<void> kill() async { | |
| await _stdoutSub?.cancel(); | |
| await _stderrSub?.cancel(); | |
| process?.kill(); | |
| await process?.exitCode; | |
| process = null; | |
| await deleteDirectoryWithRetry(directory); | |
| } | |
| void _initTestApp() { | |
| Directory( | |
| path.join(directory.path, '.dart_tool'), | |
| ).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<void> 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...'), | |
| ); | |
| } |