deno/tests/unit/flock_test.ts
Matt Mastracci f5e46c9bf2
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.

This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.

While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).

And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.

For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
2024-02-10 20:22:13 +00:00

198 lines
5.1 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "./test_util.ts";
Deno.test(
{ permissions: { read: true, run: true, hrtime: true } },
async function flockFileSync() {
await runFlockTests({ sync: true });
},
);
Deno.test(
{ permissions: { read: true, run: true, hrtime: true } },
async function flockFileAsync() {
await runFlockTests({ sync: false });
},
);
async function runFlockTests(opts: { sync: boolean }) {
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: true,
secondExclusive: false,
sync: opts.sync,
}),
true,
"exclusive blocks shared",
);
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: false,
secondExclusive: true,
sync: opts.sync,
}),
true,
"shared blocks exclusive",
);
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: true,
secondExclusive: true,
sync: opts.sync,
}),
true,
"exclusive blocks exclusive",
);
assertEquals(
await checkFirstBlocksSecond({
firstExclusive: false,
secondExclusive: false,
sync: opts.sync,
// need to wait for both to enter the lock to prevent the case where the
// first process enters and exits the lock before the second even enters
waitBothEnteredLock: true,
}),
false,
"shared does not block shared",
);
}
async function checkFirstBlocksSecond(opts: {
firstExclusive: boolean;
secondExclusive: boolean;
sync: boolean;
waitBothEnteredLock?: boolean;
}) {
const firstProcess = runFlockTestProcess({
exclusive: opts.firstExclusive,
sync: opts.sync,
});
const secondProcess = runFlockTestProcess({
exclusive: opts.secondExclusive,
sync: opts.sync,
});
try {
const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
await Promise.all([
firstProcess.waitStartup(),
secondProcess.waitStartup(),
]);
await firstProcess.enterLock();
await firstProcess.waitEnterLock();
await secondProcess.enterLock();
await sleep(100);
if (!opts.waitBothEnteredLock) {
await firstProcess.exitLock();
}
await secondProcess.waitEnterLock();
if (opts.waitBothEnteredLock) {
await firstProcess.exitLock();
}
await secondProcess.exitLock();
// collect the final output
const firstPsTimes = await firstProcess.getTimes();
const secondPsTimes = await secondProcess.getTimes();
return firstPsTimes.exitTime < secondPsTimes.enterTime;
} finally {
await firstProcess.close();
await secondProcess.close();
}
}
function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) {
const path = "tests/testdata/assets/lock_target.txt";
const scriptText = `
const { rid } = Deno.openSync("${path}");
// ready signal
Deno.stdout.writeSync(new Uint8Array(1));
// wait for enter lock signal
Deno.stdin.readSync(new Uint8Array(1));
// entering signal
Deno.stdout.writeSync(new Uint8Array(1));
// lock and record the entry time
${
opts.sync
? `Deno.flockSync(rid, ${opts.exclusive ? "true" : "false"});`
: `await Deno.flock(rid, ${opts.exclusive ? "true" : "false"});`
}
const enterTime = new Date().getTime();
// entered signal
Deno.stdout.writeSync(new Uint8Array(1));
// wait for exit lock signal
Deno.stdin.readSync(new Uint8Array(1));
// record the exit time and wait a little bit before releasing
// the lock so that the enter time of the next process doesn't
// occur at the same time as this exit time
const exitTime = new Date().getTime();
await new Promise(resolve => setTimeout(resolve, 100));
// release the lock
${opts.sync ? "Deno.funlockSync(rid);" : "await Deno.funlock(rid);"}
// exited signal
Deno.stdout.writeSync(new Uint8Array(1));
// output the enter and exit time
console.log(JSON.stringify({ enterTime, exitTime }));
`;
const process = new Deno.Command(Deno.execPath(), {
args: ["eval", "--unstable", scriptText],
stdin: "piped",
stdout: "piped",
stderr: "null",
}).spawn();
const waitSignal = async () => {
const reader = process.stdout.getReader({ mode: "byob" });
await reader.read(new Uint8Array(1));
reader.releaseLock();
};
const signal = async () => {
const writer = process.stdin.getWriter();
await writer.write(new Uint8Array(1));
writer.releaseLock();
};
return {
async waitStartup() {
await waitSignal();
},
async enterLock() {
await signal();
await waitSignal(); // entering signal
},
async waitEnterLock() {
await waitSignal();
},
async exitLock() {
await signal();
await waitSignal();
},
getTimes: async () => {
const { stdout } = await process.output();
const text = new TextDecoder().decode(stdout);
return JSON.parse(text) as {
enterTime: number;
exitTime: number;
};
},
close: async () => {
await process.status;
await process.stdin.close();
},
};
}