| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:math'; |
| |
| import 'package:native_test_helpers/native_test_helpers.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../helpers.dart'; |
| |
| const Timeout longTimeout = Timeout(Duration(minutes: 5)); |
| |
| void main() async { |
| final packageUri = findPackageRoot('hooks_runner'); |
| |
| test('Concurrent invocations', timeout: longTimeout, () async { |
| await inTempDir((tempUri) async { |
| Future<ProcessResult> runInProcess() async { |
| final result = await Process.run(dartExecutable.toFilePath(), [ |
| packageUri |
| .resolve('test/locking/locking_test_helper.dart') |
| .toFilePath(), |
| tempUri.toFilePath(), |
| ]); |
| printOnFailure(result.stderr.toString()); |
| printOnFailure(result.stdout.toString()); |
| expect(result.exitCode, 0); |
| return result; |
| } |
| |
| await Future.wait([runInProcess(), runInProcess(), runInProcess()]); |
| }); |
| }); |
| |
| File? findLockFile(Uri tempUri) { |
| final lockFile = File.fromUri(tempUri.resolve('.lock')); |
| if (lockFile.existsSync()) { |
| final lockFileContents = lockFile.readAsStringSync(); |
| if (lockFileContents.isNotEmpty) { |
| // The process might have been killed in between creating the lock |
| // file and writing to it. |
| expect(lockFileContents, stringContainsInOrder(['Last acquired by'])); |
| } |
| return lockFile; |
| } |
| return null; |
| } |
| |
| test('Terminations unlock', timeout: longTimeout, () async { |
| await inTempDir((tempUri) async { |
| Future<int> runProcess({Duration? killAfter}) async { |
| final process = await Process.start(dartExecutable.toFilePath(), [ |
| packageUri |
| .resolve('test/locking/locking_test_helper.dart') |
| .toFilePath(), |
| tempUri.toFilePath(), |
| ]); |
| |
| final stdoutSub = process.stdout |
| .transform(systemEncoding.decoder) |
| .transform(const LineSplitter()) |
| .listen(logger.fine); |
| final stderrSub = process.stderr |
| .transform(systemEncoding.decoder) |
| .transform(const LineSplitter()) |
| .listen(logger.severe); |
| |
| Timer? timer; |
| if (killAfter != null) { |
| timer = Timer(killAfter, () async { |
| printOnFailure('killing process'); |
| process.kill(); |
| }); |
| } |
| final (exitCode, _, _) = await ( |
| process.exitCode, |
| stdoutSub.asFuture<void>(), |
| stderrSub.asFuture<void>(), |
| ).wait; |
| if (timer != null) { |
| timer.cancel(); |
| } |
| |
| return exitCode; |
| } |
| |
| // Kill process before it finishes. To check lock is properly released. |
| var milliseconds = 100; |
| while (findLockFile(tempUri) == null) { |
| final result = await runProcess( |
| killAfter: Duration(milliseconds: milliseconds), |
| ); |
| expect(result, isNot(0)); |
| milliseconds = max((milliseconds * 1.1).round(), milliseconds + 100); |
| } |
| expect(findLockFile(tempUri), isNotNull); |
| |
| final result2 = await runProcess(); |
| expect(result2, 0); |
| }); |
| }); |
| |
| test('Timeout exits process', timeout: longTimeout, () async { |
| await inTempDir((tempUri) async { |
| Future<void> runProcess({ |
| Duration? timeout, |
| bool expectTimeOut = false, |
| }) async { |
| final process = await Process.start(dartExecutable.toFilePath(), [ |
| packageUri |
| .resolve('test/locking/locking_test_helper.dart') |
| .toFilePath(), |
| tempUri.toFilePath(), |
| if (timeout != null) timeout.inMilliseconds.toString(), |
| ]); |
| |
| final stdoutSub = process.stdout |
| .transform(systemEncoding.decoder) |
| .transform(const LineSplitter()) |
| .listen(logger.fine); |
| final stderrSub = process.stderr |
| .transform(systemEncoding.decoder) |
| .transform(const LineSplitter()) |
| .listen(logger.severe); |
| |
| final (exitCode, _, _) = await ( |
| process.exitCode, |
| stdoutSub.asFuture<void>(), |
| stderrSub.asFuture<void>(), |
| ).wait; |
| |
| if (expectTimeOut) { |
| expect(exitCode, isNot(0)); |
| } else { |
| expect(exitCode, 0); |
| } |
| } |
| |
| await runProcess(); |
| |
| final lockFile = findLockFile(tempUri); |
| expect(lockFile, isNotNull); |
| lockFile!; |
| |
| final s = Stopwatch(); |
| s.start(); |
| await runProcess(); |
| s.stop(); |
| final oneTimeRun = s.elapsed; |
| printOnFailure('oneTimeRun: $oneTimeRun'); |
| // Some arbitrary time that the inner process will wait to try to grab the |
| // lock. At least a multiple of the magic constant of 50 milliseconds. |
| const helperProcessTimeout = Duration(milliseconds: 200); |
| printOnFailure('helperProcessTimeout: $helperProcessTimeout'); |
| // In a normal test run, the timer should always be cancelled. But pass |
| // some reasonable upper bound. |
| final timerTimeout = oneTimeRun * 10; |
| printOnFailure('timerTimeout: $timerTimeout'); |
| |
| final randomAccessFile = await lockFile.open(mode: FileMode.write); |
| final lock = await randomAccessFile.lock(FileLock.exclusive); |
| var helperCompletedFirst = false; |
| var timeoutCompletedFirst = false; |
| final timer = Timer(timerTimeout, () async { |
| printOnFailure('${DateTime.now()}: Timer expired.'); |
| if (!helperCompletedFirst) { |
| printOnFailure('${DateTime.now()}: timeoutCompletedFirst'); |
| timeoutCompletedFirst = true; |
| } |
| await lock.unlock(); |
| }); |
| await runProcess(timeout: helperProcessTimeout, expectTimeOut: true).then( |
| (v) async { |
| printOnFailure('${DateTime.now()}: Helper exited.'); |
| if (!timeoutCompletedFirst) { |
| printOnFailure('${DateTime.now()}: timeoutCompletedFirst'); |
| helperCompletedFirst = true; |
| } |
| timer.cancel(); |
| }, |
| ); |
| expect(helperCompletedFirst, isTrue); |
| expect(timeoutCompletedFirst, isFalse); |
| }); |
| }); |
| } |