Skip to content

Commit 0bc46f6

Browse files
Merge pull request #1214 from gemini-testing/TESTPLANE-921.clear_dump_unused
fix(selectivity): ensure selectivity dump has no stale data
2 parents ec048f6 + af5b616 commit 0bc46f6

6 files changed

Lines changed: 554 additions & 199 deletions

File tree

src/browser/cdp/selectivity/fs-cache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os from "node:os";
22
import path from "node:path";
3+
import { performance } from "node:perf_hooks";
34
import pLimit from "p-limit";
45
import lockfile from "proper-lockfile";
56
import fs from "fs-extra";
@@ -14,7 +15,6 @@ export const CacheType = {
1415
type CacheTypeValue = (typeof CacheType)[keyof typeof CacheType];
1516

1617
// Cache is considered fresh if it was created after process start
17-
const processStartTime = Number(new Date());
1818
const tmpDir = path.join(os.tmpdir(), SELECTIVITY_CACHE_DIRECTIRY);
1919

2020
// https://nodejs.org/api/cli.html#uv_threadpool_sizesize
@@ -27,7 +27,7 @@ const ensureSelectivityCacheDirectory = async (): Promise<void> => {
2727
const wasModifiedAfterProcessStart = async (flagFilePath: string): Promise<boolean> => {
2828
try {
2929
const stats = await libUVLimited(() => fs.stat(flagFilePath));
30-
return stats.mtimeMs >= processStartTime;
30+
return stats.mtimeMs >= performance.timeOrigin;
3131
} catch {
3232
return false;
3333
}

src/browser/cdp/selectivity/hash-writer.ts

Lines changed: 20 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { memoize } from "lodash";
22
import path from "node:path";
33
import { HashProvider } from "./hash-provider";
4-
import { getSelectivityHashesPath, readHashFileContents, shallowSortObject } from "./utils";
4+
import { getSelectivityHashesPath } from "./utils";
55
import { writeJsonWithCompression } from "./json-utils";
6-
import type { NormalizedDependencies, SelectivityCompressionType } from "./types";
6+
import type { HashFileContents, NormalizedDependencies, SelectivityCompressionType } from "./types";
77

88
export class HashWriter {
99
private readonly _hashProvider = new HashProvider();
10-
// "null" - successfully writed, "Promise<string>" - file/module hash, "Promise<Error>" - calculating hash error
11-
private readonly _stagedFileHashes = new Map<string, null | Promise<string | Error>>();
12-
private readonly _stagedModuleHashes = new Map<string, null | Promise<string | Error>>();
13-
private readonly _stagedPatternHashes = new Map<string, null | Promise<string | Error>>();
10+
// "Promise<string>" - file/module hash, "Promise<Error>" - calculating hash error
11+
private readonly _stagedFileHashes = new Map<string, Promise<string | Error>>();
12+
private readonly _stagedModuleHashes = new Map<string, Promise<string | Error>>();
13+
private readonly _stagedPatternHashes = new Map<string, Promise<string | Error>>();
1414
private readonly _selectivityHashesPath: string;
1515
private readonly _compresion: SelectivityCompressionType;
1616

@@ -50,8 +50,8 @@ export class HashWriter {
5050
this._stagedModuleHashes.set(modulePath, value);
5151
}
5252

53-
addPatternDependencyHash(dependencyPatterns: string): void {
54-
return this._addPatternDependency(dependencyPatterns);
53+
addPatternDependencyHash(dependencyPattern: string): void {
54+
return this._addPatternDependency(dependencyPattern);
5555
}
5656

5757
addTestDependencyHashes(dependencies: NormalizedDependencies): void {
@@ -60,7 +60,7 @@ export class HashWriter {
6060
dependencies.modules.forEach(dependency => this._addModuleDependency(dependency));
6161
}
6262

63-
async commit(): Promise<void> {
63+
async save(): Promise<void> {
6464
const hasStaged = Boolean(
6565
this._stagedFileHashes.size || this._stagedModuleHashes.size || this._stagedPatternHashes.size,
6666
);
@@ -69,96 +69,34 @@ export class HashWriter {
6969
return;
7070
}
7171

72-
const stagedModuleNames = Array.from(this._stagedModuleHashes.keys());
73-
const stagedFileNames = Array.from(this._stagedFileHashes.keys());
74-
const stagedPatternNames = Array.from(this._stagedPatternHashes.keys());
75-
76-
const filterMatchingHashes = async (
77-
keys: string[],
78-
src: Map<string, null | Promise<string | Error>>,
79-
dest: Record<string, string>,
80-
): Promise<string[]> => {
81-
const remainingKeys: string[] = [];
82-
83-
for (const key of keys) {
84-
const oldValue = dest[key];
85-
const newValue = await src.get(key);
86-
87-
if (newValue === null) {
88-
continue;
89-
}
90-
91-
if (newValue === oldValue) {
92-
src.set(key, null);
93-
} else {
94-
remainingKeys.push(key);
95-
}
96-
}
97-
98-
return remainingKeys;
99-
};
100-
10172
const writeTo = async (
102-
keys: string[],
103-
src: Map<string, null | Promise<string | Error>>,
73+
src: Map<string, Promise<string | Error>>,
10474
dest: Record<string, string>,
10575
): Promise<void> => {
106-
let needsReSort = false;
76+
const keys = Array.from(src.keys());
10777

10878
for (const key of keys) {
10979
const hash = await src.get(key);
11080

111-
if (!hash) {
112-
continue;
113-
}
114-
11581
if (hash instanceof Error) {
11682
throw hash;
11783
}
11884

119-
needsReSort = needsReSort || !Object.hasOwn(dest, key);
120-
121-
dest[key] = hash;
122-
}
123-
124-
if (needsReSort) {
125-
shallowSortObject(dest);
85+
dest[key] = hash as string;
12686
}
12787
};
12888

129-
const markAsCommited = (keys: string[], src: Map<string, null | Promise<string | Error>>): void => {
130-
keys.forEach(key => src.set(key, null));
89+
const fileContents: HashFileContents = {
90+
files: {},
91+
modules: {},
92+
patterns: {},
13193
};
13294

133-
// Waiting for hashes to be calculated before locking file to reduce lock time
134-
await Promise.all([
135-
...Object.values(this._stagedFileHashes),
136-
...Object.values(this._stagedModuleHashes),
137-
...Object.values(this._stagedPatternHashes),
138-
]);
139-
140-
const existingHashesContent = await readHashFileContents(this._selectivityHashesPath, this._compresion);
141-
142-
const [updatedModules, updatedFiles, updatedPatterns] = await Promise.all([
143-
filterMatchingHashes(stagedModuleNames, this._stagedModuleHashes, existingHashesContent.modules),
144-
filterMatchingHashes(stagedFileNames, this._stagedFileHashes, existingHashesContent.files),
145-
filterMatchingHashes(stagedPatternNames, this._stagedPatternHashes, existingHashesContent.patterns),
146-
]);
147-
148-
if (!updatedFiles.length && !updatedModules.length && !updatedPatterns.length) {
149-
return;
150-
}
151-
152-
await Promise.all([
153-
writeTo(updatedModules, this._stagedModuleHashes, existingHashesContent.modules),
154-
writeTo(updatedFiles, this._stagedFileHashes, existingHashesContent.files),
155-
writeTo(updatedPatterns, this._stagedPatternHashes, existingHashesContent.patterns),
156-
]);
157-
158-
await writeJsonWithCompression(this._selectivityHashesPath, existingHashesContent, this._compresion);
95+
await writeTo(this._stagedFileHashes, fileContents.files);
96+
await writeTo(this._stagedModuleHashes, fileContents.modules);
97+
await writeTo(this._stagedPatternHashes, fileContents.patterns);
15998

160-
markAsCommited(updatedModules, this._stagedModuleHashes);
161-
markAsCommited(updatedFiles, this._stagedFileHashes);
99+
await writeJsonWithCompression(this._selectivityHashesPath, fileContents, this._compresion);
162100
}
163101
}
164102

src/browser/cdp/selectivity/index.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import path from "node:path";
2+
import fs from "fs-extra";
13
import { CSSSelectivity } from "./css-selectivity";
24
import { JSSelectivity } from "./js-selectivity";
35
import type { ExistingBrowser } from "../../existing-browser";
46
import { getTestDependenciesWriter } from "./test-dependencies-writer";
57
import type { Test, TestDepsContext, TestDepsData } from "../../../types";
6-
import { mergeSourceDependencies, transformSourceDependencies } from "./utils";
8+
import { getSelectivityTestsPath, mergeSourceDependencies, transformSourceDependencies } from "./utils";
79
import { getHashWriter } from "./hash-writer";
810
import { Compression } from "./types";
911
import { getCollectedTestplaneDependencies } from "./testplane-selectivity";
1012
import { getHashReader } from "./hash-reader";
1113
import type { Config } from "../../../config";
1214
import { MasterEvents } from "../../../events";
1315
import { selectivityShouldWrite } from "./modes";
16+
import { debugSelectivity } from "./debug";
1417

1518
type StopSelectivityFn = (test: Test, shouldWrite: boolean) => Promise<void>;
1619

@@ -41,13 +44,78 @@ export const updateSelectivityHashes = async (config: Config): Promise<void> =>
4144
}
4245

4346
try {
44-
await hashWriter.commit();
47+
await hashWriter.save();
4548
} catch (cause) {
4649
throw new Error("Selectivity: couldn't save test dependencies hash", { cause });
4750
}
4851
}
4952
};
5053

54+
export const clearUnusedSelectivityDumps = async (config: Config): Promise<void> => {
55+
const browserIds = config.getBrowserIds();
56+
const selectivityRoots: string[] = [];
57+
58+
for (const browserId of browserIds) {
59+
const browserConfig = config.forBrowser(browserId);
60+
const { enabled, testDependenciesPath } = browserConfig.selectivity;
61+
62+
if (enabled && !selectivityRoots.includes(testDependenciesPath)) {
63+
selectivityRoots.push(testDependenciesPath);
64+
}
65+
}
66+
67+
let filesTotal = 0;
68+
let filesDeleted = 0;
69+
70+
// eslint-disable-next-line no-bitwise
71+
const rwMode = fs.constants.R_OK | fs.constants.W_OK;
72+
73+
await Promise.all(
74+
selectivityRoots.map(async selectivityRoot => {
75+
const testsPath = getSelectivityTestsPath(selectivityRoot);
76+
const accessError = await fs.access(testsPath, rwMode).catch((err: Error) => err);
77+
78+
if (accessError) {
79+
if (!("code" in accessError && accessError.code === "ENOENT")) {
80+
debugSelectivity(`Couldn't access "${testsPath}" to clear stale files: %O`, accessError);
81+
}
82+
83+
return;
84+
}
85+
86+
const testsFileNames = await fs.readdir(testsPath);
87+
88+
filesTotal += testsFileNames.length;
89+
90+
for (const testFileName of testsFileNames) {
91+
const filePath = path.join(testsPath, testFileName);
92+
const fileStat = await fs.stat(filePath).catch(() => null);
93+
94+
if (!fileStat) {
95+
debugSelectivity(`Couldn't access file "${filePath}" to check if it was used. Skipping`);
96+
continue;
97+
}
98+
99+
// File was not used in this run
100+
if (fileStat.atimeMs < performance.timeOrigin && fileStat.isFile()) {
101+
await fs
102+
.unlink(filePath)
103+
.then(() => {
104+
filesDeleted++;
105+
})
106+
.catch(err => {
107+
debugSelectivity(`Couldn't remove stale file "${filePath}": %O`, err);
108+
});
109+
}
110+
}
111+
}),
112+
);
113+
114+
if (filesDeleted) {
115+
debugSelectivity(`Out of ${filesTotal} files, ${filesDeleted} were considered as outdated and deleted`);
116+
}
117+
};
118+
51119
export const startSelectivity = async (browser: ExistingBrowser): Promise<StopSelectivityFn> => {
52120
const { enabled, compression, sourceRoot, testDependenciesPath, mapDependencyRelativePath } =
53121
browser.config.selectivity;

src/testplane.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { initDevServer } from "./dev-server";
1818
import { ConfigInput } from "./config/types";
1919
import { MasterEventHandler, Test, TestResult } from "./types";
2020
import { preloadWebdriverIO } from "./utils/preload-utils";
21-
import { updateSelectivityHashes } from "./browser/cdp/selectivity";
21+
import { clearUnusedSelectivityDumps, updateSelectivityHashes } from "./browser/cdp/selectivity";
2222
import { TagFilter } from "./utils/cli";
2323
import { ViteServer } from "./runner/browser-env/vite/server";
2424
import { getGlobalFilesToRemove, initGlobalFilesToRemove } from "./globalFilesToRemove";
@@ -208,9 +208,18 @@ export class Testplane extends BaseTestplane {
208208
);
209209

210210
if (!shouldDisableSelectivity && !this.isFailed()) {
211-
await updateSelectivityHashes(this.config).catch(err => {
212-
console.error("Skipping selectivity state update because of an error:", err);
213-
});
211+
const [updateResult, clearResult] = await Promise.allSettled([
212+
updateSelectivityHashes(this.config),
213+
clearUnusedSelectivityDumps(this.config),
214+
]);
215+
216+
if (updateResult.status === "rejected") {
217+
console.error("Couldn't update selectivity state: ", updateResult.reason);
218+
}
219+
220+
if (clearResult.status === "rejected") {
221+
console.error("Couldn't clear stale selectivity files: ", clearResult.reason);
222+
}
214223
}
215224

216225
if (this.config.afterAll) {

0 commit comments

Comments
 (0)