From 3b3b4da2f6b9f98c04a5b32526665efa42e4703f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:35:56 -0700 Subject: [PATCH] Use overlay FS when building projects (#144) --- .vscode/tasks.json | 16 ++- src/main.ts | 205 +++++++++++++++-------------------- src/utils/execUtils.ts | 1 + src/utils/gitUtils.ts | 5 +- src/utils/installPackages.ts | 7 ++ src/utils/overlayFS.ts | 174 +++++++++++++++++++++++++++++ test/main.test.ts | 3 +- 7 files changed, 287 insertions(+), 124 deletions(-) create mode 100644 src/utils/overlayFS.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d38de78..80f5833 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -23,13 +23,23 @@ "type": "npm", "script": "build", "group": { - "kind": "build", - "isDefault": true + "kind": "build" }, "problemMatcher": [], "label": "npm: build", "detail": "tsc -b ." }, + { + "label": "tsc: watch ./src", + "type": "shell", + "command": "node", + "args": ["${workspaceFolder}/node_modules/typescript/lib/tsc.js", "--build", ".", "--watch"], + "group": "build", + "isBackground": true, + "problemMatcher": [ + "$tsc-watch" + ] + }, { "label": "Clean all", "type": "shell", @@ -39,4 +49,4 @@ } }, ] -} \ No newline at end of file +} diff --git a/src/main.ts b/src/main.ts index 0222dc8..b9aca04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import path = require("path"); import mdEscape = require("markdown-escape"); import randomSeed = require("random-seed"); import { getErrorMessageFromStack, getHash, getHashForStack } from "./utils/hashStackTrace"; +import { createCopyingOverlayFS, createTempOverlayFS, OverlayBaseFS } from "./utils/overlayFS"; interface Params { /** @@ -100,7 +101,6 @@ interface Summary { oldTsEntrypointPath: string; rawErrorArtifactPath: string; replayScript: string; - downloadDir: string; replayScriptArtifactPath: string; replayScriptName: string; commit?: string; @@ -207,35 +207,36 @@ async function getTsServerRepoResult( userTestsDir: string, oldTsServerPath: string, newTsServerPath: string, - downloadDir: string, + downloadDir: OverlayBaseFS, replayScriptArtifactPath: string, rawErrorArtifactPath: string, diagnosticOutput: boolean, isPr: boolean, ): Promise { - if (!await cloneRepo(repo, userTestsDir, downloadDir, diagnosticOutput)) { + if (!await cloneRepo(repo, userTestsDir, downloadDir.path, diagnosticOutput)) { return { status: "Git clone failed" }; } - const repoDir = path.join(downloadDir, repo.name); + const repoDir = path.join(downloadDir.path, repo.name); const monorepoPackages = await getMonorepoPackages(repoDir); // Presumably, people occasionally browse repos without installing the packages first const installCommands = (prng.random() > 0.2) && monorepoPackages - ? (await installPackagesAndGetCommands(repo, downloadDir, repoDir, monorepoPackages, /*cleanOnFailure*/ true, diagnosticOutput))! + ? (await installPackagesAndGetCommands(repo, downloadDir.path, repoDir, monorepoPackages, /*cleanOnFailure*/ true, diagnosticOutput))! : []; const replayScriptName = path.basename(replayScriptArtifactPath); - const replayScriptPath = path.join(downloadDir, replayScriptName); + const replayScriptPath = path.join(downloadDir.path, replayScriptName); const rawErrorName = path.basename(rawErrorArtifactPath); - const rawErrorPath = path.join(downloadDir, rawErrorName); + const rawErrorPath = path.join(downloadDir.path, rawErrorName); const lsStart = performance.now(); try { console.log(`Testing with ${newTsServerPath} (new)`); const newSpawnResult = await spawnWithTimeoutAsync(repoDir, process.argv[0], [path.join(__dirname, "utils", "exerciseServer.js"), repoDir, replayScriptPath, newTsServerPath, diagnosticOutput.toString(), prng.string(10)], executionTimeout); + if (!newSpawnResult) { // CONSIDER: It might be interesting to treat timeouts as failures, but they'd be harder to baseline and more likely to have flaky repros console.log(`New server timed out after ${executionTimeout} ms`); @@ -285,7 +286,7 @@ async function getTsServerRepoResult( } console.log(`Testing with ${oldTsServerPath} (old)`); - const oldSpawnResult = await spawnWithTimeoutAsync(repoDir, process.argv[0], [path.join(__dirname, "..", "node_modules", "@typescript", "server-replay", "replay.js"), repoDir, replayScriptPath, oldTsServerPath, "-u"], executionTimeout); + const oldSpawnResult = await spawnWithTimeoutAsync(repoDir, process.argv[0], [path.join(__dirname, "..", "node_modules", "@typescript", "server-replay", "replay.js"), repoDir, replayScriptPath, oldTsServerPath, "-u"], executionTimeout);; if (diagnosticOutput && oldSpawnResult) { console.log("Raw spawn results (old):"); @@ -468,7 +469,7 @@ ${summary.replayScript} text += "
  • Install packages (exact steps are below, but it might be easier to follow the repo readme)
      \n"; } for (const command of summary.tsServerResult.installCommands) { - text += `
    1. In dir ${path.relative(summary.downloadDir, command.directory)}, run ${command.tool} ${command.arguments.join(" ")}
    2. \n`; + text += `
    3. In dir ${command.prettyDirectory}, run ${command.tool} ${command.arguments.join(" ")}
    4. \n`; } if (summary.tsServerResult.installCommands.length > 1) { text += "
    \n"; @@ -500,26 +501,35 @@ export async function getTscRepoResult( * 2) Errors are expected when building with the old tsc and we're specifically interested in changes (user tests) */ buildWithNewWhenOldFails: boolean, - downloadDir: string, - diagnosticOutput: boolean): Promise { + downloadDir: OverlayBaseFS, + diagnosticOutput: boolean, +): Promise { - if (!await cloneRepo(repo, userTestsDir, downloadDir, diagnosticOutput)) { + if (!await cloneRepo(repo, userTestsDir, downloadDir.path, diagnosticOutput)) { return { status: "Git clone failed" }; } - const repoDir = path.join(downloadDir, repo.name); - const monorepoPackages = await getMonorepoPackages(repoDir); + const baseRepoDir = path.join(downloadDir.path, repo.name); + const monorepoPackages = await getMonorepoPackages(baseRepoDir); - if (!monorepoPackages || !await installPackagesAndGetCommands(repo, downloadDir, repoDir, monorepoPackages, /*cleanOnFailure*/ false, diagnosticOutput)) { + if (!monorepoPackages || !await installPackagesAndGetCommands(repo, downloadDir.path, baseRepoDir, monorepoPackages, /*cleanOnFailure*/ false, diagnosticOutput)) { return { status: "Package install failed" }; } + const relativeMonorepoPackages = monorepoPackages.map(p => path.relative(baseRepoDir, path.resolve(baseRepoDir, p))); + const isUserTestRepo = !repo.url; const buildStart = performance.now(); try { console.log(`Building with ${oldTscPath} (old)`); - const oldErrors = await ge.buildAndGetErrors(repoDir, monorepoPackages, isUserTestRepo, oldTscPath, executionTimeout, /*skipLibCheck*/ true); + let oldErrors; + { + await using overlay = await downloadDir.createOverlay(); + const repoDir = path.join(overlay.path, repo.name); + const overlayMonorepoPackages = relativeMonorepoPackages.map(p => path.join(overlay.path, p)); + oldErrors = await ge.buildAndGetErrors(repoDir, overlayMonorepoPackages, isUserTestRepo, oldTscPath, executionTimeout, /*skipLibCheck*/ true); + } if (oldErrors.hasConfigFailure) { console.log("Unable to build project graph"); @@ -559,7 +569,13 @@ export async function getTscRepoResult( } console.log(`Building with ${newTscPath} (new)`); - const newErrors = await ge.buildAndGetErrors(repoDir, monorepoPackages, isUserTestRepo, newTscPath, executionTimeout, /*skipLibCheck*/ true); + let newErrors; + { + await using overlay = await downloadDir.createOverlay(); + const repoDir = path.join(overlay.path, repo.name); + const overlayMonorepoPackages = relativeMonorepoPackages.map(p => path.join(overlay.path, p)); + newErrors = await ge.buildAndGetErrors(repoDir, overlayMonorepoPackages, isUserTestRepo, newTscPath, executionTimeout, /*skipLibCheck*/ true); + } if (newErrors.hasConfigFailure) { console.log("Unable to build project graph"); @@ -704,13 +720,8 @@ export async function mainAsync(params: GitParams | UserParams): Promise { prng.seed(params.prngSeed); } - const downloadDir = params.tmpfs ? "/mnt/ts_downloads" : path.join(processCwd, "ts_downloads"); - // TODO: check first whether the directory exists and skip downloading if possible - // TODO: Seems like this should come after the typescript download - if (params.tmpfs) - await execAsync(processCwd, "sudo mkdir " + downloadDir); - else - await execAsync(processCwd, "mkdir " + downloadDir); + const downloadDirPath = params.tmpfs ? "/mnt/ts_downloads" : path.join(processCwd, "ts_downloads"); + const createFs = params.tmpfs ? createTempOverlayFS : createCopyingOverlayFS; const resultDirPath = path.join(processCwd, params.resultDirName); @@ -743,98 +754,64 @@ export async function mainAsync(params: GitParams | UserParams): Promise { let i = 1; for (const repo of repos) { console.log(`Starting #${i++} / ${repos.length}: ${repo.url ?? repo.name}`); - if (params.tmpfs) { - await execAsync(processCwd, "sudo mount -t tmpfs -o size=4g tmpfs " + downloadDir); - } const diagnosticOutput = !!params.diagnosticOutput; - try { - const repoPrefix = repo.owner - ? `${repo.owner}.${repo.name}` - : repo.name; - const replayScriptFileName = `${repoPrefix}.${replayScriptFileNameSuffix}`; - const rawErrorFileName = `${repoPrefix}.${rawErrorFileNameSuffix}`; - - const rawErrorArtifactPath = path.join(params.resultDirName, rawErrorFileName); - const replayScriptArtifactPath = path.join(params.resultDirName, replayScriptFileName); - - const { status, summary, tsServerResult, replayScriptPath, rawErrorPath } = params.entrypoint === "tsc" - ? await getTscRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, params.buildWithNewWhenOldFails, downloadDir, diagnosticOutput) - : await getTsServerRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, downloadDir, replayScriptArtifactPath, rawErrorArtifactPath, diagnosticOutput, isPr); - console.log(`Repo ${repo.url ?? repo.name} had status "${status}"`); - statusCounts[status] = (statusCounts[status] ?? 0) + 1; - - if (summary) { - const resultFileName = `${repoPrefix}.${resultFileNameSuffix}`; - await fs.promises.writeFile(path.join(resultDirPath, resultFileName), summary, { encoding: "utf-8" }); - } - if (tsServerResult) { - const replayScriptPath = path.join(downloadDir, path.basename(replayScriptArtifactPath)); - const repoDir = path.join(downloadDir, repo.name); + await using downloadDir = await createFs(downloadDirPath, diagnosticOutput); - let commit: string | undefined; - try { - console.log("Extracting commit SHA for repro steps"); - commit = (await execAsync(repoDir, `git rev-parse @`)).trim() - } - catch { - //noop - } + const repoPrefix = repo.owner + ? `${repo.owner}.${repo.name}` + : repo.name; + const replayScriptFileName = `${repoPrefix}.${replayScriptFileNameSuffix}`; + const rawErrorFileName = `${repoPrefix}.${rawErrorFileNameSuffix}`; - summaries.push({ - tsServerResult, - repo, - oldTsEntrypointPath, - rawErrorArtifactPath, - replayScript: fs.readFileSync(replayScriptPath, { encoding: "utf-8" }).split(/\r?\n/).slice(-5).join("\n"), - downloadDir, - replayScriptArtifactPath, - replayScriptName: path.basename(replayScriptArtifactPath), - commit - }); - } + const rawErrorArtifactPath = path.join(params.resultDirName, rawErrorFileName); + const replayScriptArtifactPath = path.join(params.resultDirName, replayScriptFileName); - if (summary || tsServerResult) { - // In practice, there will only be a replay script when the entrypoint is tsserver - // There can be replay steps without a summary, but then they're not interesting - if (replayScriptPath) { - await fs.promises.copyFile(replayScriptPath, path.join(resultDirPath, replayScriptFileName)); - } - if (rawErrorPath) { - await fs.promises.copyFile(rawErrorPath, path.join(resultDirPath, rawErrorFileName)); - } + const { status, summary, tsServerResult, replayScriptPath, rawErrorPath } = params.entrypoint === "tsc" + ? await getTscRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, params.buildWithNewWhenOldFails, downloadDir, diagnosticOutput) + : await getTsServerRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, downloadDir, replayScriptArtifactPath, rawErrorArtifactPath, diagnosticOutput, isPr); + console.log(`Repo ${repo.url ?? repo.name} had status "${status}"`); + statusCounts[status] = (statusCounts[status] ?? 0) + 1; + + if (summary) { + const resultFileName = `${repoPrefix}.${resultFileNameSuffix}`; + await fs.promises.writeFile(path.join(resultDirPath, resultFileName), summary, { encoding: "utf-8" }); + } + + if (tsServerResult) { + const replayScriptPath = path.join(downloadDir.path, path.basename(replayScriptArtifactPath)); + const repoDir = path.join(downloadDir.path, repo.name); + + let commit: string | undefined; + try { + console.log("Extracting commit SHA for repro steps"); + commit = (await execAsync(repoDir, `git rev-parse @`)).trim() } + catch { + //noop + } + + summaries.push({ + tsServerResult, + repo, + oldTsEntrypointPath, + rawErrorArtifactPath, + replayScript: fs.readFileSync(replayScriptPath, { encoding: "utf-8" }).split(/\r?\n/).slice(-5).join("\n"), + replayScriptArtifactPath, + replayScriptName: path.basename(replayScriptArtifactPath), + commit + }); } - finally { - // Throw away the repo so we don't run out of space - // Note that we specifically don't recover and attempt another repo if this fails - console.log("Cleaning up repo"); - if (params.tmpfs) { - if (diagnosticOutput) { - // Dump any processes holding onto the download directory in case umount fails - await execAsync(processCwd, `lsof -K i | grep ${downloadDir} || true`); - } - try { - await execAsync(processCwd, "sudo umount " + downloadDir); - } - catch (e) { - // HACK: Sometimes the server lingers for a brief period, so retry. - // Obviously, it would be better to have a way to know when it is gone-gone, - // but Linux doesn't provide such a mechanism for non-child processes. - // (You can poll for a process with the given PID after sending a kill signal, - // but best practice is to guard against the possibility of a new process - // being given the same PID.) - try { - console.log("umount failed - trying again after delay"); - await new Promise(resolve => setTimeout(resolve, 5000)); - await execAsync(processCwd, "sudo umount " + downloadDir); - } - catch { - await execAsync(processCwd, `pstree -palT`); - throw e; - } - } + + if (summary || tsServerResult) { + // In practice, there will only be a replay script when the entrypoint is tsserver + // There can be replay steps without a summary, but then they're not interesting + if (replayScriptPath) { + await fs.promises.copyFile(replayScriptPath, path.join(resultDirPath, replayScriptFileName)); + } + if (rawErrorPath) { + await fs.promises.copyFile(rawErrorPath, path.join(resultDirPath, rawErrorFileName)); } } } @@ -858,16 +835,8 @@ export async function mainAsync(params: GitParams | UserParams): Promise { } } - if (params.tmpfs) { - await execAsync(processCwd, "sudo rm -rf " + downloadDir); - await execAsync(processCwd, "sudo rm -rf " + oldTscDirPath); - await execAsync(processCwd, "sudo rm -rf " + newTscDirPath); - } - else { - await execAsync(processCwd, "rm -rf " + downloadDir); - await execAsync(processCwd, "rm -rf " + oldTscDirPath); - await execAsync(processCwd, "rm -rf " + newTscDirPath); - } + await execAsync(processCwd, "rm -rf " + oldTscDirPath); + await execAsync(processCwd, "rm -rf " + newTscDirPath); console.log("Statuses"); for (const status of Object.keys(statusCounts).sort()) { diff --git a/src/utils/execUtils.ts b/src/utils/execUtils.ts index ec76f3f..ef18bf2 100644 --- a/src/utils/execUtils.ts +++ b/src/utils/execUtils.ts @@ -29,6 +29,7 @@ export interface SpawnResult { /** Returns undefined if and only if executions times out. */ export function spawnWithTimeoutAsync(cwd: string, command: string, args: readonly string[], timeoutMs: number, env?: {}): Promise { + console.log(`${cwd}> ${command} ${args.join(" ")}`); return new Promise((resolve, reject) => { if (timeoutMs <= 0) { resolve(undefined); diff --git a/src/utils/gitUtils.ts b/src/utils/gitUtils.ts index c69a2b5..1f12381 100644 --- a/src/utils/gitUtils.ts +++ b/src/utils/gitUtils.ts @@ -84,8 +84,9 @@ export async function cloneRepoIfNecessary(parentDir: string, repo: Repo): Promi throw new Error("Repo url cannot be `undefined`"); } - if (!await utils.exists(path.join(parentDir, repo.name))) { - console.log(`Cloning ${repo.url} into ${repo.name}`); + const repoDir = path.join(parentDir, repo.name); + if (!await utils.exists(repoDir)) { + console.log(`Cloning ${repo.url} into ${repoDir}`); let options = ["--recurse-submodules", "--depth=1"]; if (repo.branch) { diff --git a/src/utils/installPackages.ts b/src/utils/installPackages.ts index 16996f8..44bc2a3 100644 --- a/src/utils/installPackages.ts +++ b/src/utils/installPackages.ts @@ -14,6 +14,7 @@ export enum InstallTool { export interface InstallCommand { directory: string; + prettyDirectory: string; tool: InstallTool; arguments: readonly string[]; } @@ -25,6 +26,8 @@ export interface InstallCommand { export async function installPackages(repoDir: string, ignoreScripts: boolean, quietOutput: boolean, recursiveSearch: boolean, monorepoPackages?: readonly string[], types?: string[]): Promise { monorepoPackages = monorepoPackages ?? await utils.getMonorepoOrder(repoDir); + const repoName = path.basename(repoDir); + const isRepoYarn = await utils.exists(path.join(repoDir, "yarn.lock")); // The existence of .yarnrc.yml indicates that this repo uses yarn 2 const isRepoYarn2 = await utils.exists(path.join(repoDir, ".yarnrc.yml")); @@ -119,8 +122,11 @@ export async function installPackages(repoDir: string, ignoreScripts: boolean, q continue; } + const prettyDirectory = path.join(repoName, path.relative(repoDir, packageRoot)); + commands.push({ directory: packageRoot, + prettyDirectory, tool, arguments: args, }); @@ -133,6 +139,7 @@ export async function installPackages(repoDir: string, ignoreScripts: boolean, q commands.push({ directory: packageRoot, + prettyDirectory, tool: InstallTool.Npm, arguments: args }); diff --git a/src/utils/overlayFS.ts b/src/utils/overlayFS.ts new file mode 100644 index 0000000..287ae02 --- /dev/null +++ b/src/utils/overlayFS.ts @@ -0,0 +1,174 @@ +import path from "path"; +import { execAsync } from "./execUtils"; + +export interface OverlayBaseFS { + path: string; + createOverlay(): Promise; +} + +export interface OverlayMergedFS extends AsyncDisposable { + path: string; +} + +export interface DisposableOverlayBaseFS extends OverlayBaseFS, AsyncDisposable {} + +const processCwd = process.cwd(); + +/** + * Creates an overlay FS using a tmpfs mount. A base directory is created on the tmpfs. + * New overlays are created by mounting an overlay on top of the base directory. + * + * This requires root access. + */ +export async function createTempOverlayFS(root: string, diagnosticOutput: boolean): Promise { + await tryUnmount(root); + await rmWithRetryAsRoot(root); + await mkdirAllAsRoot(root); + await execAsync(processCwd, `sudo mount -t tmpfs -o size=4g tmpfs ${root}`); + + const lowerDir = path.join(root, "base"); + await mkdirAll(lowerDir); + + let overlay: OverlayMergedFS | undefined; + + async function createOverlay(): Promise { + if (overlay) { + throw new Error("Overlay has already been created"); + } + + // Using short names here as these paths can appear in the summaries. + const overlayRoot = path.join(root, "_"); + await rmWithRetryAsRoot(overlayRoot); + + const upperDir = path.join(overlayRoot, ".u"); + const workDir = path.join(overlayRoot, ".w"); + const merged = path.join(overlayRoot, "m"); + + await mkdirAll(upperDir, workDir, merged); + + if (diagnosticOutput) { + await diskUsageRoot(lowerDir); + await diskUsageRoot(overlayRoot); + } + + await execAsync(processCwd, `sudo mount -t overlay overlay -o lowerdir=${lowerDir},upperdir=${upperDir},workdir=${workDir} ${merged}`); + + overlay = { + path: merged, + [Symbol.asyncDispose]: async () => { + overlay = undefined; + if (diagnosticOutput) { + await diskUsageRoot(upperDir); + } + await tryUnmount(merged); + await rmWithRetryAsRoot(overlayRoot); + } + } + + return overlay; + } + + return { + path: lowerDir, + createOverlay, + [Symbol.asyncDispose]: async () => { + if (diagnosticOutput) { + await diskUsageRoot(root); + } + if (overlay) { + await overlay[Symbol.asyncDispose](); + } + await tryUnmount(root); + await rmWithRetryAsRoot(root); + }, + } +} + +async function retry(fn: (() => void) | (() => Promise), retries: number, delayMs: number): Promise { + for (let i = 0; i < retries; i++) { + try { + await fn(); + return; + } catch (e) { + if (i === retries - 1) { + throw e; + } + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } +} + +async function tryUnmount(p: string) { + try { + await execAsync(processCwd, `sudo umount -R ${p}`) + } catch { + // ignore + } +} + +function diskUsageRoot(p: string) { + return execAsync(processCwd, `sudo du -sh ${p}`); +} + +function rmWithRetry(p: string) { + return retry(() => execAsync(processCwd, `rm -rf ${p}`), 3, 1000); +} + +function rmWithRetryAsRoot(p: string) { + return retry(() => execAsync(processCwd, `sudo rm -rf ${p}`), 3, 1000); +} + +function mkdirAll(...args: string[]) { + return execAsync(processCwd, `mkdir -p ${args.join(" ")}`); +} + +function mkdirAllAsRoot(...args: string[]) { + return execAsync(processCwd, `sudo mkdir -p ${args.join(" ")}`); +} + +/** + * Creates a fake overlay FS, which is just a directory on the local filesystem. + * Overlays are created by copying the contents of the `base` directory. + */ +export async function createCopyingOverlayFS(root: string, _diagnosticOutput: boolean): Promise { + await rmWithRetry(root); + await mkdirAll(root); + + const basePath = path.join(root, "base"); + await mkdirAll(basePath); + + let overlay: OverlayMergedFS | undefined; + + async function createOverlay(): Promise { + if (overlay) { + throw new Error("Overlay has already been created"); + } + + const overlayRoot = path.join(root, "overlay"); + await rmWithRetry(overlayRoot); + + await execAsync(processCwd, `cp -r --reflink=auto ${basePath} ${overlayRoot}`); + + overlay = { + path: overlayRoot, + [Symbol.asyncDispose]: async () => { + overlay = undefined; + await rmWithRetry(overlayRoot); + } + } + + return overlay; + } + + return { + path: basePath, + createOverlay, + [Symbol.asyncDispose]: async () => { + if (overlay) { + await overlay[Symbol.asyncDispose](); + overlay = undefined; + } + await rmWithRetry(root); + }, + } +} diff --git a/test/main.test.ts b/test/main.test.ts index 3cae29a..fe4c371 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -3,6 +3,7 @@ import { getTscRepoResult, downloadTsRepoAsync } from '../src/main' import { execSync } from "child_process" import { existsSync, mkdirSync } from "fs" import path = require("path") +import { createCopyingOverlayFS } from '../src/utils/overlayFS' describe("main", () => { jest.setTimeout(10 * 60 * 1000) xit("build-only correctly caches", async () => { @@ -15,7 +16,7 @@ describe("main", () => { path.resolve("./typescript-main/built/local/tsc.js"), path.resolve("./typescript-44585/built/local/tsc.js"), /*ignoreOldTscFailures*/ true, // as in a user test - "./ts_downloads", + await createCopyingOverlayFS("./ts_downloads", false), /*diagnosticOutput*/ false) expect(status).toEqual("NewBuildHadErrors") expect(summary).toBeDefined()