emigrate/packages/cli/src/test-utils.ts
Joakim Carlstein 26240f49ff fix(mysql): make sure migrations are run in order when run concurrently
Now we either lock all or none of the migrations to run,
to make sure they are not out of order when multiple instances of Emigrate run concurrently.
2025-04-24 15:57:44 +02:00

134 lines
3.9 KiB
TypeScript

import { mock, type Mock } from 'node:test';
import path from 'node:path';
import assert from 'node:assert';
import {
type SerializedError,
type EmigrateReporter,
type FailedMigrationHistoryEntry,
type MigrationHistoryEntry,
type MigrationMetadata,
type NonFailedMigrationHistoryEntry,
type Storage,
} from '@emigrate/types';
import { toSerializedError } from './errors.js';
export type Mocked<T> = {
// @ts-expect-error - This is a mock
[K in keyof T]: Mock<T[K]>;
};
export async function noop(): Promise<void> {
// noop
}
export function getErrorCause(error: Error | undefined): Error | SerializedError | undefined {
if (error?.cause instanceof Error) {
return error.cause;
}
if (typeof error?.cause === 'object' && error.cause !== null) {
return error.cause as unknown as SerializedError;
}
return undefined;
}
export function getMockedStorage(historyEntries: Array<string | MigrationHistoryEntry>): Mocked<Storage> {
return {
lock: mock.fn(async (migrations) => migrations),
unlock: mock.fn(async () => {
// void
}),
getHistory: mock.fn(async function* () {
yield* toEntries(historyEntries);
}),
remove: mock.fn(),
onSuccess: mock.fn(),
onError: mock.fn(),
end: mock.fn(),
};
}
export function getMockedReporter(): Mocked<Required<EmigrateReporter>> {
return {
onFinished: mock.fn(noop),
onInit: mock.fn(noop),
onAbort: mock.fn(noop),
onCollectedMigrations: mock.fn(noop),
onLockedMigrations: mock.fn(noop),
onNewMigration: mock.fn(noop),
onMigrationStart: mock.fn(noop),
onMigrationSuccess: mock.fn(noop),
onMigrationError: mock.fn(noop),
onMigrationSkip: mock.fn(noop),
};
}
export function toMigration(cwd: string, directory: string, name: string): MigrationMetadata {
return {
name,
filePath: `${cwd}/${directory}/${name}`,
relativeFilePath: `${directory}/${name}`,
extension: path.extname(name),
directory,
cwd,
};
}
export function toMigrations(cwd: string, directory: string, names: string[]): MigrationMetadata[] {
return names.map((name) => toMigration(cwd, directory, name));
}
export function toEntry(name: MigrationHistoryEntry): MigrationHistoryEntry;
export function toEntry<S extends MigrationHistoryEntry['status']>(
name: string,
status?: S,
): S extends 'failed' ? FailedMigrationHistoryEntry : NonFailedMigrationHistoryEntry;
export function toEntry(name: string | MigrationHistoryEntry, status?: 'done' | 'failed'): MigrationHistoryEntry {
if (typeof name !== 'string') {
return name.status === 'failed' ? name : name;
}
if (status === 'failed') {
return {
name,
status,
date: new Date(),
error: { name: 'Error', message: 'Failed' },
};
}
return {
name,
status: status ?? 'done',
date: new Date(),
};
}
export function toEntries(
names: Array<string | MigrationHistoryEntry>,
status?: MigrationHistoryEntry['status'],
): MigrationHistoryEntry[] {
return names.map((name) => (typeof name === 'string' ? toEntry(name, status) : name));
}
export function assertErrorEqualEnough(actual?: Error | SerializedError, expected?: Error, message?: string): void {
if (expected === undefined) {
assert.strictEqual(actual, undefined);
return;
}
const {
cause: actualCause,
stack: actualStack,
...actualError
} = actual instanceof Error ? toSerializedError(actual) : actual ?? {};
const { cause: expectedCause, stack: expectedStack, ...expectedError } = toSerializedError(expected);
// @ts-expect-error Ignore
const { stack: actualCauseStack, ...actualCauseRest } = actualCause ?? {};
// @ts-expect-error Ignore
const { stack: expectedCauseStack, ...expectedCauseRest } = expectedCause ?? {};
assert.deepStrictEqual(actualError, expectedError, message);
assert.deepStrictEqual(actualCauseRest, expectedCauseRest, message ? `${message} (cause)` : undefined);
}