mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 17:14:45 +00:00
fix(test): reduce changed-target import graph IO
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -741,13 +742,154 @@ function resolveImportSpecifier(importer, specifier, fileSet) {
|
||||
|
||||
let cachedImportGraph = null;
|
||||
let cachedImportGraphCwd = null;
|
||||
let cachedImportGraphFiles = null;
|
||||
let cachedImportGraphFilesCwd = null;
|
||||
|
||||
function isImportableGraphFile(relative) {
|
||||
return IMPORTABLE_FILE_EXTENSIONS.some((ext) => relative.endsWith(ext));
|
||||
}
|
||||
|
||||
function listImportGraphFilesFromGit(cwd) {
|
||||
const result = spawnSync("git", ["ls-files", "--", ...SOURCE_ROOTS_FOR_IMPORT_GRAPH], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((line) => normalizePathPattern(line.trim()))
|
||||
.filter((line) => line.length > 0 && isImportableGraphFile(line));
|
||||
}
|
||||
|
||||
function listImportGraphFilesForCwd(cwd) {
|
||||
if (cachedImportGraphFiles && cachedImportGraphFilesCwd === cwd) {
|
||||
return cachedImportGraphFiles;
|
||||
}
|
||||
|
||||
cachedImportGraphFiles =
|
||||
listImportGraphFilesFromGit(cwd) ??
|
||||
SOURCE_ROOTS_FOR_IMPORT_GRAPH.flatMap((root) => listImportGraphFiles(cwd, root));
|
||||
cachedImportGraphFilesCwd = cwd;
|
||||
return cachedImportGraphFiles;
|
||||
}
|
||||
|
||||
function stripImportableGraphExtension(relative) {
|
||||
for (const ext of IMPORTABLE_FILE_EXTENSIONS) {
|
||||
if (relative.endsWith(ext)) {
|
||||
return relative.slice(0, -ext.length);
|
||||
}
|
||||
}
|
||||
return relative;
|
||||
}
|
||||
|
||||
function resolveImportGraphSearchTerm(relative) {
|
||||
const basename = path.posix.basename(stripImportableGraphExtension(relative));
|
||||
if (basename === "index" || basename.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return basename;
|
||||
}
|
||||
|
||||
function listImportGraphGrepMatches(cwd, term) {
|
||||
const result = spawnSync(
|
||||
"git",
|
||||
["grep", "-l", "--fixed-strings", term, "--", ...SOURCE_ROOTS_FOR_IMPORT_GRAPH],
|
||||
{
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
if (result.status === 1) {
|
||||
return [];
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((line) => normalizePathPattern(line.trim()))
|
||||
.filter((line) => line.length > 0 && isImportableGraphFile(line));
|
||||
}
|
||||
|
||||
function findDirectImportersWithGitGrep(cwd, importedFile, fileSet) {
|
||||
const term = resolveImportGraphSearchTerm(importedFile);
|
||||
if (!term) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = listImportGraphGrepMatches(cwd, term);
|
||||
if (!candidates || candidates.length > 800) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const importers = [];
|
||||
for (const file of candidates) {
|
||||
if (file === importedFile || !fileSet.has(file)) {
|
||||
continue;
|
||||
}
|
||||
let source = "";
|
||||
try {
|
||||
source = fs.readFileSync(path.join(cwd, file), "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
||||
const imported = resolveImportSpecifier(file, match[1] ?? match[2] ?? "", fileSet);
|
||||
if (imported === importedFile) {
|
||||
importers.push(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return importers;
|
||||
}
|
||||
|
||||
function resolveAffectedTestsFromTargetedImportScan(changedPath, cwd) {
|
||||
const normalized = normalizePathPattern(changedPath);
|
||||
const files = listImportGraphFilesForCwd(cwd);
|
||||
const fileSet = new Set(files);
|
||||
if (!fileSet.has(normalized)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const testFiles = new Set(
|
||||
files.filter((file) => isTestFileTarget(file) && !file.endsWith(".live.test.ts")),
|
||||
);
|
||||
const queue = [normalized];
|
||||
const seen = new Set(queue);
|
||||
const targets = [];
|
||||
|
||||
for (let index = 0; index < queue.length; index += 1) {
|
||||
const current = queue[index];
|
||||
const importers = findDirectImportersWithGitGrep(cwd, current, fileSet);
|
||||
if (importers === null) {
|
||||
return null;
|
||||
}
|
||||
for (const importer of importers) {
|
||||
if (seen.has(importer)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(importer);
|
||||
if (testFiles.has(importer)) {
|
||||
targets.push(importer);
|
||||
}
|
||||
queue.push(importer);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(targets)].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function getImportGraph(cwd) {
|
||||
if (cachedImportGraph && cachedImportGraphCwd === cwd) {
|
||||
return cachedImportGraph;
|
||||
}
|
||||
|
||||
const files = SOURCE_ROOTS_FOR_IMPORT_GRAPH.flatMap((root) => listImportGraphFiles(cwd, root));
|
||||
const files = listImportGraphFilesForCwd(cwd);
|
||||
const fileSet = new Set(files);
|
||||
const reverseImports = new Map();
|
||||
const testFiles = new Set(
|
||||
@@ -779,6 +921,11 @@ function getImportGraph(cwd) {
|
||||
|
||||
function resolveAffectedTestsFromImportGraph(changedPath, cwd) {
|
||||
const normalized = normalizePathPattern(changedPath);
|
||||
const targetedTargets = resolveAffectedTestsFromTargetedImportScan(normalized, cwd);
|
||||
if (targetedTargets !== null) {
|
||||
return targetedTargets;
|
||||
}
|
||||
|
||||
const { reverseImports, testFiles } = getImportGraph(cwd);
|
||||
const queue = [normalized];
|
||||
const seen = new Set(queue);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import fg from "fast-glob";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||
applyDefaultMultiSpecVitestCachePaths,
|
||||
@@ -836,9 +837,16 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
});
|
||||
|
||||
it("uses import-graph targets in default changed mode", () => {
|
||||
expect(resolveChangedTestTargetPlan(["test/helpers/normalize-text.ts"]).targets).toContain(
|
||||
"src/auto-reply/status.test.ts",
|
||||
);
|
||||
const readFileSync = vi.spyOn(fs, "readFileSync");
|
||||
const before = readFileSync.mock.calls.length;
|
||||
const targets = resolveChangedTestTargetPlan(["test/helpers/normalize-text.ts"]).targets;
|
||||
const repoSourceReads = readFileSync.mock.calls
|
||||
.slice(before)
|
||||
.filter(([file]) => typeof file === "string" && normalizeRepoPath(file).includes("/src/"));
|
||||
readFileSync.mockRestore();
|
||||
|
||||
expect(targets).toContain("src/auto-reply/status.test.ts");
|
||||
expect(repoSourceReads.length).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in New Issue
Block a user