JEST: Add tests for b1tflum3 and destroyW0r1dD43m0n API (#1802)

This commit is contained in:
catloversg 2024-11-27 15:06:11 +07:00 committed by GitHub
parent 9d8ac65aaf
commit bb0b857f71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 248 additions and 13 deletions

@ -10,6 +10,7 @@ import { Page } from "./ui/Router";
import { prestigeSourceFile } from "./Prestige"; import { prestigeSourceFile } from "./Prestige";
import { getDefaultBitNodeOptions, setBitNodeOptions } from "./BitNode/BitNodeUtils"; import { getDefaultBitNodeOptions, setBitNodeOptions } from "./BitNode/BitNodeUtils";
import { prestigeWorkerScripts } from "./NetscriptWorker"; import { prestigeWorkerScripts } from "./NetscriptWorker";
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
function giveSourceFile(bitNodeNumber: number): void { function giveSourceFile(bitNodeNumber: number): void {
const sourceFileKey = "SourceFile" + bitNodeNumber.toString(); const sourceFileKey = "SourceFile" + bitNodeNumber.toString();
@ -76,14 +77,7 @@ export function enterBitNode(
try { try {
setBitNodeOptions(bitNodeOptions); setBitNodeOptions(bitNodeOptions);
} catch (error) { } catch (error) {
dialogBoxCreate( exceptionAlert(error);
<>
Invalid BitNode options. This is a bug. Please report it to developers.
<br />
<br />
{error instanceof Error ? error.stack : String(error)}
</>,
);
// Use default options // Use default options
setBitNodeOptions(getDefaultBitNodeOptions()); setBitNodeOptions(getDefaultBitNodeOptions());
} }

@ -44,6 +44,25 @@ export function GetServer(s: string): BaseServer | null {
return null; return null;
} }
/**
* In our codebase, we usually have to call GetServer() like this:
* ```
* const server = GetServer(hostname);
* if (!server) {
* throw new Error("Error message");
* }
* // Use server
* ```
* With this utility function, we don't need to write boilerplate code.
*/
export function GetServerOrThrow(serverId: string): BaseServer {
const server = GetServer(serverId);
if (!server) {
throw new Error(`Server ${serverId} does not exist.`);
}
return server;
}
//Get server by IP or hostname. Returns null if invalid or unreachable. //Get server by IP or hostname. Returns null if invalid or unreachable.
export function GetReachableServer(s: string): BaseServer | null { export function GetReachableServer(s: string): BaseServer | null {
const server = GetServer(s); const server = GetServer(s);

@ -13,9 +13,9 @@ declare const importActual: (typeof EvaluatorConfig)["doImport"];
// Replace Blob/ObjectURL functions, because they don't work natively in Jest // Replace Blob/ObjectURL functions, because they don't work natively in Jest
global.Blob = class extends Blob { global.Blob = class extends Blob {
code: string; code: string;
constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) { constructor(blobParts?: BlobPart[], __options?: BlobPropertyBag) {
super(); super();
this.code = (blobParts ?? [])[0] + ""; this.code = String((blobParts ?? [])[0]);
} }
}; };
global.URL.revokeObjectURL = function () {}; global.URL.revokeObjectURL = function () {};
@ -77,11 +77,11 @@ test.each([
}, },
])("Netscript execution: $name", async function ({ expected: expectedLog, scripts }) { ])("Netscript execution: $name", async function ({ expected: expectedLog, scripts }) {
global.URL.createObjectURL = function (blob) { global.URL.createObjectURL = function (blob) {
return "data:text/javascript," + encodeURIComponent(blob.code); return "data:text/javascript," + encodeURIComponent((blob as unknown as { code: string }).code);
}; };
let server = {} as Server; let server = {} as Server;
let eventDelete = () => {}; const eventDelete = () => {};
let alertDelete = () => {}; let alertDelete = () => {};
try { try {
const alerted = new Promise((resolve) => { const alerted = new Promise((resolve) => {
@ -98,7 +98,7 @@ test.each([
const ramUsage = script.getRamUsage(server.scripts); const ramUsage = script.getRamUsage(server.scripts);
if (!ramUsage) throw new Error(`ramUsage calculated to be ${ramUsage}`); if (!ramUsage) throw new Error(`ramUsage calculated to be ${ramUsage}`);
const runningScript = new RunningScript(script, ramUsage as number); const runningScript = new RunningScript(script, ramUsage);
const pid = startWorkerScript(runningScript, server); const pid = startWorkerScript(runningScript, server);
expect(pid).toBeGreaterThan(0); expect(pid).toBeGreaterThan(0);
// Manually attach an atExit to the now-created WorkerScript, so we can // Manually attach an atExit to the now-created WorkerScript, so we can
@ -107,6 +107,7 @@ test.each([
expect(ws).toBeDefined(); expect(ws).toBeDefined();
const result = await Promise.race([ const result = await Promise.race([
alerted, alerted,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ws was asserted above
new Promise<void>((resolve) => (ws!.atExit = new Map([["default", resolve]]))), new Promise<void>((resolve) => (ws!.atExit = new Map([["default", resolve]]))),
]); ]);
// If an error alert was thrown, we catch it here. // If an error alert was thrown, we catch it here.

@ -0,0 +1,221 @@
import { installAugmentations } from "../../../src/Augmentation/AugmentationHelpers";
import { blackOpsArray } from "../../../src/Bladeburner/data/BlackOperations";
import { AugmentationName } from "../../../src/Enums";
import { WorkerScript } from "../../../src/Netscript/WorkerScript";
import { NetscriptFunctions, type NSFull } from "../../../src/NetscriptFunctions";
import type { ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
import { Player, setPlayer } from "../../../src/Player";
import { RunningScript } from "../../../src/Script/RunningScript";
import { GetServerOrThrow, initForeignServers, prestigeAllServers } from "../../../src/Server/AllServers";
import { SpecialServers } from "../../../src/Server/data/SpecialServers";
import { initSourceFiles } from "../../../src/SourceFile/SourceFiles";
import { FormatsNeedToChange } from "../../../src/ui/formatNumber";
import { Router } from "../../../src/ui/GameRoot";
function setupBasicTestingEnvironment(): void {
prestigeAllServers();
setPlayer(new PlayerObject());
Player.init();
Player.sourceFiles.set(4, 3);
initForeignServers(Player.getHomeComputer());
}
function setNumBlackOpsComplete(value: number): void {
if (!Player.bladeburner) {
throw new Error("Invalid Bladeburner data");
}
Player.bladeburner.numBlackOpsComplete = value;
}
function getNS(): NSFull {
const home = GetServerOrThrow(SpecialServers.Home);
home.maxRam = 1024;
const filePath = "test.js" as ScriptFilePath;
home.writeToScriptFile(filePath, "");
const script = home.scripts.get(filePath);
if (!script) {
throw new Error("Invalid script");
}
const runningScript = new RunningScript(script, 1024);
const workerScript = new WorkerScript(runningScript, 1, NetscriptFunctions);
const ns = workerScript.env.vars;
if (!ns) {
throw new Error("Invalid NS instance");
}
return ns;
}
// We need to patch this function. Some APIs call it, but it only works properly after the main UI is loaded.
Router.toPage = () => {};
/**
* In src\ui\formatNumber.ts, there are some variables that need to be initialized before other functions can be
* called. We have to call FormatsNeedToChange.emit() to initialize those variables.
*/
FormatsNeedToChange.emit();
initSourceFiles();
const nextBN = 3;
describe("b1tflum3", () => {
beforeEach(() => {
setupBasicTestingEnvironment();
Player.queueAugmentation(AugmentationName.TheRedPill);
installAugmentations();
Player.gainHackingExp(1e100);
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
wdServer.hasAdminRights = true;
});
// Make sure that the player is in the next BN without SF rewards.
const expectSucceedInB1tflum3 = () => {
expect(Player.bitNodeN).toStrictEqual(nextBN);
expect(Player.augmentations.length).toStrictEqual(0);
expect(Player.sourceFileLvl(1)).toStrictEqual(0);
};
describe("Success", () => {
test("Without BN options", () => {
const ns = getNS();
ns.singularity.b1tflum3(nextBN);
expectSucceedInB1tflum3();
});
test("With BN options", () => {
const ns = getNS();
ns.singularity.b1tflum3(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
sourceFileOverrides: new Map(),
intelligenceOverride: 1,
});
expectSucceedInB1tflum3();
});
});
describe("Failure", () => {
// Make sure that the player is still in the same BN without SF rewards.
const expectFailToB1tflum3 = () => {
expect(Player.bitNodeN).toStrictEqual(1);
expect(Player.augmentations.length).toStrictEqual(1);
expect(Player.sourceFileLvl(1)).toStrictEqual(0);
};
test("Invalid intelligenceOverride", () => {
const ns = getNS();
expect(() => {
ns.singularity.b1tflum3(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: -1,
});
}).toThrow();
expectFailToB1tflum3();
});
test("Invalid sourceFileOverrides", () => {
const ns = getNS();
expect(() => {
ns.singularity.b1tflum3(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
sourceFileOverrides: [] as unknown as Map<number, number>,
});
}).toThrow();
expectFailToB1tflum3();
});
});
});
describe("destroyW0r1dD43m0n", () => {
beforeEach(() => {
setupBasicTestingEnvironment();
Player.queueAugmentation(AugmentationName.TheRedPill);
installAugmentations();
Player.gainHackingExp(1e100);
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
wdServer.hasAdminRights = true;
Player.startBladeburner();
setNumBlackOpsComplete(blackOpsArray.length);
});
describe("Success", () => {
// Make sure that the player is in the next BN and received SF rewards.
const expectSucceedInDestroyingWD = () => {
expect(Player.bitNodeN).toStrictEqual(nextBN);
expect(Player.augmentations.length).toStrictEqual(0);
expect(Player.sourceFileLvl(1)).toStrictEqual(1);
};
test("Hacking route", () => {
setNumBlackOpsComplete(0);
const ns = getNS();
ns.singularity.destroyW0r1dD43m0n(nextBN);
expectSucceedInDestroyingWD();
});
test("Hacking route with BN options", () => {
setNumBlackOpsComplete(0);
const ns = getNS();
ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
sourceFileOverrides: new Map(),
intelligenceOverride: 1,
});
expectSucceedInDestroyingWD();
});
test("Bladeburner route", () => {
Player.skills.hacking = 0;
const ns = getNS();
ns.singularity.destroyW0r1dD43m0n(nextBN);
expectSucceedInDestroyingWD();
});
test("Bladeburner route with BN options", () => {
Player.skills.hacking = 0;
const ns = getNS();
ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
sourceFileOverrides: new Map(),
intelligenceOverride: 1,
});
expectSucceedInDestroyingWD();
});
});
describe("Failure", () => {
// Make sure that the player is still in the same BN without SF rewards.
const expectFailToDestroyWD = () => {
expect(Player.bitNodeN).toStrictEqual(1);
expect(Player.augmentations.length).toStrictEqual(1);
expect(Player.sourceFileLvl(1)).toStrictEqual(0);
};
test("Do not have enough hacking level and numBlackOpsComplete", () => {
Player.skills.hacking = 0;
setNumBlackOpsComplete(0);
const ns = getNS();
ns.singularity.destroyW0r1dD43m0n(nextBN);
expectFailToDestroyWD();
});
test("Do not have admin rights on WD and do not have enough numBlackOpsComplete", () => {
const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
wdServer.hasAdminRights = false;
setNumBlackOpsComplete(0);
const ns = getNS();
ns.singularity.destroyW0r1dD43m0n(nextBN);
expectFailToDestroyWD();
});
test("Invalid intelligenceOverride", () => {
const ns = getNS();
expect(() => {
ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
intelligenceOverride: -1,
});
}).toThrow();
expectFailToDestroyWD();
});
test("Invalid sourceFileOverrides", () => {
const ns = getNS();
expect(() => {
ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
...ns.getResetInfo().bitNodeOptions,
sourceFileOverrides: [] as unknown as Map<number, number>,
});
}).toThrow();
expectFailToDestroyWD();
});
});
});