mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 15:01:03 +00:00
234 lines
6.7 KiB
JavaScript
234 lines
6.7 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
export const behaviorManifestPath = "test/fixtures/test-parallel.behavior.json";
|
|
export const unitTimingManifestPath = "test/fixtures/test-timings.unit.json";
|
|
export const unitMemoryHotspotManifestPath = "test/fixtures/test-memory-hotspots.unit.json";
|
|
|
|
const defaultTimingManifest = {
|
|
config: "vitest.unit.config.ts",
|
|
defaultDurationMs: 250,
|
|
files: {},
|
|
};
|
|
const defaultMemoryHotspotManifest = {
|
|
config: "vitest.unit.config.ts",
|
|
defaultMinDeltaKb: 256 * 1024,
|
|
files: {},
|
|
};
|
|
|
|
const readJson = (filePath, fallback) => {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
};
|
|
|
|
const normalizeRepoPath = (value) => value.split(path.sep).join("/");
|
|
|
|
const normalizeManifestEntries = (entries) =>
|
|
entries
|
|
.map((entry) =>
|
|
typeof entry === "string"
|
|
? { file: normalizeRepoPath(entry), reason: "" }
|
|
: {
|
|
file: normalizeRepoPath(String(entry?.file ?? "")),
|
|
reason: typeof entry?.reason === "string" ? entry.reason : "",
|
|
},
|
|
)
|
|
.filter((entry) => entry.file.length > 0);
|
|
|
|
export function loadTestRunnerBehavior() {
|
|
const raw = readJson(behaviorManifestPath, {});
|
|
const unit = raw.unit ?? {};
|
|
return {
|
|
unit: {
|
|
isolated: normalizeManifestEntries(unit.isolated ?? []),
|
|
singletonIsolated: normalizeManifestEntries(unit.singletonIsolated ?? []),
|
|
threadSingleton: normalizeManifestEntries(unit.threadSingleton ?? []),
|
|
vmForkSingleton: normalizeManifestEntries(unit.vmForkSingleton ?? []),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function loadUnitTimingManifest() {
|
|
const raw = readJson(unitTimingManifestPath, defaultTimingManifest);
|
|
const defaultDurationMs =
|
|
Number.isFinite(raw.defaultDurationMs) && raw.defaultDurationMs > 0
|
|
? raw.defaultDurationMs
|
|
: defaultTimingManifest.defaultDurationMs;
|
|
const files = Object.fromEntries(
|
|
Object.entries(raw.files ?? {})
|
|
.map(([file, value]) => {
|
|
const normalizedFile = normalizeRepoPath(file);
|
|
const durationMs =
|
|
Number.isFinite(value?.durationMs) && value.durationMs >= 0 ? value.durationMs : null;
|
|
const testCount =
|
|
Number.isFinite(value?.testCount) && value.testCount >= 0 ? value.testCount : null;
|
|
if (!durationMs) {
|
|
return [normalizedFile, null];
|
|
}
|
|
return [
|
|
normalizedFile,
|
|
{
|
|
durationMs,
|
|
...(testCount !== null ? { testCount } : {}),
|
|
},
|
|
];
|
|
})
|
|
.filter(([, value]) => value !== null),
|
|
);
|
|
|
|
return {
|
|
config:
|
|
typeof raw.config === "string" && raw.config ? raw.config : defaultTimingManifest.config,
|
|
generatedAt: typeof raw.generatedAt === "string" ? raw.generatedAt : "",
|
|
defaultDurationMs,
|
|
files,
|
|
};
|
|
}
|
|
|
|
export function loadUnitMemoryHotspotManifest() {
|
|
const raw = readJson(unitMemoryHotspotManifestPath, defaultMemoryHotspotManifest);
|
|
const defaultMinDeltaKb =
|
|
Number.isFinite(raw.defaultMinDeltaKb) && raw.defaultMinDeltaKb > 0
|
|
? raw.defaultMinDeltaKb
|
|
: defaultMemoryHotspotManifest.defaultMinDeltaKb;
|
|
const files = Object.fromEntries(
|
|
Object.entries(raw.files ?? {})
|
|
.map(([file, value]) => {
|
|
const normalizedFile = normalizeRepoPath(file);
|
|
const deltaKb =
|
|
Number.isFinite(value?.deltaKb) && value.deltaKb > 0 ? Math.round(value.deltaKb) : null;
|
|
const sources = Array.isArray(value?.sources)
|
|
? value.sources.filter((source) => typeof source === "string" && source.length > 0)
|
|
: [];
|
|
if (deltaKb === null) {
|
|
return [normalizedFile, null];
|
|
}
|
|
return [
|
|
normalizedFile,
|
|
{
|
|
deltaKb,
|
|
...(sources.length > 0 ? { sources } : {}),
|
|
},
|
|
];
|
|
})
|
|
.filter(([, value]) => value !== null),
|
|
);
|
|
|
|
return {
|
|
config:
|
|
typeof raw.config === "string" && raw.config
|
|
? raw.config
|
|
: defaultMemoryHotspotManifest.config,
|
|
generatedAt: typeof raw.generatedAt === "string" ? raw.generatedAt : "",
|
|
defaultMinDeltaKb,
|
|
files,
|
|
};
|
|
}
|
|
|
|
export function selectTimedHeavyFiles({
|
|
candidates,
|
|
limit,
|
|
minDurationMs,
|
|
exclude = new Set(),
|
|
timings,
|
|
}) {
|
|
return candidates
|
|
.filter((file) => !exclude.has(file))
|
|
.map((file) => ({
|
|
file,
|
|
durationMs: timings.files[file]?.durationMs ?? timings.defaultDurationMs,
|
|
known: Boolean(timings.files[file]),
|
|
}))
|
|
.filter((entry) => entry.known && entry.durationMs >= minDurationMs)
|
|
.toSorted((a, b) => b.durationMs - a.durationMs)
|
|
.slice(0, limit)
|
|
.map((entry) => entry.file);
|
|
}
|
|
|
|
export function selectMemoryHeavyFiles({
|
|
candidates,
|
|
limit,
|
|
minDeltaKb,
|
|
exclude = new Set(),
|
|
hotspots,
|
|
}) {
|
|
return candidates
|
|
.filter((file) => !exclude.has(file))
|
|
.map((file) => ({
|
|
file,
|
|
deltaKb: hotspots.files[file]?.deltaKb ?? 0,
|
|
known: Boolean(hotspots.files[file]),
|
|
}))
|
|
.filter((entry) => entry.known && entry.deltaKb >= minDeltaKb)
|
|
.toSorted((a, b) => b.deltaKb - a.deltaKb)
|
|
.slice(0, limit)
|
|
.map((entry) => entry.file);
|
|
}
|
|
|
|
export function selectUnitHeavyFileGroups({
|
|
candidates,
|
|
behaviorOverrides = new Set(),
|
|
timedLimit,
|
|
timedMinDurationMs,
|
|
memoryLimit,
|
|
memoryMinDeltaKb,
|
|
timings,
|
|
hotspots,
|
|
}) {
|
|
const memoryHeavyFiles =
|
|
memoryLimit > 0
|
|
? selectMemoryHeavyFiles({
|
|
candidates,
|
|
limit: memoryLimit,
|
|
minDeltaKb: memoryMinDeltaKb,
|
|
exclude: behaviorOverrides,
|
|
hotspots,
|
|
})
|
|
: [];
|
|
const schedulingOverrides = new Set([...behaviorOverrides, ...memoryHeavyFiles]);
|
|
const timedHeavyFiles =
|
|
timedLimit > 0
|
|
? selectTimedHeavyFiles({
|
|
candidates,
|
|
limit: timedLimit,
|
|
minDurationMs: timedMinDurationMs,
|
|
exclude: schedulingOverrides,
|
|
timings,
|
|
})
|
|
: [];
|
|
|
|
return {
|
|
memoryHeavyFiles,
|
|
timedHeavyFiles,
|
|
};
|
|
}
|
|
|
|
export function packFilesByDuration(files, bucketCount, estimateDurationMs) {
|
|
const normalizedBucketCount = Math.max(0, Math.floor(bucketCount));
|
|
if (normalizedBucketCount <= 0 || files.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const buckets = Array.from({ length: Math.min(normalizedBucketCount, files.length) }, () => ({
|
|
totalMs: 0,
|
|
files: [],
|
|
}));
|
|
|
|
const sortedFiles = [...files].toSorted((left, right) => {
|
|
return estimateDurationMs(right) - estimateDurationMs(left);
|
|
});
|
|
|
|
for (const file of sortedFiles) {
|
|
const bucket = buckets.reduce((lightest, current) =>
|
|
current.totalMs < lightest.totalMs ? current : lightest,
|
|
);
|
|
bucket.files.push(file);
|
|
bucket.totalMs += estimateDurationMs(file);
|
|
}
|
|
|
|
return buckets.map((bucket) => bucket.files).filter((bucket) => bucket.length > 0);
|
|
}
|