| // 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: .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); | |
| }); | |
| }); | |
| } |