test(FN-000): address PR review feedback
This commit is contained in:
@@ -8,10 +8,13 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildReverseDependencyMap,
|
||||
shouldForceFullSuite,
|
||||
resolveAffectedPackages,
|
||||
decideExecutionPlan,
|
||||
computePackageHash,
|
||||
expandWithReverseDependents,
|
||||
listWorkspacePackageInfos,
|
||||
readCache,
|
||||
writeCache,
|
||||
applyCacheToPlan,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import fg from "fast-glob";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -19,29 +21,27 @@ function readJson(filePath) {
|
||||
function readWorkspacePackageDirs() {
|
||||
const workspaceFile = path.join(repoRoot, "pnpm-workspace.yaml");
|
||||
const workspaceYaml = readFileSync(workspaceFile, "utf8");
|
||||
const patterns = [...workspaceYaml.matchAll(/^\s*-\s+"([^"]+)"\s*$/gm)].map((match) => match[1]);
|
||||
const dirs = new Set();
|
||||
const parsed = parseYaml(workspaceYaml);
|
||||
const patterns = Array.isArray(parsed?.packages)
|
||||
? parsed.packages.filter((pattern) => typeof pattern === "string")
|
||||
: [];
|
||||
return fg.sync(patterns.map(workspacePatternToPackageJsonGlob), {
|
||||
absolute: true,
|
||||
cwd: repoRoot,
|
||||
dot: false,
|
||||
onlyFiles: true,
|
||||
unique: true,
|
||||
}).map((packageJsonPath) => path.dirname(packageJsonPath)).sort();
|
||||
}
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.endsWith("/*")) {
|
||||
const parentDir = path.join(repoRoot, pattern.slice(0, -2));
|
||||
for (const entry of readdirSync(parentDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const packageDir = path.join(parentDir, entry.name);
|
||||
if (existsSync(path.join(packageDir, "package.json"))) {
|
||||
dirs.add(packageDir);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageDir = path.join(repoRoot, pattern);
|
||||
if (existsSync(path.join(packageDir, "package.json"))) {
|
||||
dirs.add(packageDir);
|
||||
}
|
||||
}
|
||||
|
||||
return [...dirs].sort();
|
||||
function workspacePatternToPackageJsonGlob(pattern) {
|
||||
const trimmed = pattern.trim();
|
||||
const isNegated = trimmed.startsWith("!");
|
||||
const body = (isNegated ? trimmed.slice(1) : trimmed)
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\/+$/, "");
|
||||
const packageJsonGlob = body.endsWith("package.json") ? body : `${body}/package.json`;
|
||||
return isNegated ? `!${packageJsonGlob}` : packageJsonGlob;
|
||||
}
|
||||
|
||||
function hasSharedIsolation(config) {
|
||||
|
||||
@@ -5,18 +5,7 @@ import { cpus } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { ensureTestArtifacts } from "./ensure-test-artifacts.mjs";
|
||||
|
||||
const DEFAULT_TEST_PACKAGES = [
|
||||
"@fusion/core",
|
||||
"@fusion/engine",
|
||||
"@fusion/dashboard",
|
||||
"@runfusion/fusion",
|
||||
"@fusion/plugin-sdk",
|
||||
"@fusion/desktop",
|
||||
"@fusion/mobile",
|
||||
"@fusion/droid-cli",
|
||||
"@fusion/pi-claude-cli",
|
||||
];
|
||||
import { listWorkspacePackageInfos } from "./test-changed.mjs";
|
||||
|
||||
function run(command, commandArgs, options = {}) {
|
||||
const result = spawnSync(command, commandArgs, {
|
||||
@@ -70,9 +59,15 @@ export function selectShardPackages(packages, shard, total) {
|
||||
return packages.filter((_, index) => index % total === shard - 1);
|
||||
}
|
||||
|
||||
export function listWorkspaceTestPackages({ projectRoot = process.cwd() } = {}) {
|
||||
return listWorkspacePackageInfos({ projectRoot })
|
||||
.filter((workspacePackage) => workspacePackage.hasTestScript)
|
||||
.map((workspacePackage) => workspacePackage.name);
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2), env = process.env) {
|
||||
const { shard, total } = parseShardArgs(argv, env);
|
||||
const shardPackages = selectShardPackages(DEFAULT_TEST_PACKAGES, shard, total);
|
||||
const shardPackages = selectShardPackages(listWorkspaceTestPackages(), shard, total);
|
||||
|
||||
if (shardPackages.length === 0) {
|
||||
console.log(`[ci-test-shard] shard ${shard}/${total} has no assigned packages; skipping.`);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url";
|
||||
import { createHash } from "node:crypto";
|
||||
import { cpus, tmpdir } from "node:os";
|
||||
import { ensureTestArtifacts } from "./ensure-test-artifacts.mjs";
|
||||
import fg from "fast-glob";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const scriptDir = path.dirname(currentFilePath);
|
||||
@@ -110,62 +112,144 @@ function getBaseBranch() {
|
||||
return changesetConfig.baseBranch || "main";
|
||||
}
|
||||
|
||||
function workspacePatterns() {
|
||||
function readWorkspacePatterns(projectRoot = rootDir) {
|
||||
try {
|
||||
const workspacePath = path.join(rootDir, "pnpm-workspace.yaml");
|
||||
const content = readFileSync(workspacePath, "utf8");
|
||||
return content
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith("-"))
|
||||
.map((line) => line.replace(/^-\s*/, "").replace(/^['"]|['"]$/g, ""))
|
||||
.filter(Boolean);
|
||||
const workspacePath = path.join(projectRoot, "pnpm-workspace.yaml");
|
||||
const parsed = parseYaml(readFileSync(workspacePath, "utf8"));
|
||||
return Array.isArray(parsed?.packages)
|
||||
? parsed.packages.filter((pattern) => typeof pattern === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return ["packages/*"];
|
||||
}
|
||||
}
|
||||
|
||||
function expandWorkspacePattern(pattern) {
|
||||
if (!pattern.includes("*")) {
|
||||
return [pattern.replace(/\/$/, "")];
|
||||
}
|
||||
|
||||
const normalized = pattern.replace(/\/$/, "");
|
||||
if (!normalized.endsWith("/*")) {
|
||||
function expandWorkspacePattern(projectRoot, pattern) {
|
||||
if (pattern.trim().startsWith("!")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const base = normalized.slice(0, -2);
|
||||
const basePath = path.join(rootDir, base);
|
||||
|
||||
try {
|
||||
return readdirSync(basePath, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => `${base}/${entry.name}`);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return fg.sync(workspacePatternToPackageJsonGlob(pattern), {
|
||||
absolute: true,
|
||||
cwd: projectRoot,
|
||||
dot: false,
|
||||
onlyFiles: true,
|
||||
unique: true,
|
||||
});
|
||||
}
|
||||
|
||||
function listWorkspacePackages() {
|
||||
const packageNameByDir = new Map();
|
||||
const dirs = new Set(workspacePatterns().flatMap(expandWorkspacePattern));
|
||||
function expandWorkspacePatterns(projectRoot, patterns) {
|
||||
if (!patterns.some((pattern) => pattern.trim().startsWith("!"))) {
|
||||
return patterns.flatMap((pattern) => expandWorkspacePattern(projectRoot, pattern));
|
||||
}
|
||||
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
const packageJsonPath = path.join(rootDir, dir, "package.json");
|
||||
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
if (typeof pkg.name === "string") {
|
||||
packageNameByDir.set(dir, pkg.name);
|
||||
return fg.sync(patterns.map(workspacePatternToPackageJsonGlob), {
|
||||
absolute: true,
|
||||
cwd: projectRoot,
|
||||
dot: false,
|
||||
onlyFiles: true,
|
||||
unique: true,
|
||||
});
|
||||
}
|
||||
|
||||
function workspacePatternToPackageJsonGlob(pattern) {
|
||||
const trimmed = pattern.trim();
|
||||
const isNegated = trimmed.startsWith("!");
|
||||
const body = (isNegated ? trimmed.slice(1) : trimmed)
|
||||
.replace(/^\/+/, "")
|
||||
.replace(/\/+$/, "");
|
||||
const packageJsonGlob = body.endsWith("package.json") ? body : `${body}/package.json`;
|
||||
return isNegated ? `!${packageJsonGlob}` : packageJsonGlob;
|
||||
}
|
||||
|
||||
function collectWorkspaceDependencyNames(pkg) {
|
||||
return [
|
||||
pkg.dependencies,
|
||||
pkg.devDependencies,
|
||||
pkg.peerDependencies,
|
||||
pkg.optionalDependencies,
|
||||
].flatMap((deps) => deps && typeof deps === "object" ? Object.keys(deps) : []);
|
||||
}
|
||||
|
||||
export function listWorkspacePackageInfos({ projectRoot = rootDir } = {}) {
|
||||
const packageJsonPaths = [
|
||||
...new Set(expandWorkspacePatterns(projectRoot, readWorkspacePatterns(projectRoot))),
|
||||
];
|
||||
|
||||
return packageJsonPaths
|
||||
.map((packageJsonPath) => {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
if (typeof pkg.name !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dir = path.relative(projectRoot, path.dirname(packageJsonPath)).split(path.sep).join("/");
|
||||
return {
|
||||
name: pkg.name,
|
||||
dir,
|
||||
hasTestScript: typeof pkg.scripts?.test === "string",
|
||||
dependencyNames: collectWorkspaceDependencyNames(pkg),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// ignore directories without package.json
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.dir.localeCompare(b.dir));
|
||||
}
|
||||
|
||||
function listWorkspacePackages(workspacePackages = listWorkspacePackageInfos()) {
|
||||
const packageNameByDir = new Map();
|
||||
for (const workspacePackage of workspacePackages) {
|
||||
packageNameByDir.set(workspacePackage.dir, workspacePackage.name);
|
||||
if (workspacePackage.dir.startsWith("packages/")) {
|
||||
packageNameByDir.set(workspacePackage.dir.split("/")[1], workspacePackage.name);
|
||||
}
|
||||
}
|
||||
|
||||
return packageNameByDir;
|
||||
}
|
||||
|
||||
export function buildPackageDirByName(workspacePackages) {
|
||||
const packageDirByName = new Map();
|
||||
for (const workspacePackage of workspacePackages) {
|
||||
packageDirByName.set(workspacePackage.name, workspacePackage.dir);
|
||||
}
|
||||
return packageDirByName;
|
||||
}
|
||||
|
||||
export function buildReverseDependencyMap(workspacePackages) {
|
||||
const workspaceNames = new Set(workspacePackages.map((workspacePackage) => workspacePackage.name));
|
||||
const reverseDependencyMap = new Map(workspacePackages.map((workspacePackage) => [workspacePackage.name, []]));
|
||||
|
||||
for (const workspacePackage of workspacePackages) {
|
||||
for (const dependencyName of workspacePackage.dependencyNames ?? []) {
|
||||
if (workspaceNames.has(dependencyName)) {
|
||||
reverseDependencyMap.get(dependencyName)?.push(workspacePackage.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reverseDependencyMap;
|
||||
}
|
||||
|
||||
export function expandWithReverseDependents(packageNames, reverseDependencyMap) {
|
||||
const expanded = new Set(packageNames);
|
||||
const queue = [...packageNames];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
for (const dependent of reverseDependencyMap.get(current) ?? []) {
|
||||
if (expanded.has(dependent)) continue;
|
||||
expanded.add(dependent);
|
||||
queue.push(dependent);
|
||||
}
|
||||
}
|
||||
|
||||
return [...expanded];
|
||||
}
|
||||
|
||||
export function shouldForceFullSuite(changedFiles) {
|
||||
const fullSuitePaths = [
|
||||
"package.json",
|
||||
@@ -188,7 +272,7 @@ export function shouldForceFullSuite(changedFiles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (file.startsWith("packages/") && /vitest|test/.test(path.basename(file))) {
|
||||
if ((file.startsWith("packages/") || file.startsWith("plugins/")) && /vitest|test/.test(path.basename(file))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -230,23 +314,32 @@ function changedFilesSince(baseSha) {
|
||||
|
||||
export function resolveAffectedPackages(changedFiles, packageNameByDir) {
|
||||
const affected = new Set();
|
||||
const packageDirs = [...packageNameByDir.keys()]
|
||||
.filter((dir) => dir.includes("/"))
|
||||
.sort((a, b) => b.length - a.length);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
if (!file.startsWith("packages/") && !file.startsWith("plugins/")) {
|
||||
let packageName = null;
|
||||
|
||||
for (const packageDir of packageDirs) {
|
||||
if (file === packageDir || file.startsWith(`${packageDir}/`)) {
|
||||
packageName = packageNameByDir.get(packageDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!packageName && file.startsWith("packages/")) {
|
||||
const [, dir] = file.split("/");
|
||||
packageName = packageNameByDir.get(dir) ?? packageNameByDir.get(`packages/${dir}`) ?? null;
|
||||
}
|
||||
|
||||
if (!packageName) {
|
||||
if (file.startsWith("packages/") || file.startsWith("plugins/")) {
|
||||
return null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceDir = [...packageNameByDir.keys()]
|
||||
.find((dir) => file === dir || file.startsWith(`${dir}/`));
|
||||
|
||||
if (!workspaceDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageName = packageNameByDir.get(workspaceDir);
|
||||
if (!packageName) {
|
||||
return null;
|
||||
}
|
||||
affected.add(packageName);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user