Fix some tests. Boy tests run slow

This commit is contained in:
thepaperpilot 2023-01-08 10:15:46 -06:00
parent 5f3dd1162d
commit 7593fea512
2 changed files with 326 additions and 280 deletions

View file

@ -32,14 +32,18 @@ function isVariableFormula(value: FormulaSource): value is VariableFormula {
} }
function calculateInvertibility(...inputs: FormulaSource[]) { function calculateInvertibility(...inputs: FormulaSource[]) {
const invertible = !inputs.some(input => input instanceof Formula && !input.invertible); if (inputs.some(input => input instanceof Formula && !input.invertible)) {
const hasVariable =
invertible &&
inputs.filter(input => input instanceof Formula && input.invertible && input.hasVariable)
.length === 1;
return { return {
invertible, invertible: false,
hasVariable hasVariable: false
};
}
const numVariables = inputs.filter(
input => input instanceof Formula && input.invertible && input.hasVariable
).length;
return {
invertible: numVariables <= 1,
hasVariable: numVariables === 1
}; };
} }
@ -622,11 +626,9 @@ export default class Formula {
return new Formula( return new Formula(
() => this.evaluate().sub(unrefFormulaSource(v)), () => this.evaluate().sub(unrefFormulaSource(v)),
invertible invertible
? value => ? isVariableFormula(this)
Decimal.add( ? value => Decimal.add(value, unrefFormulaSource(v))
value, : value => Decimal.sub(this.evaluate(), value)
isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate()
)
: undefined, : undefined,
hasVariable hasVariable
); );

View file

@ -1,6 +1,6 @@
import Formula, { InvertibleFormula } from "game/formulas"; import Formula, { FormulaSource, InvertibleFormula } from "game/formulas";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource, format } from "util/bignum";
import { beforeAll, describe, expect, test } from "vitest"; import { beforeAll, describe, expect, test, vi } from "vitest";
import { ref } from "vue"; import { ref } from "vue";
type FormulaFunctions = keyof Formula & keyof typeof Formula & keyof typeof Decimal; type FormulaFunctions = keyof Formula & keyof typeof Formula & keyof typeof Decimal;
@ -9,8 +9,34 @@ interface FixedLengthArray<T, L extends number> extends ArrayLike<T> {
length: L; length: L;
} }
function compare_tolerance(value: DecimalSource) { expect.extend({
return (other: DecimalSource) => Decimal.eq_tolerance(value, other); compare_tolerance(received, expected) {
const { isNot } = this;
return {
// do not alter your "pass" based on isNot. Vitest does it for you
pass: Decimal.eq_tolerance(received, expected),
message: () =>
`Expected ${received} to${
(isNot as boolean) ? " not" : ""
} be close to ${expected}`,
expected: format(expected),
actual: format(received)
};
}
});
interface CustomMatchers<R = unknown> {
compare_tolerance(expected: DecimalSource): R;
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Vi {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Assertion extends CustomMatchers {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
} }
function testConstant( function testConstant(
@ -23,17 +49,20 @@ function testConstant(
beforeAll(() => { beforeAll(() => {
formula = formulaFunc(); formula = formulaFunc();
}); });
test("evaluates correctly", () => expect(formula.evaluate()).toEqual(expectedValue)); test("evaluates correctly", async () =>
test("inverts correctly", () => expect(formula.invert(10)).toEqual(expectedValue)); expect(formula.evaluate()).compare_tolerance(expectedValue));
test("is invertible", () => expect(formula.invertible).toBe(true)); test("invert is pass-through", async () =>
test("is not marked as having a variable", () => expect(formula.hasVariable).toBe(false)); expect(formula.invert(25)).compare_tolerance(25));
test("is invertible", async () => expect(formula.invertible).toBe(true));
test("is not marked as having a variable", async () =>
expect(formula.hasVariable).toBe(false));
}); });
} }
// Utility function that will test all the different // Utility function that will test all the different
// It's a lot of tests, but I'd rather be exhaustive // It's a lot of tests, but I'd rather be exhaustive
function testFormula<T extends FormulaFunctions>( function testFormula<T extends FormulaFunctions>(
functionNames: readonly T[], functionName: T,
args: Readonly<FixedLengthArray<number, Parameters<typeof Formula[T]>["length"]>>, args: Readonly<FixedLengthArray<number, Parameters<typeof Formula[T]>["length"]>>,
invertible = true invertible = true
) { ) {
@ -43,8 +72,7 @@ function testFormula<T extends FormulaFunctions>(
value = testValueFormulas[args[0]].evaluate(); value = testValueFormulas[args[0]].evaluate();
}); });
functionNames.forEach(name => { let testName = functionName + "(";
let testName = name + "(";
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (i !== 0) { if (i !== 0) {
testName += ", "; testName += ", ";
@ -54,7 +82,7 @@ function testFormula<T extends FormulaFunctions>(
testName += ")"; testName += ")";
describe(testName, () => { describe(testName, () => {
let expectedEvaluation: Decimal | undefined; let expectedEvaluation: Decimal | undefined;
let formulaArgs: Formula[]; const formulaArgs: Formula[] = [];
let staticFormula: Formula; let staticFormula: Formula;
let instanceFormula: Formula; let instanceFormula: Formula;
beforeAll(() => { beforeAll(() => {
@ -63,135 +91,136 @@ function testFormula<T extends FormulaFunctions>(
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
staticFormula = Formula[name](...formulaArgs); staticFormula = Formula[functionName](...formulaArgs);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
instanceFormula = formulaArgs[0][name](...formulaArgs.slice(1)); instanceFormula = formulaArgs[0][functionName](...formulaArgs.slice(1));
try { try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
expectedEvaluation = Decimal[name](...args); expectedEvaluation = Decimal[functionName](...args);
} catch { } catch {
// If this is an invalid Decimal operation, then ignore this test case // If this is an invalid Decimal operation, then ignore this test case
return; return;
} }
}); });
test("Static formula is not marked as having a variable", () => test("Static formula is not marked as having a variable", async () =>
expect(staticFormula.hasVariable).toBe(false)); expect(staticFormula.hasVariable).toBe(false));
test("Static function evaluates correctly", () => test("Static function evaluates correctly", async () =>
expectedEvaluation != null && expectedEvaluation != null &&
expect(staticFormula.evaluate()).toSatisfy(compare_tolerance(expectedEvaluation))); expect(staticFormula.evaluate()).compare_tolerance(expectedEvaluation));
test("Static function invertible", () => test("Static function invertible", async () =>
expect(staticFormula.invertible).toBe(invertible)); expect(staticFormula.invertible).toBe(invertible));
if (invertible) { if (invertible) {
test("Static function inverts correctly", () => test("Static function inverts correctly", async () =>
expectedEvaluation != null && expectedEvaluation != null &&
!Decimal.isNaN(expectedEvaluation) && !Decimal.isNaN(expectedEvaluation) &&
expect(staticFormula.invert(expectedEvaluation)).toSatisfy( expect(staticFormula.invert(expectedEvaluation)).compare_tolerance(value));
compare_tolerance(value)
));
} }
// Do those tests again but for non-static methods // Do those tests again but for non-static methods
test("Instance formula is not marked as having a variable", () => test("Instance formula is not marked as having a variable", async () =>
expect(instanceFormula.hasVariable).toBe(false)); expect(instanceFormula.hasVariable).toBe(false));
test("Instance function evaluates correctly", () => test("Instance function evaluates correctly", async () =>
expectedEvaluation != null && expectedEvaluation != null &&
expect(instanceFormula.evaluate()).toSatisfy( expect(instanceFormula.evaluate()).compare_tolerance(expectedEvaluation));
compare_tolerance(expectedEvaluation) test("Instance function invertible", async () =>
));
test("Instance function invertible", () =>
expect(instanceFormula.invertible).toBe(invertible)); expect(instanceFormula.invertible).toBe(invertible));
if (invertible) { if (invertible) {
test("Instance function inverts correctly", () => test("Instance function inverts correctly", async () =>
expectedEvaluation != null && expectedEvaluation != null &&
!Decimal.isNaN(expectedEvaluation) && !Decimal.isNaN(expectedEvaluation) &&
expect(instanceFormula.invert(expectedEvaluation)).toSatisfy( expect(instanceFormula.invert(expectedEvaluation)).compare_tolerance(value));
compare_tolerance(value)
));
} }
}); });
}); }
function testAliases<T extends FormulaFunctions[]>(
formula: Formula,
aliases: T,
args: FormulaSource[]
) {
const spy = vi.spyOn(formula, aliases[0]);
expect(spy).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
aliases.slice(1).forEach(name => formula[name](...args));
expect(spy).toHaveBeenCalledTimes(aliases.length - 1);
} }
const testValues = [-2.5, -1, -0.1, 0, 0.1, 1, 2.5] as const; const testValues = [-2.5, -1, -0.1, 0, 0.1, 1, 2.5] as const;
let testValueFormulas: InvertibleFormula[] = []; let testValueFormulas: InvertibleFormula[] = [];
const invertibleZeroParamFunctionNames = [ const invertibleZeroParamFunctionNames = [
["neg", "negate", "negated"], "neg",
["recip", "reciprocal", "reciprocate"], "recip",
["log10"], "log10",
["log2"], "log2",
["ln"], "ln",
["pow10"], "pow10",
["exp"], "exp",
["sqr"], "sqr",
["sqrt"], "sqrt",
["cube"], "cube",
["cbrt"], "cbrt",
["lambertw"], "lambertw",
["ssqrt"], "ssqrt",
["sin"], "sin",
["cos"], "cos",
["tan"], "tan",
["asin"], "asin",
["acos"], "acos",
["atan"], "atan",
["sinh"], "sinh",
["cosh"], "cosh",
["tanh"], "tanh",
["asinh"], "asinh",
["acosh"], "acosh",
["atanh"] "atanh"
] as const; ] as const;
const nonInvertibleZeroParamFunctionNames = [ const nonInvertibleZeroParamFunctionNames = [
["abs"], "abs",
["sign", "sgn"], "sign",
["round"], "round",
["floor"], "floor",
["ceil"], "ceil",
["trunc"], "trunc",
["pLog10"], "pLog10",
["absLog10"], "absLog10",
["factorial"], "factorial",
["gamma"], "gamma",
["lngamma"] "lngamma"
] as const; ] as const;
const invertibleOneParamFunctionNames = [ const invertibleOneParamFunctionNames = [
["add", "plus"], "add",
["sub", "subtract", "minus"], "sub",
["mul", "multiply", "times"], "mul",
["div", "divide"], "div",
["log", "logarithm"], "log",
["pow"], "pow",
["root"], "root",
["slog"] "slog"
] as const; ] as const;
const nonInvertibleOneParamFunctionNames = [ const nonInvertibleOneParamFunctionNames = [
["max"], "max",
["min"], "min",
["maxabs"], "maxabs",
["minabs"], "minabs",
["clampMin"], "clampMin",
["clampMax"], "clampMax",
["layeradd10"] "layeradd10"
] as const; ] as const;
const invertibleTwoParamFunctionNames = [["tetrate"], ["layeradd"]] as const; const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const;
const nonInvertibleTwoParamFunctionNames = [ const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const;
["clamp"],
["iteratedexp"],
["iteratedlog"],
["pentate"]
] as const;
describe("Creating Formulas", () => { describe.concurrent("Creating Formulas", () => {
beforeAll(() => { beforeAll(() => {
testValueFormulas = testValues.map(v => Formula.constant(v)); testValueFormulas = testValues.map(v => Formula.constant(v));
}); });
@ -204,6 +233,23 @@ describe("Creating Formulas", () => {
testConstant("ref", () => Formula.constant(ref(10))); testConstant("ref", () => Formula.constant(ref(10)));
}); });
// Test that these are just pass-throughts so we don't need to test each one everywhere else
describe("Function aliases", () => {
let formula: Formula;
beforeAll(() => {
formula = Formula.constant(10);
});
test("neg", async () => testAliases(formula, ["neg", "negate", "negated"], [0]));
test("recip", async () =>
testAliases(formula, ["recip", "reciprocal", "reciprocate"], [0]));
test("sign", async () => testAliases(formula, ["sign", "sgn"], [0]));
test("add", async () => testAliases(formula, ["add", "plus"], [0]));
test("sub", async () => testAliases(formula, ["sub", "subtract", "minus"], [0]));
test("mul", async () => testAliases(formula, ["mul", "multiply", "times"], [0]));
test("div", async () => testAliases(formula, ["div", "divide"], [1]));
test("log", async () => testAliases(formula, ["log", "logarithm"], [0]));
});
describe("Invertible 0-param", () => { describe("Invertible 0-param", () => {
invertibleZeroParamFunctionNames.forEach(names => { invertibleZeroParamFunctionNames.forEach(names => {
for (let i = 0; i < testValues.length; i++) { for (let i = 0; i < testValues.length; i++) {
@ -258,6 +304,7 @@ describe("Creating Formulas", () => {
} }
}); });
}); });
});
describe("Variables", () => { describe("Variables", () => {
let variable: Formula; let variable: Formula;
@ -267,15 +314,13 @@ describe("Creating Formulas", () => {
constant = Formula.constant(10); constant = Formula.constant(10);
}); });
test("Created variable is marked as a variable", () => test("Created variable is marked as a variable", () => expect(variable.hasVariable).toBe(true));
expect(variable.hasVariable).toBe(true));
test("Evaluate() returns variable's value", () => test("Evaluate() returns variable's value", () =>
expect(variable.evaluate()).toSatisfy(compare_tolerance(10))); expect(variable.evaluate()).compare_tolerance(10));
test("Invert() is pass-through", () => test("Invert() is pass-through", () => expect(variable.invert(100)).compare_tolerance(100));
expect(variable.invert(100)).toSatisfy(compare_tolerance(100)));
test("Nested variable is marked as having a variable", () => test("Nested variable is marked as having a variable", () =>
expect(variable.add(10).div(3).pow(2).hasVariable).toBe(false)); expect(variable.add(10).div(3).pow(2).hasVariable).toBe(true));
test("Nested non-variable is marked as not having a variable", () => test("Nested non-variable is marked as not having a variable", () =>
expect(constant.add(10).div(3).pow(2).hasVariable).toBe(false)); expect(constant.add(10).div(3).pow(2).hasVariable).toBe(false));
@ -284,13 +329,13 @@ describe("Creating Formulas", () => {
expect(formula.invertible).toBe(expectedBool); expect(formula.invertible).toBe(expectedBool);
expect(formula.hasVariable).toBe(expectedBool); expect(formula.hasVariable).toBe(expectedBool);
} }
invertibleZeroParamFunctionNames.flat().forEach(name => { invertibleZeroParamFunctionNames.forEach(name => {
describe(name, () => { describe(name, () => {
test(`${name}(var) is marked as invertible and having a variable`, () => test(`${name}(var) is marked as invertible and having a variable`, () =>
checkFormula(Formula[name](variable))); checkFormula(Formula[name](variable)));
}); });
}); });
invertibleOneParamFunctionNames.flat().forEach(name => { invertibleOneParamFunctionNames.forEach(name => {
describe(name, () => { describe(name, () => {
test(`${name}(var, const) is marked as invertible and having a variable`, () => test(`${name}(var, const) is marked as invertible and having a variable`, () =>
checkFormula(Formula[name](variable, constant))); checkFormula(Formula[name](variable, constant)));
@ -300,7 +345,7 @@ describe("Creating Formulas", () => {
checkFormula(Formula[name](variable, variable), false)); checkFormula(Formula[name](variable, variable), false));
}); });
}); });
invertibleTwoParamFunctionNames.flat().forEach(name => { invertibleTwoParamFunctionNames.forEach(name => {
describe(name, () => { describe(name, () => {
test(`${name}(var, const, const) is marked as invertible and having a variable`, () => test(`${name}(var, const, const) is marked as invertible and having a variable`, () =>
checkFormula(Formula[name](variable, constant, constant))); checkFormula(Formula[name](variable, constant, constant)));
@ -325,13 +370,13 @@ describe("Creating Formulas", () => {
expect(formula.invertible).toBe(false); expect(formula.invertible).toBe(false);
expect(formula.hasVariable).toBe(false); expect(formula.hasVariable).toBe(false);
} }
nonInvertibleZeroParamFunctionNames.flat().forEach(name => { nonInvertibleZeroParamFunctionNames.forEach(name => {
describe(name, () => { describe(name, () => {
test(`${name}(var) is marked as not invertible and not having a variable`, () => test(`${name}(var) is marked as not invertible and not having a variable`, () =>
checkFormula(Formula[name](variable))); checkFormula(Formula[name](variable)));
}); });
}); });
nonInvertibleOneParamFunctionNames.flat().forEach(name => { nonInvertibleOneParamFunctionNames.forEach(name => {
describe(name, () => { describe(name, () => {
test(`${name}(var, const) is marked as not invertible and not having a variable`, () => test(`${name}(var, const) is marked as not invertible and not having a variable`, () =>
checkFormula(Formula[name](variable, constant))); checkFormula(Formula[name](variable, constant)));
@ -341,13 +386,13 @@ describe("Creating Formulas", () => {
checkFormula(Formula[name](variable, variable))); checkFormula(Formula[name](variable, variable)));
}); });
}); });
nonInvertibleTwoParamFunctionNames.flat().forEach(name => { nonInvertibleTwoParamFunctionNames.forEach(name => {
describe(name, () => { describe(name, () => {
test(`${name}(var, const, const) is marked as invertible and having a variable`, () => test(`${name}(var, const, const) is marked as not invertible and not having a variable`, () =>
checkFormula(Formula[name](variable, constant, constant))); checkFormula(Formula[name](variable, constant, constant)));
test(`${name}(const, var, const) is marked as invertible and having a variable`, () => test(`${name}(const, var, const) is marked as not invertible and not having a variable`, () =>
checkFormula(Formula[name](constant, variable, constant))); checkFormula(Formula[name](constant, variable, constant)));
test(`${name}(const, const, var) is marked as invertible and having a variable`, () => test(`${name}(const, const, var) is marked as not invertible and not having a variable`, () =>
checkFormula(Formula[name](constant, constant, variable))); checkFormula(Formula[name](constant, constant, variable)));
test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () =>
checkFormula(Formula[name](variable, variable, constant))); checkFormula(Formula[name](variable, variable, constant)));
@ -368,39 +413,38 @@ describe("Creating Formulas", () => {
variable = Formula.variable(2); variable = Formula.variable(2);
constant = Formula.constant(3); constant = Formula.constant(3);
}); });
invertibleOneParamFunctionNames.flat().forEach(name => invertibleOneParamFunctionNames.forEach(name =>
describe(name, () => { describe(name, () => {
test(`${name}(var, const).invert()`, () => { test(`${name}(var, const).invert()`, () => {
const formula = Formula[name](variable, constant); const formula = Formula[name](variable, constant);
const result = formula.evaluate(); const result = formula.evaluate();
expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); expect(formula.invert(result)).compare_tolerance(2);
}); });
test(`${name}(const, var).invert()`, () => { test(`${name}(const, var).invert()`, () => {
const formula = Formula[name](constant, variable); const formula = Formula[name](constant, variable);
const result = formula.evaluate(); const result = formula.evaluate();
expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); expect(formula.invert(result)).compare_tolerance(2);
}); });
}) })
); );
invertibleTwoParamFunctionNames.flat().forEach(name => invertibleTwoParamFunctionNames.forEach(name =>
describe(name, () => { describe(name, () => {
test(`${name}(var, const, const).invert()`, () => { test(`${name}(var, const, const).invert()`, () => {
const formula = Formula[name](variable, constant, constant); const formula = Formula[name](variable, constant, constant);
const result = formula.evaluate(); const result = formula.evaluate();
expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); expect(formula.invert(result)).compare_tolerance(2);
}); });
test(`${name}(const, var, const).invert()`, () => { test(`${name}(const, var, const).invert()`, () => {
const formula = Formula[name](constant, variable, constant); const formula = Formula[name](constant, variable, constant);
const result = formula.evaluate(); const result = formula.evaluate();
expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); expect(formula.invert(result)).compare_tolerance(2);
}); });
test(`${name}(const, const, var).invert()`, () => { test(`${name}(const, const, var).invert()`, () => {
const formula = Formula[name](constant, constant, variable); const formula = Formula[name](constant, constant, variable);
const result = formula.evaluate(); const result = formula.evaluate();
expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); expect(formula.invert(result)).compare_tolerance(2);
}); });
}) })
); );
}); });
}); });
});