chore: replace eslint & prettier w/ biomejs (#21)

* chore: replace eslint & prettier w/ biomejs

* fix syntax error in ci.yml workflow

* ensure that build CI jobs only run if check job succeeds to save resources
This commit is contained in:
M. George Hansen 2024-07-15 08:36:52 -07:00
parent d67e4c81ad
commit e319626a1a
44 changed files with 2408 additions and 5691 deletions

View file

@ -6,13 +6,13 @@
import { areEqual, displayValue, matches } from "./utils";
class ExpectError extends Error {
public constructor(reason: string, expected: unknown, actual: unknown) {
super(
`${reason}\n` +
`\texpected: ${displayValue(expected)}\n` +
`\tactual : ${displayValue(actual)}`,
);
}
public constructor(reason: string, expected: unknown, actual: unknown) {
super(
`${reason}\n` +
`\texpected: ${displayValue(expected)}\n` +
`\tactual : ${displayValue(actual)}`,
);
}
}
/**
@ -24,138 +24,134 @@ class ExpectError extends Error {
* Expect.
*/
export class Expect<T> {
protected readonly value: T;
protected readonly value: T;
/**
* Create a new expectation around a value.
*
* @param value Value to place assertions upon.
*/
public constructor(value: T) {
this.value = value;
}
/**
* Create a new expectation around a value.
*
* @param value Value to place assertions upon.
*/
public constructor(value: T) {
this.value = value;
}
/**
* Expect the value to equal an expected value.
*
* Note that strict equality checking is used for primitives and structural
* equality is used for objects.
*
* @param expected Expected value.
*
* @throws ExpectError If the actual value does not equal the expected value.
*/
public toEqual(expected: T): void {
if (!areEqual(this.value, expected)) {
throw new ExpectError(
`value does not equal expected`,
expected,
this.value,
);
}
}
/**
* Expect the value to equal an expected value.
*
* Note that strict equality checking is used for primitives and structural
* equality is used for objects.
*
* @param expected Expected value.
*
* @throws ExpectError If the actual value does not equal the expected value.
*/
public toEqual(expected: T): void {
if (!areEqual(this.value, expected)) {
throw new ExpectError(
"value does not equal expected",
expected,
this.value,
);
}
}
}
/**
* String-specific Expect assertions.
*/
export class StringExpect extends Expect<string> {
/**
* Expect the string value to match a RegExp pattern.
*
* @param pattern Regular expression to match against.
*
* @throws ExpectError If the actual value does not match the expected
* RegExp pattern.
*/
public toMatch(pattern: RegExp): void {
if (!this.value.match(pattern)) {
throw new ExpectError(
`value does not match expected pattern`,
pattern,
this.value,
);
}
}
/**
* Expect the string value to match a RegExp pattern.
*
* @param pattern Regular expression to match against.
*
* @throws ExpectError If the actual value does not match the expected
* RegExp pattern.
*/
public toMatch(pattern: RegExp): void {
if (!this.value.match(pattern)) {
throw new ExpectError(
"value does not match expected pattern",
pattern,
this.value,
);
}
}
/**
* Expect the string value to start with a particular prefix.
*
* @param prefix Prefix that the string is expected to start with.
*
* @throws ExpectError If the actual value does not start with the expected
* prefix.
*/
public toStartWith(prefix: string): void {
if (!this.value.startsWith(prefix)) {
throw new ExpectError(
`value does not start with expected prefix`,
prefix,
this.value,
);
}
}
/**
* Expect the string value to start with a particular prefix.
*
* @param prefix Prefix that the string is expected to start with.
*
* @throws ExpectError If the actual value does not start with the expected
* prefix.
*/
public toStartWith(prefix: string): void {
if (!this.value.startsWith(prefix)) {
throw new ExpectError(
"value does not start with expected prefix",
prefix,
this.value,
);
}
}
/**
* Expect the string value to end with a particular suffix.
*
* @param suffix Suffix that the string is expected to end with.
*
* @throws ExpectError If the actual value does not end with the expected
* suffix.
*/
public toEndWith(suffix: string): void {
if (!this.value.endsWith(suffix)) {
throw new ExpectError(
`value does not end with expected suffix`,
suffix,
this.value,
);
}
}
/**
* Expect the string value to end with a particular suffix.
*
* @param suffix Suffix that the string is expected to end with.
*
* @throws ExpectError If the actual value does not end with the expected
* suffix.
*/
public toEndWith(suffix: string): void {
if (!this.value.endsWith(suffix)) {
throw new ExpectError(
"value does not end with expected suffix",
suffix,
this.value,
);
}
}
}
/**
* Function-specific Expect assertions.
*/
export class FunctionExpect<T> extends Expect<() => T> {
/**
* Expect the function to throw an Error with error message matching a
* string or pattern.
*
* @param pattern String that exactly matches the error message or RegExp
* that should match the error message.
*
* @throws ExpectError If the function does not throw an error, throws a
* non-Error value, or throws an Error whose message does not match
* the expected pattern.
*/
public toThrowErrorMatching(pattern: string | RegExp): void {
try {
this.value();
} catch (error) {
if (!(error instanceof Error)) {
throw new ExpectError(
`function threw non-Error value`,
pattern,
error,
);
}
if (!matches(error.message, pattern)) {
throw new ExpectError(
`thrown Error's message does not match pattern`,
pattern,
error.message,
);
}
return;
}
throw new ExpectError(
`function did not throw expected error`,
pattern,
null,
);
}
/**
* Expect the function to throw an Error with error message matching a
* string or pattern.
*
* @param pattern String that exactly matches the error message or RegExp
* that should match the error message.
*
* @throws ExpectError If the function does not throw an error, throws a
* non-Error value, or throws an Error whose message does not match
* the expected pattern.
*/
public toThrowErrorMatching(pattern: string | RegExp): void {
try {
this.value();
} catch (error) {
if (!(error instanceof Error)) {
throw new ExpectError("function threw non-Error value", pattern, error);
}
if (!matches(error.message, pattern)) {
throw new ExpectError(
`thrown Error's message does not match pattern`,
pattern,
error.message,
);
}
return;
}
throw new ExpectError(
"function did not throw expected error",
pattern,
null,
);
}
}
/**
@ -202,11 +198,11 @@ export function expect<T>(fn: () => T): FunctionExpect<T>;
* @param value Value to place expectations upon.
*/
export function expect(value: unknown): Expect<unknown> {
if (typeof value === "string") {
return new StringExpect(value);
}
if (typeof value === "function") {
return new FunctionExpect(value as () => unknown);
}
return new Expect(value);
if (typeof value === "string") {
return new StringExpect(value);
}
if (typeof value === "function") {
return new FunctionExpect(value as () => unknown);
}
return new Expect(value);
}

View file

@ -7,68 +7,66 @@ import { expect } from "./expect";
import { displayValue, shuffle } from "./utils";
interface Test {
readonly name: string;
readonly name: string;
runTest(): void | Promise<void>;
runTest(): void | Promise<void>;
}
type TestResult = {
testName: string;
testName: string;
} & (
| {
result: "pass";
}
| {
result: "fail";
error: Error;
}
| {
result: "pass";
}
| {
result: "fail";
error: Error;
}
);
const runTest = async (test: Test): Promise<TestResult> => {
let result: TestResult;
try {
await test.runTest();
result = {
testName: test.name,
result: "pass",
};
} catch (error) {
result = {
testName: test.name,
result: "fail",
error:
error instanceof Error
? error
: new Error(
`threw non-error object: ${displayValue(error)}`,
),
};
}
return result;
let result: TestResult;
try {
await test.runTest();
result = {
testName: test.name,
result: "pass",
};
} catch (error) {
result = {
testName: test.name,
result: "fail",
error:
error instanceof Error
? error
: new Error(`threw non-error object: ${displayValue(error)}`),
};
}
return result;
};
/**
* Context object that is passed into a test suite definition.
*/
export interface TestSuiteContext {
/**
* Define a test in this test suite.
*
* Tests are functions that pass if they are executed and don't throw (or
* that resolve for async tests), and that fail if they throw an error (or
* reject for async tests).
*
* Note that tests are executed in a random order within a test suite in
* order to prevent accidentally creating order dependencies between tests,
* which can result in brittle tests and is a code smell that might indicate
* that the code under test is also brittle.
*/
test: (name: string, def: () => void | Promise<void>) => void;
/**
* Expectation builder function used to build human-readable assertions and
* errors.
*/
expect: typeof expect;
/**
* Define a test in this test suite.
*
* Tests are functions that pass if they are executed and don't throw (or
* that resolve for async tests), and that fail if they throw an error (or
* reject for async tests).
*
* Note that tests are executed in a random order within a test suite in
* order to prevent accidentally creating order dependencies between tests,
* which can result in brittle tests and is a code smell that might indicate
* that the code under test is also brittle.
*/
test: (name: string, def: () => void | Promise<void>) => void;
/**
* Expectation builder function used to build human-readable assertions and
* errors.
*/
expect: typeof expect;
}
/**
@ -85,36 +83,36 @@ export interface TestSuiteContext {
* @param def Function used to declare the tests
*/
export const testSuite = (
suiteName: string,
def: (ctx: TestSuiteContext) => void,
suiteName: string,
def: (ctx: TestSuiteContext) => void,
): void => {
const tests: Test[] = [];
const test = (name: string, runTest: () => void | Promise<void>): void => {
tests.push({ name, runTest });
};
def({ test, expect });
const tests: Test[] = [];
const test = (name: string, runTest: () => void | Promise<void>): void => {
tests.push({ name, runTest });
};
def({ test, expect });
// Randomly shuffle the tests so that we can catch accidental order
// dependencies.
shuffle(tests);
(async () => {
const results = await Promise.all(tests.map((test) => runTest(test)));
let passed = 0;
for (const testResult of results) {
if (testResult.result === "fail") {
console.error(
`[TEST FAILURE] "${suiteName}": "${testResult.testName}": ` +
`${testResult.error.stack}\n`,
);
continue;
}
passed += 1;
}
console.info(
`[TEST] suite "${suiteName}": ${passed} of ${tests.length} succeeded\n\n`,
);
if (passed < tests.length) {
process.exitCode = 1;
}
})();
// Randomly shuffle the tests so that we can catch accidental order
// dependencies.
shuffle(tests);
(async () => {
const results = await Promise.all(tests.map((test) => runTest(test)));
let passed = 0;
for (const testResult of results) {
if (testResult.result === "fail") {
console.error(
`[TEST FAILURE] "${suiteName}": "${testResult.testName}": ` +
`${testResult.error.stack}\n`,
);
continue;
}
passed += 1;
}
console.info(
`[TEST] suite "${suiteName}": ${passed} of ${tests.length} succeeded\n\n`,
);
if (passed < tests.length) {
process.exitCode = 1;
}
})();
};

View file

@ -9,43 +9,43 @@
* @param arr Array to shuffle.
*/
export const shuffle = <T>(arr: T[]): void => {
let j: number;
let x: T;
for (let i = arr.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = arr[i];
arr[i] = arr[j];
arr[j] = x;
}
let j: number;
let x: T;
for (let i = arr.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = arr[i];
arr[i] = arr[j];
arr[j] = x;
}
};
const areArraysEqual = <T>(a: T[], b: T[]): boolean => {
if (a.length != b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!areEqual(a[i], b[i])) {
return false;
}
}
return true;
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!areEqual(a[i], b[i])) {
return false;
}
}
return true;
};
const areObjectsEqual = <T extends Record<string, unknown>>(
a: T,
b: T,
a: T,
b: T,
): boolean => {
const aKeys = Object.keys(a) as Array<keyof T>;
const bKeys = Object.keys(b) as Array<keyof T>;
if (aKeys.length !== bKeys.length) {
return false;
}
for (const key of aKeys) {
if (!areEqual(a[key], b[key])) {
return false;
}
}
return true;
const aKeys = Object.keys(a) as Array<keyof T>;
const bKeys = Object.keys(b) as Array<keyof T>;
if (aKeys.length !== bKeys.length) {
return false;
}
for (const key of aKeys) {
if (!areEqual(a[key], b[key])) {
return false;
}
}
return true;
};
/**
@ -58,19 +58,19 @@ const areObjectsEqual = <T extends Record<string, unknown>>(
* @return Whether the two values are structurally equal.
*/
export const areEqual = <T>(a: T, b: T): boolean => {
if (Array.isArray(a) && Array.isArray(b)) {
return areArraysEqual(a, b);
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source;
}
if (typeof a === "object" && typeof b === "object") {
return areObjectsEqual(
a as Record<string, unknown>,
b as Record<string, unknown>,
);
}
return a === b;
if (Array.isArray(a) && Array.isArray(b)) {
return areArraysEqual(a, b);
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source;
}
if (typeof a === "object" && typeof b === "object") {
return areObjectsEqual(
a as Record<string, unknown>,
b as Record<string, unknown>,
);
}
return a === b;
};
/**
@ -84,10 +84,10 @@ export const areEqual = <T>(a: T, b: T): boolean => {
* @param pattern String or RegExp pattern to match value against.
*/
export const matches = (value: string, pattern: string | RegExp): boolean => {
if (typeof pattern === "string") {
return value === pattern;
}
return pattern.test(value);
if (typeof pattern === "string") {
return value === pattern;
}
return pattern.test(value);
};
/**
@ -102,11 +102,11 @@ export const matches = (value: string, pattern: string | RegExp): boolean => {
* @return Rendered value to display.
*/
export const displayValue = (value: unknown): string => {
if (value === undefined) {
return "undefined";
}
if (value instanceof RegExp) {
return value.toString();
}
return JSON.stringify(value);
if (value === undefined) {
return "undefined";
}
if (value instanceof RegExp) {
return value.toString();
}
return JSON.stringify(value);
};