Skip to content
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"betterertest",
"borks",
"bufferutil",
"Cacheable",
"callsite",
"clsx",
"Comlink",
Expand Down
4 changes: 2 additions & 2 deletions goldens/api/betterer.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export type BettererAPI = typeof betterer;

// @public
export interface BettererConfig extends BettererConfigFS, BettererConfigReporter, BettererConfigContext, BettererConfigWatcher {
versionControlPath: string;
}

// @public
Expand Down Expand Up @@ -50,6 +49,7 @@ export interface BettererConfigFS {
configPaths: BettererConfigPaths;
cwd: string;
resultsPath: string;
versionControlPath: string;
}

// @public
Expand Down Expand Up @@ -420,6 +420,7 @@ export interface BettererRun {
readonly filePaths: BettererFilePaths | null;
readonly isNew: boolean;
readonly isObsolete: boolean;
readonly isRemoved: boolean;
readonly isSkipped: boolean;
readonly name: string;
}
Expand All @@ -446,7 +447,6 @@ export interface BettererRunSummary extends BettererRun {
readonly isComplete: boolean;
readonly isExpired: boolean;
readonly isFailed: boolean;
readonly isRemoved: boolean;
readonly isSame: boolean;
readonly isUpdated: boolean;
readonly isWorse: boolean;
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

12 changes: 1 addition & 11 deletions packages/betterer/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,7 @@ export interface BettererConfig
extends BettererConfigFS,
BettererConfigReporter,
BettererConfigContext,
BettererConfigWatcher {
/**
* The path to the local version control root.
*
* @remarks you might think that this should live on `BettererConfigFS`,
* but it can't! The `versionControlPath` is inferred from the instantiated
* `BettererVersionControl` instance, rather than being passed as options by the
* user.
*/
versionControlPath: string;
}
BettererConfigWatcher {}

/**
* @public Options for when you override the config via the {@link @betterer/betterer#BettererContext.options | `BettererContext.options()` API}.
Expand Down
20 changes: 19 additions & 1 deletion packages/betterer/src/fs/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BettererConfigFS, BettererOptionsFS } from './types.js';

import { promises as fs } from 'node:fs';
import path from 'node:path';

import { BettererError } from '@betterer/errors';
Expand All @@ -26,12 +27,15 @@ export async function createFSConfig(options: BettererOptionsFS): Promise<Better
validateStringArray({ cachePath });
validateStringArray({ resultsPath });

const gitRoot = await validateGitRepo(cwd);

return {
cache,
cachePath: path.resolve(cwd, cachePath),
cwd,
configPaths: validatedConfigPaths,
resultsPath: path.resolve(cwd, resultsPath)
resultsPath: path.resolve(cwd, resultsPath),
versionControlPath: path.dirname(gitRoot)
};
}

Expand Down Expand Up @@ -68,3 +72,17 @@ async function validateConfigPaths(cwd: string, configPaths: Array<string>): Pro
})
);
}

async function validateGitRepo(cwd: string): Promise<string> {
let dir = cwd;
while (dir !== path.parse(dir).root) {
try {
const gitPath = path.join(dir, '.git');
await fs.access(gitPath);
return gitPath;
} catch {
dir = path.join(dir, '..');
}
}
throw new BettererError('.git directory not found. Betterer must be used within a git repository.');
}
46 changes: 14 additions & 32 deletions packages/betterer/src/fs/file-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,26 @@ const BETTERER_CACHE_VERSION = 2;
// Of course the actual test itself could have changed so ... 🤷‍♂️

export class BettererFileCacheΩ implements BettererFileCache {
private _cachePath: string | null = null;
private _fileHashMap: BettererFileHashMap = new Map();
private _memoryCacheMap: BettererTestCacheMap = new Map();
private _reading: Promise<string | null> | null = null;

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

public clearCache(testName: string): void {
this._memoryCacheMap.delete(testName);
public static async create(cachePath: string, configPaths: BettererFilePaths): Promise<BettererFileCacheΩ> {
return new BettererFileCacheΩ(cachePath, configPaths, await read(cachePath));
}

public async enableCache(cachePath: string): Promise<void> {
this._cachePath = cachePath;
this._memoryCacheMap = await this._readCache(this._cachePath);
public clearCache(testName: string): void {
this._memoryCacheMap.delete(testName);
}

public async writeCache(): Promise<void> {
if (!this._cachePath) {
return;
}

// Clean up any expired cache entries before writing to disk:
[...this._memoryCacheMap.entries()].forEach(([, fileHashMap]) => {
[...fileHashMap.entries()].forEach(([filePath]) => {
Expand All @@ -76,7 +75,6 @@ export class BettererFileCacheΩ implements BettererFileCache {
[...this._memoryCacheMap.entries()].forEach(([testName, absoluteFileHashMap]) => {
const relativeFileHashMap: BettererFileHashMapSerialised = {};
[...absoluteFileHashMap.entries()].forEach(([absoluteFilePath, hash]) => {
invariantΔ(this._cachePath, `\`this._cachePath\` should have been validated above!`, this._cachePath);
const relativePath = normalisedPath(path.relative(path.dirname(this._cachePath), absoluteFilePath));
relativeFileHashMap[relativePath] = hash;
});
Expand All @@ -88,10 +86,6 @@ export class BettererFileCacheΩ implements BettererFileCache {
}

public filterCached(testName: string, filePaths: BettererFilePaths): BettererFilePaths {
if (!this._cachePath) {
return filePaths;
}

const testCache = this._memoryCacheMap.get(testName) ?? (new Map() as BettererTestCacheMap);
return filePaths.filter((filePath) => {
const hash = this._fileHashMap.get(filePath);
Expand All @@ -108,10 +102,6 @@ export class BettererFileCacheΩ implements BettererFileCache {
}

public updateCache(testName: string, filePaths: BettererFilePaths): void {
if (!this._cachePath) {
return;
}

if (!this._memoryCacheMap.get(testName)) {
this._memoryCacheMap.set(testName, new Map());
}
Expand All @@ -138,27 +128,19 @@ export class BettererFileCacheΩ implements BettererFileCache {
}

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

private async _readCache(cachePath: string): Promise<BettererTestCacheMap> {
if (!this._reading) {
this._reading = read(cachePath);
}
const cache = await this._reading;
this._reading = null;
if (!cache) {
private _readCache(cacheJSON: string | null): BettererTestCacheMap {
if (!cacheJSON) {
return new Map();
}

const parsed = JSON.parse(cache) as BettererCacheFile;
const parsed = JSON.parse(cacheJSON) as BettererCacheFile;
const { version } = parsed;
if (!version) {
return new Map();
Expand Down
5 changes: 4 additions & 1 deletion packages/betterer/src/fs/file-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ export class BettererFileResolverΩ implements BettererFileResolver {
}

public async filterCached(filePaths: BettererFilePaths): Promise<BettererFilePaths> {
const { versionControl } = getGlobals();
const { config, versionControl } = getGlobals();
if (!config.cache) {
return filePaths;
}
return await versionControl.api.filterCached(this.testName, filePaths);
}

Expand Down
103 changes: 32 additions & 71 deletions packages/betterer/src/fs/git.ts
Original file line number Diff line number Diff line change
@@ -1,100 +1,64 @@
import type { SimpleGit } from 'simple-git';

import type { BettererFileCache, BettererFileHashMap, BettererFilePaths, BettererVersionControl } from './types.js';
import type { BettererFileCacheΩ } from './file-cache.js';
import type {
BettererFileCache,
BettererFileHashMap,
BettererFilePath,
BettererFilePaths,
BettererVersionControl
} from './types.js';

import { BettererError } from '@betterer/errors';
import assert from 'node:assert';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { simpleGit } from 'simple-git';

import { createHash } from '../hasher.js';
import { normalisedPath } from '../utils.js';
import { BettererFileCacheΩ } from './file-cache.js';
import { read } from './reader.js';

export class BettererGitΩ implements BettererVersionControl {
private _cache: BettererFileCache | null = null;
private _configPaths: BettererFilePaths = [];
private _fileMap: BettererFileHashMap = new Map();
private _filePaths: Array<string> = [];
private _git: SimpleGit | null = null;
private _gitDir: string | null = null;
private _rootDir: string | null = null;
private _syncing: Promise<void> | null = null;

public async add(resultsPath: string): Promise<void> {
assert(this._git);
await this._git.add(resultsPath);
private constructor(
private _git: SimpleGit,
private _rootDir: BettererFilePath
) {}

public static async create(
cache: BettererFileCache | null,
versionControlPath: BettererFilePath
): Promise<BettererGitΩ> {
const git = simpleGit(versionControlPath);
const versionControl = new BettererGitΩ(git, versionControlPath);
await versionControl.sync(cache);
return versionControl;
}

public filterCached(testName: string, filePaths: BettererFilePaths): BettererFilePaths {
assert(this._cache);
return this._cache.filterCached(testName, filePaths);
public async add(resultsPath: string): Promise<void> {
await this._git.add(resultsPath);
}

public filterIgnored(filePaths: BettererFilePaths): BettererFilePaths {
return filePaths.filter((absolutePath) => this._fileMap.get(absolutePath));
}

public clearCache(testName: string): void {
assert(this._cache);
this._cache.clearCache(testName);
}

public async enableCache(cachePath: string): Promise<void> {
assert(this._cache);
await this._cache.enableCache(cachePath);
}

public updateCache(testName: string, filePaths: BettererFilePaths): void {
assert(this._cache);
this._cache.updateCache(testName, filePaths);
}

public writeCache(): Promise<void> {
assert(this._cache);
return this._cache.writeCache();
}

public getFilePaths(): BettererFilePaths {
return this._filePaths;
}

public async init(configPaths: BettererFilePaths, cwd: string): Promise<string> {
this._configPaths = configPaths;
this._gitDir = await this._findGitRoot(cwd);
this._rootDir = path.dirname(this._gitDir);
this._git = simpleGit(this._rootDir);
this._cache = new BettererFileCacheΩ(this._configPaths);
await this.sync();
return this._rootDir;
}

public async sync(): Promise<void> {
public async sync(cache: BettererFileCache | null): Promise<void> {
if (this._syncing) {
await this._syncing;
return;
}
this._syncing = this._sync();
this._syncing = this._sync(cache);
await this._syncing;
this._syncing = null;
}

private async _findGitRoot(cwd: string): Promise<string> {
let dir = cwd;
while (dir !== path.parse(dir).root) {
try {
const gitPath = path.join(dir, '.git');
await fs.access(gitPath);
return gitPath;
} catch {
dir = path.join(dir, '..');
}
}
throw new BettererError('.git directory not found. Betterer must be used within a git repository.');
}

private async _getFileHash(filePath: string): Promise<string | null> {
const content = await read(filePath);
if (content == null) {
Expand Down Expand Up @@ -123,25 +87,19 @@ export class BettererGitΩ implements BettererVersionControl {
return [hash, relativePath];
}

private async _sync(): Promise<void> {
private async _sync(cache: BettererFileCache | null): Promise<void> {
this._fileMap = new Map();
this._filePaths = [];

assert(this._cache);
assert(this._git);
assert(this._rootDir);
const treeOutput = await this._git.raw(['ls-tree', '--full-tree', '-r', 'HEAD']);
const fileInfo = this._toLines(treeOutput).map((line) => this._toFileInfo(line));

const fileHashes: Record<string, string | null> = {};

// Collect hashes from git:
const treeOutput = await this._git.raw(['ls-tree', '--full-tree', '-r', 'HEAD']);
const fileInfo = this._toLines(treeOutput).map((line) => this._toFileInfo(line));
fileInfo.forEach((fileInfo) => {
const [hash, relativePath] = fileInfo;
assert(this._rootDir);
const absolutePath = toAbsolutePath(this._rootDir, relativePath);
fileHashes[absolutePath] = hash;
return absolutePath;
});

// Collect hashes for modified files:
Expand Down Expand Up @@ -178,7 +136,10 @@ export class BettererGitΩ implements BettererVersionControl {
})
);

const cacheΩ = this._cache as BettererFileCacheΩ;
if (!cache) {
return;
}
const cacheΩ = cache as BettererFileCacheΩ;
cacheΩ.setHashes(this._fileMap);
}
}
Expand Down
Loading