| // 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 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<String> start() async { |
| await _initTestApp(); |
| process = await Process.start( |
| Platform.resolvedExecutable, |
| ['--observe=0', 'run', 'bin/main.dart'], |
| workingDirectory: directory.path, |
| ); |
| |
| final serviceUriCompleter = Completer<String>(); |
| 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<void> kill() async { |
| process?.kill(); |
| await process?.exitCode; |
| process = null; |
| await deleteDirectoryWithRetry(directory); |
| } |
| |
| Future<void> _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<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...'), |
| ); |
| } |