deno/tests/unit_node/_test_utils.ts
Daniel Osvaldo Rahmanto 432761aac5
Some checks are pending
ci / build libs (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions
fix(ext/node): fs.stat and fs.statSync compatibility (#30637)
Closes #30570

Changes in this PR:
- Implement `ino`, `nlink`, and `blocks` properties of `Deno.FileInfo`
on Windows. These changes are automatically reflected to the
corresponding node stat function. In order to do so, I had to tinker
with the
[createByteStruct](a3a904da14/ext/fs/30_fs.js (L297))
function to create another optional int type, apart from `?u64`. It's
common for small sized files on Windows (particularly NTFS file system)
to have a `Stats.blocks` property of 0, and currently all 0 values with
type `?u64` will be coerced into `null` by `createByteStruct`.
- Refactor the `BigIntStats` and `Stats` class, to use the same class
with Node.js that are provided from
[utils.mjs](7f8e488c36/ext/node/polyfills/internal/fs/utils.mjs (L577)).
Also ensures that all properties are not `null` or `undefined`.
- Addresses the `prefer-primordials` lint rule.
2025-09-15 15:29:30 +02:00

352 lines
9.4 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
import { BigIntStats, Stats } from "node:fs";
import {
assert,
assertEquals,
assertNotStrictEquals,
assertStrictEquals,
assertStringIncludes,
} from "@std/assert";
/** Asserts that an error thrown in a callback will not be wrongly caught. */
export async function assertCallbackErrorUncaught(
{ prelude, invocation, cleanup }: {
/** Any code which needs to run before the actual invocation (notably, any import statements). */
prelude?: string;
/**
* The start of the invocation of the function, e.g. `open("foo.txt", `.
* The callback will be added after it.
*/
invocation: string;
/** Called after the subprocess is finished but before running the assertions, e.g. to clean up created files. */
cleanup?: () => Promise<void> | void;
},
) {
// Since the error has to be uncaught, and that will kill the Deno process,
// the only way to test this is to spawn a subprocess.
const p = new Deno.Command(Deno.execPath(), {
args: [
"eval",
`${prelude ?? ""}
${invocation}(err) => {
// If the bug is present and the callback is called again with an error,
// don't throw another error, so if the subprocess fails we know it had the correct behaviour.
if (!err) throw new Error("success");
});`,
],
stderr: "piped",
});
const { stderr, success } = await p.output();
const error = new TextDecoder().decode(stderr);
await cleanup?.();
assert(!success);
assertStringIncludes(error, "Error: success");
}
const numberFields = [
"dev",
"mode",
"nlink",
"uid",
"gid",
"rdev",
"blksize",
"ino",
"size",
"blocks",
"atimeMs",
"mtimeMs",
"ctimeMs",
"birthtimeMs",
];
const bigintFields = [
...numberFields,
"atimeNs",
"mtimeNs",
"ctimeNs",
"birthtimeNs",
];
const dateFields = [
"atime",
"mtime",
"ctime",
"birthtime",
];
export function assertStats(actual: Stats, expected: Deno.FileInfo) {
[...numberFields, ...dateFields].forEach(function (k) {
assert(k in actual, `${k} should be in Stats`);
assertNotStrictEquals(
actual[k as keyof Stats],
undefined,
`${k} should not be undefined`,
);
assertNotStrictEquals(
actual[k as keyof Stats],
null,
`${k} should not be null`,
);
});
numberFields.forEach((k) => {
assertStrictEquals(
typeof actual[k as keyof Stats],
"number",
`${k} should be a number`,
);
});
dateFields.forEach((k) => {
assert(actual[k as keyof Stats] instanceof Date, `${k} should be a Date`);
});
// Some properties from Deno.FileInfo may be null,
// while node:fs Stats always has number / Date properties.
// So we only check properties that are not null in Deno.FileInfo.
assertEquals(actual.dev, expected.dev);
if (expected.gid) assertEquals(actual.gid, expected.gid, "Stats.gid");
assertEquals(actual.size, expected.size, "Stats.size");
if (expected.blksize) {
assertEquals(actual.blksize, expected.blksize, "Stats.blksize");
}
if (expected.blocks) {
assertEquals(actual.blocks, expected.blocks, "Stats.blocks");
}
if (expected.ino) assertEquals(actual.ino, expected.ino, "Stats.ino");
if (expected.mode) assertEquals(actual.mode, expected.mode, "Stats.mode");
if (expected.nlink) assertEquals(actual.nlink, expected.nlink, "Stats.nlink");
if (expected.rdev) assertEquals(actual.rdev, expected.rdev, "Stats.rdev");
if (expected.uid) assertEquals(actual.uid, expected.uid, "Stats.uid");
if (expected.atime?.getTime()) {
assertEquals(
actual.atime.getTime(),
expected.atime.getTime(),
"Stats.atime",
);
assertEquals(actual.atimeMs, expected.atime.getTime(), "Stats.atimeMs");
}
if (expected.mtime?.getTime()) {
assertEquals(
actual.mtime.getTime(),
expected.mtime.getTime(),
"Stats.mtime",
);
assertEquals(actual.mtimeMs, expected.mtime.getTime(), "Stats.mtimeMs");
}
if (expected.birthtime?.getTime()) {
assertEquals(
actual.birthtime.getTime(),
expected.birthtime.getTime(),
"Stats.birthtime",
);
assertEquals(
actual.birthtimeMs,
expected.birthtime.getTime(),
"Stats.birthtimeMs",
);
}
if (expected.ctime?.getTime()) {
assertEquals(
actual.ctime.getTime(),
expected.ctime.getTime(),
"Stats.ctime",
);
assertEquals(actual.ctimeMs, expected.ctime.getTime(), "Stats.ctimeMs");
}
assertEquals(actual.isFile(), expected.isFile, "Stats.isFile");
assertEquals(actual.isDirectory(), expected.isDirectory, "Stats.isDirectory");
assertEquals(
actual.isSymbolicLink(),
expected.isSymlink,
"Stats.isSymbolicLink",
);
assertEquals(
actual.isBlockDevice(),
Deno.build.os === "windows" ? false : expected.isBlockDevice,
"Stats.isBlockDevice",
);
assertEquals(
actual.isFIFO(),
Deno.build.os === "windows" ? false : expected.isFifo,
"Stats.isFIFO",
);
assertEquals(
actual.isCharacterDevice(),
Deno.build.os === "windows" ? false : expected.isCharDevice,
"Stats.isCharacterDevice",
);
assertEquals(
actual.isSocket(),
Deno.build.os === "windows" ? false : expected.isSocket,
"Stats.isSocket",
);
}
function toBigInt(num?: number | null) {
if (num === undefined || num === null) return null;
return BigInt(num);
}
export function assertStatsBigInt(
actual: BigIntStats,
expected: Deno.FileInfo,
) {
[...bigintFields, ...dateFields].forEach(function (k) {
assert(k in actual, `${k} should be in BigIntStats`);
assertNotStrictEquals(
actual[k as keyof BigIntStats],
undefined,
`${k} should not be undefined`,
);
assertNotStrictEquals(
actual[k as keyof BigIntStats],
null,
`${k} should not be null`,
);
});
bigintFields.forEach((k) => {
assertStrictEquals(
typeof actual[k as keyof BigIntStats],
"bigint",
`${k} should be a bigint`,
);
});
dateFields.forEach((k) => {
assert(
actual[k as keyof BigIntStats] instanceof Date,
`${k} should be a Date`,
);
});
// Some properties from Deno.FileInfo may be null,
// while node:fs BigIntStats always has bigint / Date properties.
// So we only check properties that are not null in Deno.FileInfo.
assertEquals(actual.dev, toBigInt(expected.dev), "BigIntStats.dev");
if (expected.gid) {
assertEquals(actual.gid, toBigInt(expected.gid), "BigIntStats.gid");
}
assertEquals(actual.size, toBigInt(expected.size), "BigIntStats.size");
if (expected.blksize) {
assertEquals(
actual.blksize,
toBigInt(expected.blksize),
"BigIntStats.blksize",
);
}
if (expected.blocks) {
assertEquals(
actual.blocks,
toBigInt(expected.blocks),
"BigIntStats.blocks",
);
}
if (expected.ino) {
assertEquals(actual.ino, toBigInt(expected.ino), "BigIntStats.ino");
}
if (expected.mode) {
assertEquals(actual.mode, toBigInt(expected.mode), "BigIntStats.mode");
}
if (expected.nlink) {
assertEquals(actual.nlink, toBigInt(expected.nlink), "BigIntStats.nlink");
}
if (expected.rdev) {
assertEquals(actual.rdev, toBigInt(expected.rdev), "BigIntStats.rdev");
}
if (expected.uid) {
assertEquals(actual.uid, toBigInt(expected.uid), "BigIntStats.uid");
}
if (expected.atime?.getTime()) {
assertEquals(
actual.atime.getTime(),
expected.atime.getTime(),
"BigIntStats.atime",
);
assertEquals(
actual.atimeMs,
toBigInt(expected.atime.getTime()),
"BigIntStats.atimeMs",
);
assertEquals(
actual.atimeNs,
toBigInt(expected.atime.getTime()) as bigint * 1000000n,
"BigIntStats.atimeNs",
);
}
if (expected.mtime?.getTime()) {
assertEquals(
actual.mtime.getTime(),
expected.mtime.getTime(),
"BigIntStats.mtime",
);
assertEquals(
actual.mtimeMs,
toBigInt(expected.mtime.getTime()),
"BigIntStats.mtimeMs",
);
assertEquals(
actual.mtimeNs,
toBigInt(expected.mtime.getTime()) as bigint * 1000000n,
"BigIntStats.mtimeNs",
);
}
if (expected.birthtime?.getTime()) {
assertEquals(
actual.birthtime.getTime(),
expected.birthtime.getTime(),
"BigIntStats.birthtime",
);
assertEquals(
actual.birthtimeMs,
toBigInt(expected.birthtime.getTime()),
"BigIntStats.birthtimeMs",
);
assertEquals(
actual.birthtimeNs,
toBigInt(expected.birthtime.getTime()) as bigint * 1000000n,
"BigIntStats.birthtimeNs",
);
}
if (expected.ctime?.getTime()) {
assertEquals(
actual.ctime.getTime(),
expected.ctime.getTime(),
"BigIntStats.ctime",
);
assertEquals(
actual.ctimeMs,
toBigInt(expected.ctime.getTime()),
"BigIntStats.ctimeMs",
);
assertEquals(
actual.ctimeNs,
toBigInt(expected.ctime.getTime()) as bigint * 1000000n,
"BigIntStats.ctimeNs",
);
}
assertEquals(
actual.isBlockDevice(),
Deno.build.os === "windows" ? false : expected.isBlockDevice,
"BigIntStats.isBlockDevice",
);
assertEquals(
actual.isFIFO(),
Deno.build.os === "windows" ? false : expected.isFifo,
"BigIntStats.isFIFO",
);
assertEquals(
actual.isCharacterDevice(),
Deno.build.os === "windows" ? false : expected.isCharDevice,
"BigIntStats.isCharacterDevice",
);
assertEquals(
actual.isSocket(),
Deno.build.os === "windows" ? false : expected.isSocket,
"BigIntStats.isSocket",
);
}