From 8535d32ef236c10cb5cffb70c2e1f260493d0af5 Mon Sep 17 00:00:00 2001 From: Matthew McEachen Date: Fri, 22 Nov 2024 11:04:41 -0800 Subject: [PATCH] test: enhance test coverage for filesystem metadata - Added tests for Linux device disk handling. - Improved assertions in fs_metadata.test.ts for better accuracy and robustness. --- src/__tests__/fs_metadata.test.ts | 15 ++--- src/__tests__/linux_dev_disk.test.ts | 84 ++++++++++++++++++++++++++++ src/linux/dev_disk.ts | 11 ++-- src/units.ts | 17 ++++++ 4 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/linux_dev_disk.test.ts create mode 100644 src/units.ts diff --git a/src/__tests__/fs_metadata.test.ts b/src/__tests__/fs_metadata.test.ts index 25890f6..b92e031 100644 --- a/src/__tests__/fs_metadata.test.ts +++ b/src/__tests__/fs_metadata.test.ts @@ -1,18 +1,15 @@ // src/__tests__/fs_metadata.test.ts import { jest } from "@jest/globals"; -import { platform } from "node:os"; import { times } from "../array.js"; import { TimeoutError } from "../async.js"; import { getVolumeMetadata, getVolumeMountPoints } from "../index.js"; import { omit } from "../object.js"; +import { isLinux, isMacOS, isWindows } from "../platform.js"; import { pickRandom, randomLetter, randomLetters, shuffle } from "../random.js"; import { sortByLocale } from "../string.js"; import { assertMetadata } from "../test-utils/assert.js"; - -const isWindows = platform() === "win32"; -const isMacOS = platform() === "darwin"; -const isLinux = platform() === "linux"; +import { MiB } from "../units.js"; describe("Filesystem Metadata", () => { jest.setTimeout(15_000); @@ -142,8 +139,12 @@ describe("Filesystem Metadata", () => { expect(omit(ea, "available", "used")).toEqual( omit(expected, "available", "used"), ); - expect(ea.available).toBeCloseTo(expected.available, 5); - expect(ea.used).toBeCloseTo(expected.used, 5); + // REMEMBER: NEVER USE toBeCloseTo -- the api is bonkers and only applicable for fractional numbers + expect(ea.available).toBeWithin( + expected.available - MiB, + expected.available + MiB, + ); + expect(ea.used).toBeWithin(expected.used - MiB, expected.used + MiB); } } }); diff --git a/src/__tests__/linux_dev_disk.test.ts b/src/__tests__/linux_dev_disk.test.ts new file mode 100644 index 0000000..5ecff40 --- /dev/null +++ b/src/__tests__/linux_dev_disk.test.ts @@ -0,0 +1,84 @@ +import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { getBasenameLinkedTo } from "../linux/dev_disk.js"; +import { describePlatform } from "../test-utils/platform.js"; + +/* +Rather than fooling around with mocks, we're going to create a temporary +directory structure that mimics the real /dev/disk/by-uuid and +/dev/disk/by-label directories. We'll create some device files and symlinks to +test the getBasenameLinkedTo function. + +- Create base temp directory: $tmp = /tmp/test-dev-disk + - Create subdirectories: $tmp/dev/disk/by-uuid and /dev/disk/by-label + - Create some device files: $tmp/dev/sda1, /dev/sda2 + - Create symlinks in by-uuid and by-label directories that match relative + paths in /dev/disk/by-*: + + - by-uuid/ABC-DEF -> ../../sda2 + - by-uuid/123-456 -> ../../sda1 + - by-label/ROOT -> ../../sda1 + - by-label/DATA -> ../../sda2 +*/ + +describePlatform("linux")("dev_disk", () => { + let tempDir: string; + let devDir: string; + let byUuidDir: string; + let byLabelDir: string; + + beforeAll(async () => { + // Create base temp directory + tempDir = await mkdtemp(join(tmpdir(), "test-dev-disk-")); + + // Create directory structure + devDir = join(tempDir, "dev"); + byUuidDir = join(devDir, "disk", "by-uuid"); + byLabelDir = join(devDir, "disk", "by-label"); + + await mkdir(devDir, { recursive: true }); + await mkdir(byUuidDir, { recursive: true }); + await mkdir(byLabelDir, { recursive: true }); + + // Create some device files + for (const device of ["sda1", "sda2"]) { + const devicePath = join(devDir, device); + await writeFile(devicePath, "# " + devicePath); + } + + // Create symlinks + await symlink("../../sda1", join(byUuidDir, "123-456")); + await symlink("../../sda2", join(byUuidDir, "789-ABC")); + await symlink("../../sda1", join(byLabelDir, "ROOT")); + await symlink("../../sda2", join(byLabelDir, "1tb\\x20\\x28test\\x29")); + // Create a broken symlink + await symlink("../../sdX1", join(byUuidDir, "BAD-LINK")); + }); + + afterAll(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + it("should find UUID for existing device", async () => { + const result = await getBasenameLinkedTo(byUuidDir, join(devDir, "sda1")); + expect(result).toBe("123-456"); + }); + + it("should find label for existing device", async () => { + const result = await getBasenameLinkedTo(byLabelDir, join(devDir, "sda2")); + expect(result).toBe("1tb (test)"); + }); + + it("should return undefined for non-existent device", async () => { + const result = await getBasenameLinkedTo(byUuidDir, join(devDir, "sdz9")); + expect(result).toBeUndefined(); + }); + + it("should handle empty directory", async () => { + const emptyDir = join(devDir, "empty"); + await mkdir(emptyDir); + const result = await getBasenameLinkedTo(emptyDir, join(devDir, "sda1")); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/linux/dev_disk.ts b/src/linux/dev_disk.ts index f47d990..9700201 100644 --- a/src/linux/dev_disk.ts +++ b/src/linux/dev_disk.ts @@ -1,6 +1,7 @@ import { Dirent } from "node:fs"; import { readdir, readlink } from "node:fs/promises"; import { join, resolve } from "node:path"; +import { decodeEscapeSequences } from "../string.js"; /** * Gets the UUID from symlinks for a given device path asynchronously @@ -24,26 +25,28 @@ export function getLabelFromDevDisk(devicePath: string) { ); } -async function getBasenameLinkedTo( +// only exposed for tests +export async function getBasenameLinkedTo( linkDir: string, linkPath: string, ): Promise { for await (const ea of readLinks(linkDir)) { if (ea.linkTarget === linkPath) { - return ea.dirent.name; + // Expect the symlink to be named like '1tb\x20\x28test\x29' + return decodeEscapeSequences(ea.dirent.name); } } return; } -// only exposed for test mocking -export async function* readLinks( +async function* readLinks( directory: string, ): AsyncGenerator<{ dirent: Dirent; linkTarget: string }, void, unknown> { for (const dirent of await readdir(directory, { withFileTypes: true })) { if (dirent.isSymbolicLink()) { try { const linkTarget = resolve( + directory, await readlink(join(directory, dirent.name)), ); yield { dirent, linkTarget }; diff --git a/src/units.ts b/src/units.ts new file mode 100644 index 0000000..5f303cf --- /dev/null +++ b/src/units.ts @@ -0,0 +1,17 @@ +/** + * KiB = 1024 bytes + * @see https://en.wikipedia.org/wiki/Kibibyte + */ +export const KiB = 1024; + +/** + * MiB = 1024 KiB + * @see https://en.wikipedia.org/wiki/Mebibyte + */ +export const MiB = 1024 * KiB; + +/** + * GiB = 1024 MiB + * @see https://en.wikipedia.org/wiki/Gibibyte + */ +export const GiB = 1024 * MiB;