Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(betterer ✨): no more git hashes for cache #1228

Merged
merged 5 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
82 changes: 20 additions & 62 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/betterer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@
"optionalDependencies": {
"typescript": "*"
}
}
}
116 changes: 61 additions & 55 deletions packages/betterer/src/fs/file-cache.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type { BettererTestMeta } from '../test/index.js';
import type {
BettererCacheFile,
BettererFileCache,
BettererFilePaths,
BettererFileHashMap,
BettererFileHashMapSerialised,
BettererTestCacheMap,
BettererTestCacheMapSerialised
BettererTestCacheMapSerialised,
BettererFilePath
} from './types.js';

import { invariantΔ } from '@betterer/errors';
import path from 'node:path';
import { normalisedPath } from '../utils.js';
import { normalisedPath, sortEntriesKeys } from '../utils.js';
import { read } from './reader.js';
import { write } from './writer.js';
import { createCacheHash } from '../hasher.js';

const BETTERER_CACHE_VERSION = 2;

Expand All @@ -30,11 +33,9 @@ const BETTERER_CACHE_VERSION = 2;
// }
// }
//
// Each stored hash is the concatenated hash of the Betterer config files
// and the file at that path at the point it is written. It is written each
// time a test runs on a given file and the file gets better, stays the same,
// or gets updated. If the hash hasn't changed, then the config file hasn't
// changed, and the file hasn't changed, so running the test on the file again
// Each stored hash is the hash of contents of that path at the point it is written.
// It is written each time a test runs on a given file and the file gets better, stays the same,
// or gets updated. If the hash hasn't changed, then the file hasn't changed, so running the test on the file again
// *should* have the same result.
//
// Of course the actual test itself could have changed so ... 🤷‍♂️
Expand All @@ -45,14 +46,13 @@ export class BettererFileCacheΩ implements BettererFileCache {

private constructor(
private _cachePath: string,
private _configPaths: BettererFilePaths,
cacheJson: string | null
) {
this._memoryCacheMap = this._readCache(cacheJson);
}

public static async create(cachePath: string, configPaths: BettererFilePaths): Promise<BettererFileCacheΩ> {
return new BettererFileCacheΩ(cachePath, configPaths, await read(cachePath));
public static async create(cachePath: string): Promise<BettererFileCacheΩ> {
return new BettererFileCacheΩ(cachePath, await read(cachePath));
}

public clearCache(testName: string): void {
Expand All @@ -72,67 +72,77 @@ export class BettererFileCacheΩ implements BettererFileCache {

// Convert Map to Record so it can be serialised to disk:
const relativeTestCache: BettererTestCacheMapSerialised = {};
[...this._memoryCacheMap.entries()].forEach(([testName, absoluteFileHashMap]) => {
[...this._memoryCacheMap.entries()].sort(sortEntriesKeys).forEach(([testName, absoluteFileHashMap]) => {
const relativeFileHashMap: BettererFileHashMapSerialised = {};
[...absoluteFileHashMap.entries()].forEach(([absoluteFilePath, hash]) => {
const relativePath = normalisedPath(path.relative(path.dirname(this._cachePath), absoluteFilePath));
relativeFileHashMap[relativePath] = hash;
});
[...absoluteFileHashMap.entries()]
.map(([absoluteFilePath, hash]): [string, string] => {
const relativePath = normalisedPath(path.relative(path.dirname(this._cachePath), absoluteFilePath));
return [relativePath, hash] as [string, string];
})
.sort(sortEntriesKeys)
.forEach(([relativePath, hash]) => {
relativeFileHashMap[relativePath] = hash;
});
relativeTestCache[testName] = relativeFileHashMap;
});
const cache = { version: BETTERER_CACHE_VERSION, testCache: relativeTestCache };
const cacheString = JSON.stringify(cache, null, ' ');
await write(cacheString, this._cachePath);
}

public filterCached(testName: string, filePaths: BettererFilePaths): BettererFilePaths {
const testCache = this._memoryCacheMap.get(testName) ?? (new Map() as BettererTestCacheMap);
return filePaths.filter((filePath) => {
const hash = this._fileHashMap.get(filePath);
public async filterCached(testMeta: BettererTestMeta, filePaths: BettererFilePaths): Promise<BettererFilePaths> {
const { name } = testMeta;
const testCache = this._memoryCacheMap.get(name) ?? (new Map() as BettererTestCacheMap);

// If hash is null, then the file isn't tracked by version control *and* it can't be read,
// so it probably doesn't exist
if (hash == null) {
return true;
}
const configHash = testCache.get(testMeta.configPath);
if (configHash !== testMeta.configHash) {
return filePaths;
}

const previousHash = testCache.get(filePath);
return hash !== previousHash;
});
const cacheMisses: Array<BettererFilePath> = [];
await Promise.all(
filePaths.map(async (filePath) => {
const contents = await read(filePath);
if (contents == null) {
return;
}
const hash = createCacheHash(contents);
const previousHash = testCache.get(filePath);
if (hash !== previousHash) {
cacheMisses.push(filePath);
}
})
);
return cacheMisses;
}

public updateCache(testName: string, filePaths: BettererFilePaths): void {
if (!this._memoryCacheMap.get(testName)) {
this._memoryCacheMap.set(testName, new Map());
public async updateCache(testMeta: BettererTestMeta, filePaths: BettererFilePaths): Promise<void> {
const { name } = testMeta;
if (!this._memoryCacheMap.get(name)) {
this._memoryCacheMap.set(name, new Map());
}
const testCache = this._memoryCacheMap.get(testName);

const testCache = this._memoryCacheMap.get(name);
invariantΔ(testCache, '`testCache` entry should have been validated above!', testCache);
const existingFilePaths = [...testCache.keys()];

const existingFilePaths = [...testCache.keys()];
const cacheFilePaths = Array.from(new Set([...existingFilePaths, ...filePaths])).sort();

const updatedCache: BettererFileHashMap = new Map();
cacheFilePaths.forEach((filePath) => {
const hash = this._fileHashMap.get(filePath);

// If hash is null, then the file isn't tracked by version control *and* it can't be read,
// so it probably doesn't exist
if (hash == null) {
return;
}
await Promise.all(
cacheFilePaths.map(async (filePath) => {
const contents = await read(filePath);
if (contents === null) {
return;
}

updatedCache.set(filePath, hash);
});
const hash = createCacheHash(contents);
updatedCache.set(filePath, hash);
})
);

this._memoryCacheMap.set(testName, updatedCache);
}
updatedCache.set(testMeta.configPath, testMeta.configHash);

public setHashes(newHashes: BettererFileHashMap): void {
const configHash = this._getConfigHash(newHashes);
this._fileHashMap = new Map();
[...newHashes.entries()].forEach(([absolutePath, hash]) => {
this._fileHashMap.set(absolutePath, `${configHash}${hash}`);
});
this._memoryCacheMap.set(name, updatedCache);
}

private _readCache(cacheJSON: string | null): BettererTestCacheMap {
Expand Down Expand Up @@ -160,8 +170,4 @@ export class BettererFileCacheΩ implements BettererFileCache {
});
return absoluteTestCacheMap;
}

private _getConfigHash(newFileHashMap: BettererFileHashMap): string {
return this._configPaths.map((configPath) => newFileHashMap.get(normalisedPath(configPath))).join('');
}
}
15 changes: 7 additions & 8 deletions packages/betterer/src/fs/file-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class BettererFileResolverΩ implements BettererFileResolver {
private _excluded: Array<RegExp> = [];
private _included: Array<string> = [INCLUDE_ALL];
private _includedResolved: Array<string> | null = null;
private _testName: string | null = null;
private _testMeta: BettererTestMeta | null = null;
private _validatedFilePaths: Array<string> = [];
private _validatedFilePathsMap: Record<string, boolean> = {};

Expand All @@ -36,15 +36,14 @@ export class BettererFileResolverΩ implements BettererFileResolver {
return !!this._baseDirectory;
}

public get testName(): string {
invariantΔ(this._testName, '`baseDirectory` is only set once the resolver is initialised!');
return this._testName;
public get testMeta(): BettererTestMeta {
invariantΔ(this._testMeta, '`testMeta` is only set once the resolver is initialised!');
return this._testMeta;
}

public init(testMeta: BettererTestMeta): void {
const { configPath, name } = testMeta;
this._testName = name;
this._baseDirectory = path.dirname(configPath);
this._testMeta = testMeta;
this._baseDirectory = path.dirname(this._testMeta.configPath);
}

public async validate(filePaths: BettererFilePaths): Promise<BettererFilePaths> {
Expand All @@ -61,7 +60,7 @@ export class BettererFileResolverΩ implements BettererFileResolver {
if (!config.cache) {
return filePaths;
}
return await versionControl.api.filterCached(this.testName, filePaths);
return await versionControl.api.filterCached(this.testMeta, filePaths);
}

public included(filePaths: BettererFilePaths): BettererFilePaths {
Expand Down
Loading