Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/core/src/core/listTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const collectTests = async ({
const { getRsbuildStats, closeServer } = await createRsbuildServer({
globTestSourceEntries,
globalSetupFiles,
isWatchMode: false,
inspectedConfig: {
...context.normalizedConfig,
projects: context.projects.map((p) => p.normalizedConfig),
Expand Down
165 changes: 112 additions & 53 deletions packages/core/src/core/rsbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,16 +155,20 @@ export const prepareRsbuild = async (
export const calcEntriesToRerun = (
entries: EntryInfo[],
chunks: Rspack.StatsChunk[] | undefined,
buildData: { entryToChunkHashes?: TestEntryToChunkHashes },
buildData: {
entryToChunkHashes?: TestEntryToChunkHashes;
setupEntryToChunkHashes?: TestEntryToChunkHashes;
},
runtimeChunkName: string,
setupEntries: EntryInfo[],
): {
affectedEntries: EntryInfo[];
deletedEntries: string[];
} => {
const entryToChunkHashesMap = new Map<string, Record<string, string>>();

// Build current chunk hashes map
const buildChunkHashes = (entry: EntryInfo) => {
const buildChunkHashes = (
entry: EntryInfo,
map: Map<string, Record<string, string>>,
) => {
const validChunks = (entry.chunks || []).filter(
(chunk) => chunk !== runtimeChunkName,
);
Expand All @@ -174,70 +178,115 @@ export const calcEntriesToRerun = (
c.names?.includes(chunkName as string),
);
if (chunkInfo) {
const existing = entryToChunkHashesMap.get(entry.testPath) || {};
const existing = map.get(entry.testPath) || {};
existing[chunkName] = chunkInfo.hash ?? '';
entryToChunkHashesMap.set(entry.testPath, existing);
map.set(entry.testPath, existing);
}
});
};

(entries || []).forEach(buildChunkHashes);
const processEntryChanges = (
_entries: EntryInfo[],
prevHashes: TestEntryToChunkHashes | undefined,
currentHashesMap: Map<string, Record<string, string>>,
): {
affectedPaths: Set<string>;
deletedPaths: string[];
} => {
const affectedPaths = new Set<string>();
const deletedPaths: string[] = [];

if (prevHashes) {
const prevMap = new Map(prevHashes.map((e) => [e.name, e.chunks]));
const currentNames = new Set(currentHashesMap.keys());

deletedPaths.push(
...Array.from(prevMap.keys()).filter((name) => !currentNames.has(name)),
);

const entryToChunkHashes: TestEntryToChunkHashes = Array.from(
entryToChunkHashesMap.entries(),
).map(([name, chunks]) => ({ name, chunks }));
const findAffectedEntry = (testPath: string) => {
const currentChunks = currentHashesMap.get(testPath);
const prevChunks = prevMap.get(testPath);

// Process changes if we have previous data
const affectedTestPaths = new Set<string>();
const deletedEntries: string[] = [];
if (!currentChunks) return;

if (buildData.entryToChunkHashes) {
const prevMap = new Map(
buildData.entryToChunkHashes.map((e) => [e.name, e.chunks]),
);
const currentNames = new Set(entryToChunkHashesMap.keys());
if (!prevChunks) {
affectedPaths.add(testPath);
return;
}

// Find deleted entries
deletedEntries.push(
...Array.from(prevMap.keys()).filter((name) => !currentNames.has(name)),
);
const hasChanges = Object.entries(currentChunks).some(
([chunkName, hash]) => prevChunks[chunkName] !== hash,
);

// Find modified or added entries
const findAffectedEntry = (testPath: string) => {
const currentChunks = entryToChunkHashesMap.get(testPath);
const prevChunks = prevMap.get(testPath);
if (hasChanges) {
affectedPaths.add(testPath);
}
};

if (!currentChunks) return;
currentHashesMap.forEach((_, testPath) => {
findAffectedEntry(testPath);
});
}

if (!prevChunks) {
// New entry
affectedTestPaths.add(testPath);
return;
}
return { affectedPaths, deletedPaths };
};

const previousSetupHashes = buildData.setupEntryToChunkHashes;
const previousEntryHashes = buildData.entryToChunkHashes;

const setupEntryToChunkHashesMap = new Map<string, Record<string, string>>();
setupEntries.forEach((entry) => {
buildChunkHashes(entry, setupEntryToChunkHashesMap);
});

const setupEntryToChunkHashes: TestEntryToChunkHashes = Array.from(
setupEntryToChunkHashesMap.entries(),
).map(([name, chunks]) => ({ name, chunks }));

// Check for modified chunks
const hasChanges = Object.entries(currentChunks).some(
([chunkName, hash]) => prevChunks[chunkName] !== hash,
// apply latest setup entry chunk hashes
buildData.setupEntryToChunkHashes = setupEntryToChunkHashes;

const entryToChunkHashesMap = new Map<string, Record<string, string>>();
(entries || []).forEach((entry) => {
buildChunkHashes(entry, entryToChunkHashesMap);
});

const entryToChunkHashes: TestEntryToChunkHashes = Array.from(
entryToChunkHashesMap.entries(),
).map(([name, chunks]) => ({ name, chunks }));

// apply latest entry chunk hashes
buildData.entryToChunkHashes = entryToChunkHashes;

const isSetupChanged = () => {
const { affectedPaths: affectedSetupPaths, deletedPaths: deletedSetups } =
processEntryChanges(
setupEntries,
previousSetupHashes,
setupEntryToChunkHashesMap,
);

if (hasChanges) {
affectedTestPaths.add(testPath);
}
};
const affectedSetups = Array.from(affectedSetupPaths)
.map((testPath) => setupEntries.find((e) => e.testPath === testPath))
.filter((entry): entry is EntryInfo => entry !== undefined);

entryToChunkHashesMap.forEach((_, testPath) => {
findAffectedEntry(testPath);
});
return affectedSetups.length > 0 || deletedSetups.length > 0;
Comment thread
9aoy marked this conversation as resolved.
};
Comment thread
9aoy marked this conversation as resolved.

if (isSetupChanged()) {
// if setup files changed, all test entries are affected
return { affectedEntries: entries, deletedEntries: [] };
}

buildData.entryToChunkHashes = entryToChunkHashes;
const { affectedPaths: affectedTestPaths, deletedPaths } =
processEntryChanges(entries, previousEntryHashes, entryToChunkHashesMap);

// Convert affected test paths to EntryInfo objects
const affectedEntries = Array.from(affectedTestPaths)
.map((testPath) => entries.find((e) => e.testPath === testPath))
.filter((entry): entry is EntryInfo => entry !== undefined);

return { affectedEntries, deletedEntries };
return { affectedEntries, deletedEntries: deletedPaths };
};

class AssetsMemorySafeMap extends Map<string, string> {
Expand All @@ -259,7 +308,9 @@ export const createRsbuildServer = async ({
globalSetupFiles,
rsbuildInstance,
inspectedConfig,
isWatchMode,
}: {
isWatchMode: boolean;
rsbuildInstance: RsbuildInstance;
inspectedConfig: RstestContext['normalizedConfig'] & {
projects: NormalizedProjectConfig[];
Expand All @@ -280,7 +331,9 @@ export const createRsbuildServer = async ({
assetNames: string[];
getAssetFiles: (names: string[]) => Promise<Record<string, string>>;
getSourceMaps: (names: string[]) => Promise<Record<string, string>>;
/** affected test entries only available in watch mode */
affectedEntries: EntryInfo[];
/** deleted test entries only available in watch mode */
deletedEntries: string[];
}>;
closeServer: () => Promise<void>;
Expand Down Expand Up @@ -360,7 +413,10 @@ export const createRsbuildServer = async ({

const buildData: Record<
string,
{ entryToChunkHashes?: TestEntryToChunkHashes }
{
entryToChunkHashes?: TestEntryToChunkHashes;
setupEntryToChunkHashes?: TestEntryToChunkHashes;
}
> = {};

const getEntryFiles = async (manifest: ManifestData, outputPath: string) => {
Expand Down Expand Up @@ -475,12 +531,15 @@ export const createRsbuildServer = async ({

// affectedEntries: entries affected by source code.
// deletedEntries: entry files deleted from compilation.
const { affectedEntries, deletedEntries } = calcEntriesToRerun(
entries,
chunks,
buildData[environmentName],
`${environmentName}-${RUNTIME_CHUNK_NAME}`,
);
const { affectedEntries, deletedEntries } = isWatchMode
? calcEntriesToRerun(
entries,
chunks,
buildData[environmentName],
`${environmentName}-${RUNTIME_CHUNK_NAME}`,
setupEntries,
)
: { affectedEntries: [], deletedEntries: [] };

const cachedAssetFiles = new AssetsMemorySafeMap();
const cachedSourceMaps = new AssetsMemorySafeMap();
Expand Down
20 changes: 11 additions & 9 deletions packages/core/src/core/runTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,22 @@ export async function runTests(context: Rstest): Promise<void> {
globalSetupFiles,
);

const isWatchMode = command === 'watch';

const { getRsbuildStats, closeServer } = await createRsbuildServer({
inspectedConfig: {
...context.normalizedConfig,
projects: context.projects.map((p) => p.normalizedConfig),
},
globTestSourceEntries:
command === 'watch'
? globTestSourceEntries
: async (name) => {
if (entriesCache.has(name)) {
return entriesCache.get(name)!.entries;
}
return globTestSourceEntries(name);
},
isWatchMode,
globTestSourceEntries: isWatchMode
? globTestSourceEntries
: async (name) => {
if (entriesCache.has(name)) {
return entriesCache.get(name)!.entries;
}
return globTestSourceEntries(name);
},
setupFiles,
globalSetupFiles,
rsbuildInstance,
Expand Down
Loading