Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions denops/dpp/tests/linkpath_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { assertEquals } from "@std/assert/equals";
import { join } from "@std/path/join";
import { linkPath } from "../utils.ts";

Deno.test(
"linkPath: symlinks a file on non-Windows",
{ ignore: Deno.build.os === "windows" },
async () => {
const tmpDir = await Deno.makeTempDir();
try {
const srcFile = join(tmpDir, "src.txt");
const destFile = join(tmpDir, "dest.txt");
await Deno.writeTextFile(srcFile, "hello");

await linkPath(false, srcFile, destFile);

const stat = await Deno.stat(destFile);
assertEquals(stat.isFile, true);
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
},
);

Deno.test(
"linkPath: skips existing dest file",
{ ignore: Deno.build.os === "windows" },
async () => {
const tmpDir = await Deno.makeTempDir();
try {
const srcFile = join(tmpDir, "src.txt");
const destFile = join(tmpDir, "dest.txt");
await Deno.writeTextFile(srcFile, "hello");
await Deno.writeTextFile(destFile, "existing");

// Should not throw even though dest already exists.
await linkPath(false, srcFile, destFile);

const content = await Deno.readTextFile(destFile);
assertEquals(content, "existing");
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
},
);

Deno.test(
"linkPath: recursively symlinks directory contents",
{ ignore: Deno.build.os === "windows" },
async () => {
const tmpDir = await Deno.makeTempDir();
try {
const srcDir = join(tmpDir, "src");
const destDir = join(tmpDir, "dest");
await Deno.mkdir(srcDir);
await Deno.writeTextFile(join(srcDir, "a.txt"), "a");
await Deno.writeTextFile(join(srcDir, "b.txt"), "b");

await linkPath(false, srcDir, destDir);

const aStat = await Deno.stat(join(destDir, "a.txt"));
const bStat = await Deno.stat(join(destDir, "b.txt"));
assertEquals(aStat.isFile, true);
assertEquals(bStat.isFile, true);
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
},
);

Deno.test(
"linkPath: recursively symlinks nested subdirectories",
{ ignore: Deno.build.os === "windows" },
async () => {
const tmpDir = await Deno.makeTempDir();
try {
const srcDir = join(tmpDir, "src");
const subDir = join(srcDir, "sub");
const destDir = join(tmpDir, "dest");
await Deno.mkdir(subDir, { recursive: true });
await Deno.writeTextFile(join(subDir, "c.txt"), "c");

await linkPath(false, srcDir, destDir);

const cStat = await Deno.stat(join(destDir, "sub", "c.txt"));
assertEquals(cStat.isFile, true);
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
},
);

Deno.test(
"linkPath: no-ops when src does not exist",
{ ignore: Deno.build.os === "windows" },
async () => {
const tmpDir = await Deno.makeTempDir();
try {
const nonExistent = join(tmpDir, "nonexistent.txt");
const destFile = join(tmpDir, "dest.txt");

// Should not throw even when src is missing.
await linkPath(false, nonExistent, destFile);

// dest must not have been created.
let created = false;
try {
await Deno.stat(destFile);
created = true;
} catch (_) {
// expected
}
assertEquals(created, false);
} finally {
await Deno.remove(tmpDir, { recursive: true });
}
},
);
31 changes: 28 additions & 3 deletions denops/dpp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,23 @@
return null;
}

const LINK_BATCH_SIZE = 32;

export async function linkPath(hasWindows: boolean, src: string, dest: string) {
if (await isDirectory(src)) {
await linkPathInternal(hasWindows, src, dest);
}

async function linkPathInternal(
hasWindows: boolean,
src: string,
dest: string,
) {
const srcStat = await safeStat(src);
if (!srcStat) {
return;
}

if (srcStat.isDirectory) {
if (!await safeStat(dest)) {
// Not exists directory
await Deno.mkdir(dest, { recursive: true });
Expand All @@ -95,9 +110,19 @@
return;
}

// Recursive
// Collect all entries first, then process in parallel batches.
const entries: string[] = [];
for await (const entry of Deno.readDir(src)) {
await linkPath(hasWindows, join(src, entry.name), join(dest, entry.name));
entries.push(entry.name);
}

for (let i = 0; i < entries.length; i += LINK_BATCH_SIZE) {
const batch = entries.slice(i, i + LINK_BATCH_SIZE);
await Promise.allSettled(
batch.map((name) =>
linkPathInternal(hasWindows, join(src, name), join(dest, name))
),
);
}
} else {
if (await safeStat(dest)) {
Expand Down Expand Up @@ -296,7 +321,7 @@
}
return await importer.import(`${url}#${suffix}`);
} else {
return await import(`${url}#${suffix}`);

Check warning on line 324 in denops/dpp/utils.ts

View workflow job for this annotation

GitHub Actions / deno-test-publish

unable to analyze dynamic import
}
}

Expand Down
Loading