Better RamCost testing (see desc)

* RamCostGenerator will have an error if ramcosts are defined for nonexistent functions, in addition to error if not all functions have ram costs defined
* Removed a few random blank comment lines in NetscriptDefinitions.d.ts
* RamCalculation.test.ts checks exact expected static and dynamic ram usage from (almost) every function, based on defined RamCosts in RamCostGenerator.
This commit is contained in:
Snarling 2022-10-05 10:42:07 -04:00
parent e71e5988cb
commit 8bb88a5080
3 changed files with 95 additions and 93 deletions

@ -1,19 +1,12 @@
import { Player } from "../Player";
import { NSFull } from "../NetscriptFunctions";
import { NS as INS } from "../ScriptEditor/NetscriptDefinitions";
import { INetscriptExtra } from "../NetscriptFunctions/Extra";
/** This type assumes any value that isn't an API layer or a function has been omitted (args and enum) */
type RamCostTree<API> = {
[Property in keyof API]: API[Property] extends () => void
? number | (() => void)
: API[Property] extends object
? RamCostTree<API[Property]>
: never;
[Property in keyof API]: API[Property] extends Function ? number | (() => number) : RamCostTree<API[Property]>;
};
// TODO remember to update RamCalculations.js and WorkerScript.js
// RAM costs for Netscript functions
/** Constants for assigning costs to ns functions */
export const RamCostConstants: Record<string, number> = {
ScriptBaseRamCost: 1.6,
ScriptDomRamCost: 25,
@ -120,7 +113,7 @@ const hacknet = {
getHashUpgradeLevel: 0,
getStudyMult: 0,
getTrainingMult: 0,
};
} as const;
// Stock API
const stock = {
@ -149,7 +142,7 @@ const stock = {
purchase4SMarketDataTixApi: RamCostConstants.ScriptBuySellStockRamCost,
purchaseWseAccount: RamCostConstants.ScriptBuySellStockRamCost,
purchaseTixApi: RamCostConstants.ScriptBuySellStockRamCost,
};
} as const;
// Singularity API
const singularity = {
@ -208,7 +201,7 @@ const singularity = {
b1tflum3: SF4Cost(16),
destroyW0r1dD43m0n: SF4Cost(32),
getCurrentWork: SF4Cost(0.5),
};
} as const;
// Gang API
const gang = {
@ -233,7 +226,7 @@ const gang = {
setTerritoryWarfare: RamCostConstants.ScriptGangApiBaseRamCost / 2,
getChanceToWinClash: RamCostConstants.ScriptGangApiBaseRamCost,
getBonusTime: 0,
};
} as const;
// Bladeburner API
const bladeburner = {
@ -272,12 +265,12 @@ const bladeburner = {
joinBladeburnerFaction: RamCostConstants.ScriptBladeburnerApiBaseRamCost,
joinBladeburnerDivision: RamCostConstants.ScriptBladeburnerApiBaseRamCost,
getBonusTime: 0,
};
} as const;
const infiltration = {
getPossibleLocations: RamCostConstants.ScriptInfiltrationGetLocations,
getInfiltration: RamCostConstants.ScriptInfiltrationGetInfiltrations,
};
} as const;
// Coding Contract API
const codingcontract = {
@ -286,7 +279,7 @@ const codingcontract = {
getData: RamCostConstants.ScriptCodingContractBaseRamCost / 2,
getDescription: RamCostConstants.ScriptCodingContractBaseRamCost / 2,
getNumTriesRemaining: RamCostConstants.ScriptCodingContractBaseRamCost / 5,
};
} as const;
// Duplicate Sleeve API
const sleeve = {
@ -308,7 +301,7 @@ const sleeve = {
setToBladeburnerAction: RamCostConstants.ScriptSleeveBaseRamCost,
getSleeveAugmentationPrice: RamCostConstants.ScriptSleeveBaseRamCost,
getSleeveAugmentationRepReq: RamCostConstants.ScriptSleeveBaseRamCost,
};
} as const;
// Stanek API
const stanek = {
@ -323,7 +316,7 @@ const stanek = {
getFragment: RamCostConstants.ScriptStanekFragmentAt,
removeFragment: RamCostConstants.ScriptStanekDeleteAt,
acceptGift: RamCostConstants.ScriptStanekAcceptGift,
};
} as const;
// UI API
const ui = {
@ -336,7 +329,7 @@ const ui = {
getGameInfo: 0,
clearTerminal: 0,
windowSize: 0,
};
} as const;
// Grafting API
const grafting = {
@ -344,7 +337,7 @@ const grafting = {
getAugmentationGraftTime: 3.75,
getGraftableAugmentations: 5,
graftAugmentation: 7.5,
};
} as const;
const corporation = {
getMaterialNames: 0,
@ -412,16 +405,17 @@ const corporation = {
hasResearched: 0,
setAutoJobAssignment: 0,
getOfficeSizeUpgradeCost: 0,
};
} as const;
const SourceRamCosts = {
args: undefined as unknown as never[], // special use case
enums: undefined as unknown as never,
/** RamCosts guaranteed to match ns structure 1:1 (aside from args and enums).
* An error will be generated if there are missing OR additional ram costs defined.
* To avoid errors, define every function in NetscriptDefinition.d.ts and NetscriptFunctions,
* and have a ram cost associated here. */
export const RamCosts: RamCostTree<Omit<NSFull, "args" | "enums">> = {
corporation,
hacknet,
stock,
singularity,
...singularity, // singularity is in namespace & toplevel
gang,
bladeburner,
infiltration,
@ -601,13 +595,7 @@ const SourceRamCosts = {
factionGains: 0,
},
},
};
export const RamCosts: Record<string, any> = SourceRamCosts;
// This line in particular is there so typescript typechecks that we are not missing any static ram cost.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _typecheck: RamCostTree<INS & INetscriptExtra> = SourceRamCosts;
} as const;
export function getRamCost(...args: string[]): number {
if (args.length === 0) {
@ -615,7 +603,7 @@ export function getRamCost(...args: string[]): number {
return 0;
}
let curr = RamCosts[args[0]];
let curr = RamCosts[args[0] as keyof typeof RamCosts];
for (let i = 1; i < args.length; ++i) {
if (curr == null) {
console.warn(`Invalid function passed to getRamCost: ${args}`);
@ -627,7 +615,7 @@ export function getRamCost(...args: string[]): number {
break;
}
curr = curr[args[i]];
curr = curr[args[i] as keyof typeof curr];
}
if (typeof curr === "number") {

@ -4408,54 +4408,55 @@ export interface NS {
* @remarks RAM cost: 4 GB
*/
readonly hacknet: Hacknet;
/**
*
* Namespace for bladeburner functions.
* @remarks RAM cost: 0 GB
*/
readonly bladeburner: Bladeburner;
/**
*
* Namespace for codingcontract functions.
* @remarks RAM cost: 0 GB
*/
readonly codingcontract: CodingContract;
/**
*
* Namespace for gang functions.
* @remarks RAM cost: 0 GB
*/
readonly gang: Gang;
/**
*
* Namespace for sleeve functions.
* @remarks RAM cost: 0 GB
*/
readonly sleeve: Sleeve;
/**
*
* Namespace for stock functions.
* @remarks
* RAM cost: 0 GB
* @remarks RAM cost: 0 GB
*/
readonly stock: TIX;
/**
*
* Namespace for formulas functions.
* @remarks
* RAM cost: 0 GB
* @remarks RAM cost: 0 GB
*/
readonly formulas: Formulas;
/**
* Namespace for stanek functions.
* RAM cost: 0 GB
*/
readonly stanek: Stanek;
/**
* Namespace for infiltration functions.
* RAM cost: 0 GB
*/
readonly infiltration: Infiltration;
/**
* Namespace for corporation functions.
* RAM cost: 1022.4 GB
@ -4476,8 +4477,7 @@ export interface NS {
/**
* Namespace for grafting functions.
* @remarks
* RAM cost: 0 GB
* @remarks RAM cost: 0 GB
*/
readonly grafting: Grafting;

@ -42,20 +42,7 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
};
const ns = NetscriptFunctions(workerScript as WorkerScript);
/**
* Tests that:
* 1. A function has non-zero RAM cost, or zero if it is flagged as zero cost.
* 2. Running the function properly updates the MockWorkerScript's dynamic RAM calculation
* 3. Running multiple calls of the function does not result in additional RAM cost
* @param {string[]} fnDesc - describes the name of the function being tested,
* including the namespace(s). e.g. ["gang", "getMemberNames"]
*/
function testDynamicRamCost(fnDesc: string[], zero: boolean = false) {
const expected = getRamCost(...fnDesc);
zero ? expect(expected).toEqual(0) : expect(expected).toBeGreaterThan(0);
}
function dynamicCheck(fnPath: string[], expectedRamCost: number) {
function dynamicCheck(fnPath: string[], expectedRamCost: number, extraLayerCost = 0) {
const code = `${fnPath.join(".")}();\n`.repeat(3);
const fnName = fnPath[fnPath.length - 1];
@ -82,10 +69,10 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
expect(workerScript.dynamicLoadedFns).toHaveProperty(fnName);
expect(workerScript.dynamicRamUsage - ScriptBaseCost).toBeCloseTo(expectedRamCost, 5);
expect(workerScript.dynamicRamUsage).toBeCloseTo(runningScript.ramUsage, 5);
expect(workerScript.dynamicRamUsage).toBeCloseTo(runningScript.ramUsage - extraLayerCost, 5);
}
function testFunctionExpectZero(fnPath: string[]) {
function testFunctionExpectZero(fnPath: string[], extraLayerCost = 0) {
const wholeFn = `${fnPath.join(".")}()`;
describe(wholeFn, () => {
it("Zero Ram Static Check", () => {
@ -93,13 +80,13 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
expect(ramCost).toEqual(0);
const code = wholeFn;
const staticCost = calculateRamUsage(code, []).cost;
expect(staticCost).toEqual(ScriptBaseCost);
expect(staticCost).toEqual(ScriptBaseCost + extraLayerCost);
});
it("Zero Ram Dynamic check", () => dynamicCheck(fnPath, 0));
it("Zero Ram Dynamic check", () => dynamicCheck(fnPath, 0, extraLayerCost));
});
}
function testFunctionExpectNonzero(fnPath: string[]) {
function testFunctionExpectNonzero(fnPath: string[], extraLayerCost = 0) {
const wholeFn = `${fnPath.join(".")}()`;
const ramCost = getRamCost(...fnPath);
describe(wholeFn, () => {
@ -107,13 +94,15 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
expect(ramCost).toBeGreaterThan(0);
const code = wholeFn;
const staticCost = calculateRamUsage(code, []).cost;
expect(staticCost).toBeCloseTo(ramCost + ScriptBaseCost, 5);
expect(staticCost).toBeCloseTo(ramCost + ScriptBaseCost + extraLayerCost, 5);
});
it("Dynamic Check", () => dynamicCheck(fnPath, ramCost));
it("Dynamic Check", () => dynamicCheck(fnPath, ramCost, extraLayerCost));
});
}
//input type for testSingularityFunctions
type singularityData = { fnPath: string[]; baseCost: number };
function testSingularityFunctions(data: singularityData[]) {
const sf4 = Player.sourceFiles[0];
data.forEach(({ fnPath, baseCost }) => {
@ -154,64 +143,89 @@ describe("Netscript RAM Calculation/Generation Tests", function () {
[key: string]: NSLayer | unknown[] | (() => unknown);
};
type RamLayer = {
[key: string]: number | (() => number);
[key: string]: RamLayer | number | (() => number);
};
function testLayer(nsLayer: NSLayer, ramLayer: RamLayer, path: string[]) {
function testLayer(nsLayer: NSLayer, ramLayer: RamLayer, path: string[], extraLayerCost = 0) {
const zeroCostFunctions = Object.entries(nsLayer)
.filter(([key, val]) => ramLayer[key] === 0 && typeof val === "function")
.map(([key]) => [...path, key]);
zeroCostFunctions.forEach(testFunctionExpectZero);
zeroCostFunctions.forEach((fn) => testFunctionExpectZero(fn, extraLayerCost));
const nonzeroCostFunctions = Object.entries(nsLayer)
.filter(([key, val]) => ramLayer[key] > 0 && typeof val === "function")
.map(([key]) => [...path, key]);
nonzeroCostFunctions.forEach(testFunctionExpectNonzero);
nonzeroCostFunctions.forEach((fn) => testFunctionExpectNonzero(fn, extraLayerCost));
}
describe("Top level ns functions", function () {
describe("Top level ns functions", () => {
const nsScope = ns as unknown as NSLayer;
const ramScope = RamCosts as unknown as RamLayer;
const ramScope = RamCosts;
testLayer(nsScope, ramScope, []);
});
describe("TIX API (stock) functions", function () {
const nsScope = ns.stock as unknown as NSLayer;
const ramScope = RamCosts.stock as unknown as RamLayer;
testLayer(nsScope, ramScope, ["stock"]);
});
describe("Bladeburner API (bladeburner) functions", function () {
describe("Bladeburner API (bladeburner) functions", () => {
const nsScope = ns.bladeburner as unknown as NSLayer;
const ramScope = RamCosts.bladeburner as unknown as RamLayer;
const ramScope = RamCosts.bladeburner;
testLayer(nsScope, ramScope, ["bladeburner"]);
});
describe("Gang API (gang) functions", function () {
describe("Corporation API (corporation) functions", () => {
const nsScope = ns.corporation as unknown as NSLayer;
const ramScope = RamCosts.corporation;
testLayer(nsScope, ramScope, ["corporation"], 1024 - ScriptBaseCost);
});
describe("TIX API (stock) functions", () => {
const nsScope = ns.stock as unknown as NSLayer;
const ramScope = RamCosts.stock;
testLayer(nsScope, ramScope, ["stock"]);
});
describe("Gang API (gang) functions", () => {
const nsScope = ns.gang as unknown as NSLayer;
const ramScope = RamCosts.gang as unknown as RamLayer;
const ramScope = RamCosts.gang;
testLayer(nsScope, ramScope, ["gang"]);
});
describe("Coding Contract API (codingcontract) functions", function () {
describe("Coding Contract API (codingcontract) functions", () => {
const nsScope = ns.codingcontract as unknown as NSLayer;
const ramScope = RamCosts.codingcontract as unknown as RamLayer;
const ramScope = RamCosts.codingcontract;
testLayer(nsScope, ramScope, ["codingcontract"]);
});
describe("Sleeve API (sleeve) functions", function () {
describe("Sleeve API (sleeve) functions", () => {
const nsScope = ns.sleeve as unknown as NSLayer;
const ramScope = RamCosts.sleeve as unknown as RamLayer;
const ramScope = RamCosts.sleeve;
testLayer(nsScope, ramScope, ["sleeve"]);
});
//Singularity functions are tested in a different way because they also check SF4 level effect
describe("ns.singularity functions", function () {
const singularityFunctions = Object.entries(RamCosts.singularity).map(([key, val]) => {
describe("ns.singularity functions", () => {
const singularityFunctions = Object.entries(RamCosts.singularity).map(([fnName, ramFn]) => {
return {
fnPath: ["singularity", key],
baseCost: (val as () => number)(),
fnPath: ["singularity", fnName],
// This will error if a singularity function is assigned a flat cost instead of using the SF4 function
baseCost: (ramFn as () => number)(),
};
});
testSingularityFunctions(singularityFunctions);
});
//Formulas requires deeper layer penetration
function formulasTest(
newLayer = "formulas",
oldNSLayer = ns as unknown as NSLayer,
oldRamLayer = RamCosts as unknown as RamLayer,
path = ["formulas"],
nsLayer = oldNSLayer[newLayer] as NSLayer,
ramLayer = oldRamLayer[newLayer] as RamLayer,
) {
testLayer(nsLayer, ramLayer, path);
for (const [key, val] of Object.entries(nsLayer)) {
if (Array.isArray(val) || typeof val === "function" || key === "enums") continue;
// Only other option is an object / new layer
describe(key, () => formulasTest(key, nsLayer, ramLayer, [...path, key]));
}
}
describe("ns.formulas functions", formulasTest);
});