diff --git a/src/RedPill.tsx b/src/RedPill.tsx index 2b566b4d9..217c0aebc 100644 --- a/src/RedPill.tsx +++ b/src/RedPill.tsx @@ -10,6 +10,7 @@ import { Page } from "./ui/Router"; import { prestigeSourceFile } from "./Prestige"; import { getDefaultBitNodeOptions, setBitNodeOptions } from "./BitNode/BitNodeUtils"; import { prestigeWorkerScripts } from "./NetscriptWorker"; +import { exceptionAlert } from "./utils/helpers/exceptionAlert"; function giveSourceFile(bitNodeNumber: number): void { const sourceFileKey = "SourceFile" + bitNodeNumber.toString(); @@ -76,14 +77,7 @@ export function enterBitNode( try { setBitNodeOptions(bitNodeOptions); } catch (error) { - dialogBoxCreate( - <> - Invalid BitNode options. This is a bug. Please report it to developers. -
-
- {error instanceof Error ? error.stack : String(error)} - , - ); + exceptionAlert(error); // Use default options setBitNodeOptions(getDefaultBitNodeOptions()); } diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts index f2c5ba30b..4b4fbd110 100644 --- a/src/Server/AllServers.ts +++ b/src/Server/AllServers.ts @@ -44,6 +44,25 @@ export function GetServer(s: string): BaseServer | 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. export function GetReachableServer(s: string): BaseServer | null { const server = GetServer(s); diff --git a/test/jest/Netscript/RunScript.test.ts b/test/jest/Netscript/RunScript.test.ts index 7e520cf7f..c4353c86e 100644 --- a/test/jest/Netscript/RunScript.test.ts +++ b/test/jest/Netscript/RunScript.test.ts @@ -13,9 +13,9 @@ declare const importActual: (typeof EvaluatorConfig)["doImport"]; // Replace Blob/ObjectURL functions, because they don't work natively in Jest global.Blob = class extends Blob { code: string; - constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) { + constructor(blobParts?: BlobPart[], __options?: BlobPropertyBag) { super(); - this.code = (blobParts ?? [])[0] + ""; + this.code = String((blobParts ?? [])[0]); } }; global.URL.revokeObjectURL = function () {}; @@ -77,11 +77,11 @@ test.each([ }, ])("Netscript execution: $name", async function ({ expected: expectedLog, scripts }) { 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 eventDelete = () => {}; + const eventDelete = () => {}; let alertDelete = () => {}; try { const alerted = new Promise((resolve) => { @@ -98,7 +98,7 @@ test.each([ const ramUsage = script.getRamUsage(server.scripts); 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); expect(pid).toBeGreaterThan(0); // Manually attach an atExit to the now-created WorkerScript, so we can @@ -107,6 +107,7 @@ test.each([ expect(ws).toBeDefined(); const result = await Promise.race([ alerted, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ws was asserted above new Promise((resolve) => (ws!.atExit = new Map([["default", resolve]]))), ]); // If an error alert was thrown, we catch it here. diff --git a/test/jest/Netscript/Singularity.test.ts b/test/jest/Netscript/Singularity.test.ts new file mode 100644 index 000000000..b92bb26ff --- /dev/null +++ b/test/jest/Netscript/Singularity.test.ts @@ -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, + }); + }).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, + }); + }).toThrow(); + expectFailToDestroyWD(); + }); + }); +});