blob: 218a9a4c951a160dfc498f5300edf68240cd67c6 [file] [log] [blame] [edit]
// 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);
});
});
}