diff --git a/cli/tests/unit/flock_test.ts b/cli/tests/unit/flock_test.ts index 13d09bcf56..6c4f1e90d0 100644 --- a/cli/tests/unit/flock_test.ts +++ b/cli/tests/unit/flock_test.ts @@ -1,102 +1,164 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. import { assertEquals, unitTest } from "./test_util.ts"; +import { readAll } from "../../../test_util/std/io/util.ts"; unitTest( { perms: { read: true, run: true, hrtime: true } }, async function flockFileSync() { - const path = "cli/tests/testdata/fixture.json"; - const script = (exclusive: boolean, wait: number) => ` - const { rid } = Deno.openSync("${path}"); - Deno.flockSync(rid, ${exclusive ? "true" : "false"}); - await new Promise(res => setTimeout(res, ${wait})); - Deno.funlockSync(rid); - `; - const run = (e: boolean, w: number) => - Deno.run({ cmd: [Deno.execPath(), "eval", "--unstable", script(e, w)] }); - const firstBlocksSecond = async ( - first: boolean, - second: boolean, - ): Promise => { - const firstPs = run(first, 1000); - await new Promise((res) => setTimeout(res, 250)); - const start = performance.now(); - const secondPs = run(second, 0); - await secondPs.status(); - const didBlock = (performance.now() - start) > 500; - firstPs.close(); - secondPs.close(); - return didBlock; - }; - - assertEquals( - await firstBlocksSecond(true, false), - true, - "exclusive blocks shared", - ); - assertEquals( - await firstBlocksSecond(false, true), - true, - "shared blocks exclusive", - ); - assertEquals( - await firstBlocksSecond(true, true), - true, - "exclusive blocks exclusive", - ); - assertEquals( - await firstBlocksSecond(false, false), - false, - "shared does not block shared", - ); + await runFlockTests({ sync: true }); }, ); unitTest( { perms: { read: true, run: true, hrtime: true } }, async function flockFileAsync() { - const path = "cli/tests/testdata/fixture.json"; - const script = (exclusive: boolean, wait: number) => ` - const { rid } = await Deno.open("${path}"); - await Deno.flock(rid, ${exclusive ? "true" : "false"}); - await new Promise(res => setTimeout(res, ${wait})); - await Deno.funlock(rid); - `; - const run = (e: boolean, w: number) => - Deno.run({ cmd: [Deno.execPath(), "eval", "--unstable", script(e, w)] }); - const firstBlocksSecond = async ( - first: boolean, - second: boolean, - ): Promise => { - const firstPs = run(first, 1000); - await new Promise((res) => setTimeout(res, 250)); - const start = performance.now(); - const secondPs = run(second, 0); - await secondPs.status(); - const didBlock = (performance.now() - start) > 500; - firstPs.close(); - secondPs.close(); - return didBlock; - }; - - assertEquals( - await firstBlocksSecond(true, false), - true, - "exclusive blocks shared", - ); - assertEquals( - await firstBlocksSecond(false, true), - true, - "shared blocks exclusive", - ); - assertEquals( - await firstBlocksSecond(true, true), - true, - "exclusive blocks exclusive", - ); - assertEquals( - await firstBlocksSecond(false, false), - false, - "shared does not block shared", - ); + 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, + }), + false, + "shared does not block shared", + ); +} + +async function checkFirstBlocksSecond(opts: { + firstExclusive: boolean; + secondExclusive: boolean; + sync: 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)); + + // wait for both processes to signal that they're ready + await Promise.all([firstProcess.waitSignal(), secondProcess.waitSignal()]); + + // signal to the first process to enter the lock + await firstProcess.signal(); + await firstProcess.waitSignal(); // entering signal + await firstProcess.waitSignal(); // entered signal + await sleep(20); + // signal the second to enter the lock + await secondProcess.signal(); + await secondProcess.waitSignal(); // entering signal + await sleep(20); + // signal to the first to exit the lock + await firstProcess.signal(); + await sleep(20); + // signal to the second to exit the lock + await secondProcess.waitSignal(); // entered signal + await secondProcess.signal(); + // collect the remaining JSON output of both processes + const firstPsTimes = await firstProcess.getTimes(); + const secondPsTimes = await secondProcess.getTimes(); + return firstPsTimes.exitTime < secondPsTimes.enterTime; + } finally { + firstProcess.close(); + secondProcess.close(); + } +} + +function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) { + const path = "cli/tests/testdata/fixture.json"; + 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 (do double the + // windows clock resolution) + const exitTime = new Date().getTime(); + await new Promise((resolve) => setTimeout(resolve, 30)); + + // release the lock + ${opts.sync ? "Deno.funlockSync(rid);" : "await Deno.funlock(rid);"} + + // output the enter and exit time + console.log(JSON.stringify({ enterTime, exitTime })); +`; + + const process = Deno.run({ + cmd: [Deno.execPath(), "eval", "--unstable", scriptText], + stdout: "piped", + stdin: "piped", + }); + + return { + waitSignal: () => process.stdout.read(new Uint8Array(1)), + signal: () => process.stdin.write(new Uint8Array(1)), + getTimes: async () => { + const outputBytes = await readAll(process.stdout); + const text = new TextDecoder().decode(outputBytes); + return JSON.parse(text) as { + enterTime: number; + exitTime: number; + }; + }, + close: () => { + process.stdout.close(); + process.stdin.close(); + process.close(); + }, + }; +}