From 22d8f1373035c4358cf3e297778765f483c4a1c1 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 3 Jul 2024 15:37:26 +0000 Subject: [PATCH 1/5] Start testing, kind of tricky! --- gui/src/app/StanSampler/StanSampler.ts | 1 + gui/test/app/StanSampler/MockStanModel.ts | 45 +++++++++++++++++++ gui/test/app/StanSampler/StanSampler.test.ts | 19 ++++++++ .../app/StanSampler/useStanSampler.test.ts | 45 +++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 gui/test/app/StanSampler/MockStanModel.ts create mode 100644 gui/test/app/StanSampler/StanSampler.test.ts create mode 100644 gui/test/app/StanSampler/useStanSampler.test.ts diff --git a/gui/src/app/StanSampler/StanSampler.ts b/gui/src/app/StanSampler/StanSampler.ts index 9f33d5a4..ac2ea8bb 100644 --- a/gui/src/app/StanSampler/StanSampler.ts +++ b/gui/src/app/StanSampler/StanSampler.ts @@ -33,6 +33,7 @@ class StanSampler { } { const sampler = new StanSampler(compiledUrl); const cleanup = () => { + console.log("terminating model worker"); sampler.#worker && sampler.#worker.terminate(); sampler.#worker = undefined; }; diff --git a/gui/test/app/StanSampler/MockStanModel.ts b/gui/test/app/StanSampler/MockStanModel.ts new file mode 100644 index 00000000..df99cadf --- /dev/null +++ b/gui/test/app/StanSampler/MockStanModel.ts @@ -0,0 +1,45 @@ +import { vi } from "vitest"; + +import StanModel, { PrintCallback } from "tinystan"; + +const mockModelLoad = (_create: any, printCallback: PrintCallback | null) => { + const model = { + stanVersion: vi.fn(() => "1.2.3"), + sample: vi.fn(({ num_samples, refresh }) => { + if (printCallback && refresh) { + for (let i = 0; i < num_samples; i++) { + if (num_samples % refresh === 0) { + printCallback( + `Chain [1] Iteration: ${i} / ${num_samples} [${Math.round((100 * i) / num_samples)}%] (Sampling)`, + ); + + // sleep for 0.1 seconds + const start = Date.now(); + while (Date.now() - start < 100) { + // + } + } + } + } + + return { + paramNames: ["a", "b"], + draws: [ + [1, 2], + [3, 4], + ], + }; + }), + pathfinder: vi.fn(() => ({ + paramNames: ["a", "b"], + draws: [ + [1, 2], + [3, 4], + ], + })), + } as unknown as StanModel; + + return Promise.resolve(model); +}; + +export default mockModelLoad; diff --git a/gui/test/app/StanSampler/StanSampler.test.ts b/gui/test/app/StanSampler/StanSampler.test.ts new file mode 100644 index 00000000..a451040b --- /dev/null +++ b/gui/test/app/StanSampler/StanSampler.test.ts @@ -0,0 +1,19 @@ +// @vitest-environment jsdom +import { expect, test, describe } from "vitest"; +import "@vitest/web-worker"; +import { + defaultSamplingOpts, + isSamplingOpts, +} from "../../../src/app/StanSampler/StanSampler"; + +describe("isSamplingOpts", () => { + test("defaultSamplingOpts passes", () => { + expect(isSamplingOpts(defaultSamplingOpts)).toBe(true); + }); + + test("nonexistent options fail", () => { + expect(isSamplingOpts(undefined)).toBe(false); + expect(isSamplingOpts(false)).toBe(false); + expect(isSamplingOpts(0)).toBe(false); + }); +}); diff --git a/gui/test/app/StanSampler/useStanSampler.test.ts b/gui/test/app/StanSampler/useStanSampler.test.ts new file mode 100644 index 00000000..b8e626cb --- /dev/null +++ b/gui/test/app/StanSampler/useStanSampler.test.ts @@ -0,0 +1,45 @@ +// @vitest-environment jsdom + +import { expect, test, describe, vi, afterEach } from "vitest"; +import "@vitest/web-worker"; +import { renderHook, waitFor } from "@testing-library/react"; +import useStanSampler from "../../../src/app/StanSampler/useStanSampler"; +import StanModel from "tinystan"; +import mockModelLoad from "./MockStanModel"; + +const mockedStdout = vi + .spyOn(console, "log") + .mockImplementation(() => undefined); +const mockedStderr = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + +const mockedLoadModel = vi + .spyOn(StanModel, "load") + .mockImplementation(mockModelLoad); + +afterEach(() => { + mockedStdout.mockClear(); + mockedStderr.mockClear(); + mockedLoadModel.mockClear(); +}); + +describe("useStanSampler", () => { + test("empty URL should return undefined", () => { + const { result } = renderHook(() => useStanSampler(undefined)); + + expect(result.current.sampler).toBeUndefined(); + }); + + test("other URLs are nonempty", () => { + const { result, unmount } = renderHook(() => useStanSampler("localhost")); + + expect(result.current.sampler).toBeDefined(); + + expect(mockedStdout).not.toHaveBeenCalledWith("terminating model worker"); + unmount(); + waitFor(() => { + expect(mockedStdout).toHaveBeenCalledWith("terminating model worker"); + }); + }); +}); From a581219122c06c0f2c7b97f1fe21a5e15c319e48 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Mon, 8 Jul 2024 21:54:14 +0000 Subject: [PATCH 2/5] Start tests --- gui/src/app/StanSampler/StanModelWorker.ts | 7 +- gui/src/app/StanSampler/StanSampler.ts | 2 +- gui/test/app/StanSampler/MockStanModel.ts | 41 ++---- gui/test/app/StanSampler/empty.js | 1 + .../app/StanSampler/useStanSampler.test.ts | 120 ++++++++++++++++-- 5 files changed, 125 insertions(+), 46 deletions(-) create mode 100644 gui/test/app/StanSampler/empty.js diff --git a/gui/src/app/StanSampler/StanModelWorker.ts b/gui/src/app/StanSampler/StanModelWorker.ts index d1f463e7..d8e60115 100644 --- a/gui/src/app/StanSampler/StanModelWorker.ts +++ b/gui/src/app/StanSampler/StanModelWorker.ts @@ -30,12 +30,13 @@ const parseProgress = (msg: string): Progress => { if (msg.startsWith("Iteration:")) { msg = "Chain [1] " + msg; } + msg = msg.replace(/\[|\]/g, ""); const parts = msg.split(/\s+/); - const chain = parseInt(parts[1].slice(1, -1)); + const chain = parseInt(parts[1]); const iteration = parseInt(parts[3]); const totalIterations = parseInt(parts[5]); - const percent = parseInt(parts[7].slice(0, -2)); - const warmup = parts[8] === "(Warmup)"; + const percent = parseInt(parts[6].slice(0, -1)); + const warmup = parts[7] === "(Warmup)"; return { chain, iteration, totalIterations, percent, warmup }; }; diff --git a/gui/src/app/StanSampler/StanSampler.ts b/gui/src/app/StanSampler/StanSampler.ts index ac2ea8bb..3427afc8 100644 --- a/gui/src/app/StanSampler/StanSampler.ts +++ b/gui/src/app/StanSampler/StanSampler.ts @@ -110,10 +110,10 @@ class StanSampler { this.#samplingOpts = samplingOpts; this.#draws = []; this.#paramNames = []; - this.#worker.postMessage({ purpose: Requests.Sample, sampleConfig }); this.#samplingStartTimeSec = Date.now() / 1000; this.#status = "sampling"; this.#onStatusChangedCallbacks.forEach((cb) => cb()); + this.#worker.postMessage({ purpose: Requests.Sample, sampleConfig }); } onProgress(callback: (progress: Progress) => void) { this.#onProgressCallbacks.push(callback); diff --git a/gui/test/app/StanSampler/MockStanModel.ts b/gui/test/app/StanSampler/MockStanModel.ts index df99cadf..ef6b3d24 100644 --- a/gui/test/app/StanSampler/MockStanModel.ts +++ b/gui/test/app/StanSampler/MockStanModel.ts @@ -1,27 +1,19 @@ import { vi } from "vitest"; -import StanModel, { PrintCallback } from "tinystan"; +import type StanModel from "tinystan"; +import type { PrintCallback } from "tinystan"; + +const mockedLoad = async ( + _create: any, + printCallback: PrintCallback | null, +) => { + await new Promise((resolve) => setTimeout(resolve, 50)); -const mockModelLoad = (_create: any, printCallback: PrintCallback | null) => { const model = { stanVersion: vi.fn(() => "1.2.3"), - sample: vi.fn(({ num_samples, refresh }) => { - if (printCallback && refresh) { - for (let i = 0; i < num_samples; i++) { - if (num_samples % refresh === 0) { - printCallback( - `Chain [1] Iteration: ${i} / ${num_samples} [${Math.round((100 * i) / num_samples)}%] (Sampling)`, - ); - - // sleep for 0.1 seconds - const start = Date.now(); - while (Date.now() - start < 100) { - // - } - } - } - } - + sample: vi.fn(() => { + printCallback && + printCallback(`Chain [1] Iteration: 123 / 1000 [ 45%] (Sampling)`); return { paramNames: ["a", "b"], draws: [ @@ -30,16 +22,9 @@ const mockModelLoad = (_create: any, printCallback: PrintCallback | null) => { ], }; }), - pathfinder: vi.fn(() => ({ - paramNames: ["a", "b"], - draws: [ - [1, 2], - [3, 4], - ], - })), } as unknown as StanModel; - return Promise.resolve(model); + return model; }; -export default mockModelLoad; +export default mockedLoad; diff --git a/gui/test/app/StanSampler/empty.js b/gui/test/app/StanSampler/empty.js new file mode 100644 index 00000000..f05309dd --- /dev/null +++ b/gui/test/app/StanSampler/empty.js @@ -0,0 +1 @@ +// intentionally empty, used to create a URL vitest can resolve diff --git a/gui/test/app/StanSampler/useStanSampler.test.ts b/gui/test/app/StanSampler/useStanSampler.test.ts index b8e626cb..697cad33 100644 --- a/gui/test/app/StanSampler/useStanSampler.test.ts +++ b/gui/test/app/StanSampler/useStanSampler.test.ts @@ -2,10 +2,14 @@ import { expect, test, describe, vi, afterEach } from "vitest"; import "@vitest/web-worker"; -import { renderHook, waitFor } from "@testing-library/react"; -import useStanSampler from "../../../src/app/StanSampler/useStanSampler"; -import StanModel from "tinystan"; -import mockModelLoad from "./MockStanModel"; +import { renderHook, waitFor, act } from "@testing-library/react"; +import useStanSampler, { + useSamplerProgress, + useSamplerStatus, +} from "../../../src/app/StanSampler/useStanSampler"; +import { defaultSamplingOpts } from "../../../src/app/StanSampler/StanSampler"; + +import fakeURL from "./empty.js?url"; const mockedStdout = vi .spyOn(console, "log") @@ -14,16 +18,26 @@ const mockedStderr = vi .spyOn(console, "error") .mockImplementation(() => undefined); -const mockedLoadModel = vi - .spyOn(StanModel, "load") - .mockImplementation(mockModelLoad); +vi.mock("tinystan", async (importOriginal) => { + const mockedLoad = await import("./MockStanModel"); + const mod = await importOriginal(); + mod.default.load = mockedLoad.default; + return mod; +}); afterEach(() => { - mockedStdout.mockClear(); - mockedStderr.mockClear(); - mockedLoadModel.mockClear(); + vi.clearAllMocks(); }); +const loadedSampler = async () => { + const ret = renderHook(() => useStanSampler(fakeURL)); + const status = renderHook(() => useSamplerStatus(ret.result.current.sampler)); + await waitFor(() => { + expect(status.result.current.status).toBe("loaded"); + }); + return [ret, status] as const; +}; + describe("useStanSampler", () => { test("empty URL should return undefined", () => { const { result } = renderHook(() => useStanSampler(undefined)); @@ -31,15 +45,93 @@ describe("useStanSampler", () => { expect(result.current.sampler).toBeUndefined(); }); - test("other URLs are nonempty", () => { - const { result, unmount } = renderHook(() => useStanSampler("localhost")); + test("other URLs are nonempty", async () => { + const { result, unmount } = renderHook(() => useStanSampler(fakeURL)); expect(result.current.sampler).toBeDefined(); expect(mockedStdout).not.toHaveBeenCalledWith("terminating model worker"); unmount(); - waitFor(() => { - expect(mockedStdout).toHaveBeenCalledWith("terminating model worker"); + expect(mockedStdout).toHaveBeenCalledWith("terminating model worker"); + }); + + describe("useSamplerStatus", () => { + test("loading changes status", async () => { + const { result, rerender } = renderHook( + (props: { url: string } | undefined) => useStanSampler(props?.url), + { initialProps: undefined }, + ); + + const { result: statusResult, rerender: rerenderStatus } = renderHook( + (sampler) => useSamplerStatus(sampler), + { initialProps: result.current.sampler }, + ); + + expect(statusResult.current.status).toBe(""); + + rerender({ url: fakeURL }); + rerenderStatus(result.current.sampler); + + await waitFor(() => { + expect(statusResult.current.status).toBe("loading"); + }); + + await waitFor(() => { + expect(statusResult.current.status).toBe("loaded"); + }); + expect(mockedStderr).not.toHaveBeenCalled(); + }); + + test("sampling changes status", async () => { + const [{ result }, { result: statusResult }] = await loadedSampler(); + + act(() => { + result.current.sampler?.sample({}, defaultSamplingOpts); + }); + + await waitFor(() => { + expect(statusResult.current.status).toBe("completed"); + expect(result.current.sampler?.paramNames).toEqual(["a", "b"]); + }); + expect(mockedStderr).not.toHaveBeenCalled(); + }); + + // NOTE: Because vitest-web-worker does not actually run anything concurrently, this test will not work + // test("cancelling reloads", async () => { + // const [{ result }, { result: statusResult }] = await loadedSampler(); + // act(() => { + // result.current.sampler?.sample({}, defaultSamplingOpts); + // }); + // act(() => { + // result.current.sampler?.cancel(); + // }); + // await waitFor(() => { + // expect(statusResult.current.status).toBe("loaded"); + // }); + // expect(mockedStderr).not.toHaveBeenCalled(); + // }); + }); + + describe("useSamplerProgress", () => { + test("sampling changes status", async () => { + const [{ result }] = await loadedSampler(); + + const { result: progress } = renderHook(() => + useSamplerProgress(result.current.sampler), + ); + + expect(progress.current).toBeUndefined(); + + act(() => { + result.current.sampler?.sample({}, defaultSamplingOpts); + }); + + await waitFor(() => { + expect(progress.current).toBeDefined(); + expect(progress.current?.iteration).toBe(123); + }); + + expect(mockedStderr).not.toHaveBeenCalled(); }); }); }); From fb0add1102706de97dd53d66ca4c204c73fbb5b3 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Tue, 9 Jul 2024 14:25:03 +0000 Subject: [PATCH 3/5] More StanSampler tests --- gui/src/app/StanSampler/StanModelWorker.ts | 2 +- gui/src/app/StanSampler/StanSampler.ts | 17 +-- gui/test/app/StanSampler/MockStanModel.ts | 10 +- gui/test/app/StanSampler/StanSampler.test.ts | 19 --- gui/test/app/StanSampler/fail.js | 3 + .../app/StanSampler/useStanSampler.test.ts | 125 +++++++++++++++++- 6 files changed, 133 insertions(+), 43 deletions(-) delete mode 100644 gui/test/app/StanSampler/StanSampler.test.ts create mode 100644 gui/test/app/StanSampler/fail.js diff --git a/gui/src/app/StanSampler/StanModelWorker.ts b/gui/src/app/StanSampler/StanModelWorker.ts index d8e60115..38bb240d 100644 --- a/gui/src/app/StanSampler/StanModelWorker.ts +++ b/gui/src/app/StanSampler/StanModelWorker.ts @@ -65,7 +65,7 @@ self.onmessage = (e) => { m.stanVersion(), ); self.postMessage({ purpose: Replies.ModelLoaded }); - }); + }, console.error); break; } case Requests.Sample: { diff --git a/gui/src/app/StanSampler/StanSampler.ts b/gui/src/app/StanSampler/StanSampler.ts index 3427afc8..eedaf3af 100644 --- a/gui/src/app/StanSampler/StanSampler.ts +++ b/gui/src/app/StanSampler/StanSampler.ts @@ -82,31 +82,16 @@ class StanSampler { sample(data: any, samplingOpts: SamplingOpts) { const refresh = calculateReasonableRefreshRate(samplingOpts); const sampleConfig: Partial = { + ...samplingOpts, data, - num_chains: samplingOpts.num_chains, - num_warmup: samplingOpts.num_warmup, - num_samples: samplingOpts.num_samples, - init_radius: samplingOpts.init_radius, seed: samplingOpts.seed !== undefined ? samplingOpts.seed : null, refresh, }; if (!this.#worker) return; - if (this.#status === "") { - console.warn("Model not loaded yet"); - return; - } - if (sampleConfig.num_chains === undefined) { - console.warn("Number of chains not specified"); - return; - } if (this.#status === "sampling") { console.warn("Already sampling"); return; } - if (this.#status === "loading") { - console.warn("Model not loaded yet"); - return; - } this.#samplingOpts = samplingOpts; this.#draws = []; this.#paramNames = []; diff --git a/gui/test/app/StanSampler/MockStanModel.ts b/gui/test/app/StanSampler/MockStanModel.ts index ef6b3d24..ffb792ee 100644 --- a/gui/test/app/StanSampler/MockStanModel.ts +++ b/gui/test/app/StanSampler/MockStanModel.ts @@ -9,9 +9,17 @@ const mockedLoad = async ( ) => { await new Promise((resolve) => setTimeout(resolve, 50)); + if (_create === "fail") { + return Promise.reject(new Error("error for testing in load!")); + } + const model = { stanVersion: vi.fn(() => "1.2.3"), - sample: vi.fn(() => { + sample: vi.fn(({ num_chains }) => { + if (num_chains === 999) { + throw new Error("error for testing in sample!"); + } + printCallback && printCallback(`Chain [1] Iteration: 123 / 1000 [ 45%] (Sampling)`); return { diff --git a/gui/test/app/StanSampler/StanSampler.test.ts b/gui/test/app/StanSampler/StanSampler.test.ts deleted file mode 100644 index a451040b..00000000 --- a/gui/test/app/StanSampler/StanSampler.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @vitest-environment jsdom -import { expect, test, describe } from "vitest"; -import "@vitest/web-worker"; -import { - defaultSamplingOpts, - isSamplingOpts, -} from "../../../src/app/StanSampler/StanSampler"; - -describe("isSamplingOpts", () => { - test("defaultSamplingOpts passes", () => { - expect(isSamplingOpts(defaultSamplingOpts)).toBe(true); - }); - - test("nonexistent options fail", () => { - expect(isSamplingOpts(undefined)).toBe(false); - expect(isSamplingOpts(false)).toBe(false); - expect(isSamplingOpts(0)).toBe(false); - }); -}); diff --git a/gui/test/app/StanSampler/fail.js b/gui/test/app/StanSampler/fail.js new file mode 100644 index 00000000..460d5859 --- /dev/null +++ b/gui/test/app/StanSampler/fail.js @@ -0,0 +1,3 @@ +// used to create a URL vitest can resolve +const load = "fail"; +export default load; diff --git a/gui/test/app/StanSampler/useStanSampler.test.ts b/gui/test/app/StanSampler/useStanSampler.test.ts index 697cad33..f50e6582 100644 --- a/gui/test/app/StanSampler/useStanSampler.test.ts +++ b/gui/test/app/StanSampler/useStanSampler.test.ts @@ -1,15 +1,21 @@ // @vitest-environment jsdom -import { expect, test, describe, vi, afterEach } from "vitest"; +import { expect, test, describe, vi, afterEach, onTestFinished } from "vitest"; import "@vitest/web-worker"; import { renderHook, waitFor, act } from "@testing-library/react"; import useStanSampler, { + useSamplerOutput, useSamplerProgress, useSamplerStatus, } from "../../../src/app/StanSampler/useStanSampler"; import { defaultSamplingOpts } from "../../../src/app/StanSampler/StanSampler"; import fakeURL from "./empty.js?url"; +import erroringURL from "./fail.js?url"; +const erroringSamplingOpts = { + ...defaultSamplingOpts, + num_chains: 999, +}; const mockedStdout = vi .spyOn(console, "log") @@ -35,6 +41,17 @@ const loadedSampler = async () => { await waitFor(() => { expect(status.result.current.status).toBe("loaded"); }); + + onTestFinished(() => { + expect(ret.result.current.sampler?.status).toEqual( + status.result.current.status, + ); + + expect(mockedStdout).not.toHaveBeenCalledWith("terminating model worker"); + ret.unmount(); + expect(mockedStdout).toHaveBeenCalledWith("terminating model worker"); + }); + return [ret, status] as const; }; @@ -46,13 +63,9 @@ describe("useStanSampler", () => { }); test("other URLs are nonempty", async () => { - const { result, unmount } = renderHook(() => useStanSampler(fakeURL)); + const { result } = renderHook(() => useStanSampler(fakeURL)); expect(result.current.sampler).toBeDefined(); - - expect(mockedStdout).not.toHaveBeenCalledWith("terminating model worker"); - unmount(); - expect(mockedStdout).toHaveBeenCalledWith("terminating model worker"); }); describe("useSamplerStatus", () => { @@ -82,6 +95,42 @@ describe("useStanSampler", () => { expect(mockedStderr).not.toHaveBeenCalled(); }); + test("failing to load changes status", async () => { + const { result, rerender } = renderHook( + (props: { url: string } | undefined) => useStanSampler(props?.url), + { initialProps: undefined }, + ); + + const { result: statusResult, rerender: rerenderStatus } = renderHook( + (sampler) => useSamplerStatus(sampler), + { initialProps: result.current.sampler }, + ); + + expect(statusResult.current.status).toBe(""); + + rerender({ url: erroringURL }); + rerenderStatus(result.current.sampler); + + await waitFor(() => { + expect(statusResult.current.status).toBe("loading"); + }); + + await waitFor(() => { + expect(mockedStderr).toHaveBeenCalledWith( + new Error("error for testing in load!"), + ); + }); + + act(() => { + result.current.sampler?.sample({}, defaultSamplingOpts); + }); + + await waitFor(() => { + expect(statusResult.current.status).toBe("failed"); + expect(statusResult.current.errorMessage).toBe("Model not loaded yet!"); + }); + }); + test("sampling changes status", async () => { const [{ result }, { result: statusResult }] = await loadedSampler(); @@ -96,6 +145,22 @@ describe("useStanSampler", () => { expect(mockedStderr).not.toHaveBeenCalled(); }); + test("error during sampling changes status", async () => { + const [{ result }, { result: statusResult }] = await loadedSampler(); + + act(() => { + result.current.sampler?.sample({}, erroringSamplingOpts); + }); + + await waitFor(() => { + expect(statusResult.current.status).toBe("failed"); + expect(statusResult.current.errorMessage).toBe( + "Error: error for testing in sample!", + ); + }); + expect(mockedStderr).not.toHaveBeenCalled(); + }); + // NOTE: Because vitest-web-worker does not actually run anything concurrently, this test will not work // test("cancelling reloads", async () => { // const [{ result }, { result: statusResult }] = await loadedSampler(); @@ -134,4 +199,52 @@ describe("useStanSampler", () => { expect(mockedStderr).not.toHaveBeenCalled(); }); }); + + describe("useSamplerOutput", () => { + test("undefined sampler returns undefined", () => { + const { result } = renderHook(() => useSamplerOutput(undefined)); + expect(result.current.draws).toBeUndefined(); + expect(result.current.paramNames).toBeUndefined(); + expect(result.current.numChains).toBeUndefined(); + expect(result.current.computeTimeSec).toBeUndefined(); + }); + + test("sampling changes output", async () => { + const [{ result }] = await loadedSampler(); + + const { result: output } = renderHook(() => + useSamplerOutput(result.current.sampler), + ); + + expect(output.current.draws).toBeUndefined(); + expect(output.current.paramNames).toBeUndefined(); + expect(output.current.numChains).toBeUndefined(); + expect(output.current.computeTimeSec).toBeUndefined(); + + act(() => { + result.current.sampler?.sample({}, defaultSamplingOpts); + }); + + await waitFor(() => { + expect(output.current.draws).toEqual([ + [1, 2], + [3, 4], + ]); + expect(output.current.paramNames).toEqual(["a", "b"]); + expect(output.current.numChains).toBe(4); + expect(output.current.computeTimeSec).toBeDefined(); + }); + + expect(result.current.sampler?.status).toBe("completed"); + + expect(result.current.sampler?.draws).toBe(output.current.draws); + expect(result.current.sampler?.paramNames).toBe( + output.current.paramNames, + ); + expect(result.current.sampler?.samplingOpts).toBe(defaultSamplingOpts); + expect(result.current.sampler?.computeTimeSec).toBe( + output.current.computeTimeSec, + ); + }); + }); }); From f5f5b9bacc6c275defa521df0cd07cc629aada55 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 10 Jul 2024 18:48:17 +0000 Subject: [PATCH 4/5] Clean up tests --- gui/test/app/StanSampler/MockStanModel.ts | 52 +++++++++-- .../app/StanSampler/{empty.js => empty.ts} | 0 gui/test/app/StanSampler/fail.js | 3 - gui/test/app/StanSampler/fail.ts | 3 + .../app/StanSampler/useStanSampler.test.ts | 91 +++++++++---------- 5 files changed, 90 insertions(+), 59 deletions(-) rename gui/test/app/StanSampler/{empty.js => empty.ts} (100%) delete mode 100644 gui/test/app/StanSampler/fail.js create mode 100644 gui/test/app/StanSampler/fail.ts diff --git a/gui/test/app/StanSampler/MockStanModel.ts b/gui/test/app/StanSampler/MockStanModel.ts index ffb792ee..81a449cf 100644 --- a/gui/test/app/StanSampler/MockStanModel.ts +++ b/gui/test/app/StanSampler/MockStanModel.ts @@ -2,6 +2,41 @@ import { vi } from "vitest"; import type StanModel from "tinystan"; import type { PrintCallback } from "tinystan"; +import { defaultSamplingOpts } from "../../../src/app/Project/ProjectDataModel"; + +import fakeURL from "./empty.ts?url"; +import erroringURL from "./fail.ts?url"; +import failSentinel from "./fail.ts"; + +export const mockCompiledMainJsUrl = fakeURL; +export const erroringCompiledMainJsUrl = erroringURL; + +const erroring_num_chains = 999; + +export const erroringSamplingOpts = { + ...defaultSamplingOpts, + num_chains: erroring_num_chains, +}; + +export const mockedParamNames = ["a", "b"]; +export const mockedDraws = [ + [1, 2], + [3, 4], +]; + +export const mockedProgress = { + chain: 1, + iteration: 123, + totalIterations: 1000, + percent: 45, + warmup: false, +}; + +const mockedProgressString = `Chain [${mockedProgress.chain}] \ +Iteration: ${mockedProgress.iteration} \ +/ ${mockedProgress.totalIterations} \ +[${mockedProgress.percent.toString().padStart(3)}%] \ +(${mockedProgress.warmup ? "Warmup" : "Sampling"})`; const mockedLoad = async ( _create: any, @@ -9,25 +44,22 @@ const mockedLoad = async ( ) => { await new Promise((resolve) => setTimeout(resolve, 50)); - if (_create === "fail") { - return Promise.reject(new Error("error for testing in load!")); + if (_create === failSentinel) { + return Promise.reject("error for testing in load!"); } const model = { stanVersion: vi.fn(() => "1.2.3"), sample: vi.fn(({ num_chains }) => { - if (num_chains === 999) { + if (num_chains === erroring_num_chains) { throw new Error("error for testing in sample!"); } - printCallback && - printCallback(`Chain [1] Iteration: 123 / 1000 [ 45%] (Sampling)`); + printCallback && printCallback(mockedProgressString); + return { - paramNames: ["a", "b"], - draws: [ - [1, 2], - [3, 4], - ], + paramNames: mockedParamNames, + draws: mockedDraws, }; }), } as unknown as StanModel; diff --git a/gui/test/app/StanSampler/empty.js b/gui/test/app/StanSampler/empty.ts similarity index 100% rename from gui/test/app/StanSampler/empty.js rename to gui/test/app/StanSampler/empty.ts diff --git a/gui/test/app/StanSampler/fail.js b/gui/test/app/StanSampler/fail.js deleted file mode 100644 index 460d5859..00000000 --- a/gui/test/app/StanSampler/fail.js +++ /dev/null @@ -1,3 +0,0 @@ -// used to create a URL vitest can resolve -const load = "fail"; -export default load; diff --git a/gui/test/app/StanSampler/fail.ts b/gui/test/app/StanSampler/fail.ts new file mode 100644 index 00000000..138f0f3f --- /dev/null +++ b/gui/test/app/StanSampler/fail.ts @@ -0,0 +1,3 @@ +// used to create a URL vitest can resolve +const failSentinel = "fail"; +export default failSentinel; diff --git a/gui/test/app/StanSampler/useStanSampler.test.ts b/gui/test/app/StanSampler/useStanSampler.test.ts index f50e6582..5974abb3 100644 --- a/gui/test/app/StanSampler/useStanSampler.test.ts +++ b/gui/test/app/StanSampler/useStanSampler.test.ts @@ -3,19 +3,22 @@ import { expect, test, describe, vi, afterEach, onTestFinished } from "vitest"; import "@vitest/web-worker"; import { renderHook, waitFor, act } from "@testing-library/react"; +import mockedLoad, { + mockCompiledMainJsUrl, + erroringCompiledMainJsUrl, + erroringSamplingOpts, + mockedDraws, + mockedParamNames, + mockedProgress, +} from "./MockStanModel"; + import useStanSampler, { useSamplerOutput, useSamplerProgress, useSamplerStatus, } from "../../../src/app/StanSampler/useStanSampler"; -import { defaultSamplingOpts } from "../../../src/app/StanSampler/StanSampler"; - -import fakeURL from "./empty.js?url"; -import erroringURL from "./fail.js?url"; -const erroringSamplingOpts = { - ...defaultSamplingOpts, - num_chains: 999, -}; +import { defaultSamplingOpts } from "../../../src/app/Project/ProjectDataModel"; +import type StanSampler from "../../../src/app/StanSampler/StanSampler"; const mockedStdout = vi .spyOn(console, "log") @@ -25,9 +28,8 @@ const mockedStderr = vi .mockImplementation(() => undefined); vi.mock("tinystan", async (importOriginal) => { - const mockedLoad = await import("./MockStanModel"); const mod = await importOriginal(); - mod.default.load = mockedLoad.default; + mod.default.load = mockedLoad; return mod; }); @@ -36,7 +38,7 @@ afterEach(() => { }); const loadedSampler = async () => { - const ret = renderHook(() => useStanSampler(fakeURL)); + const ret = renderHook(() => useStanSampler(mockCompiledMainJsUrl)); const status = renderHook(() => useSamplerStatus(ret.result.current.sampler)); await waitFor(() => { expect(status.result.current.status).toBe("loaded"); @@ -55,6 +57,21 @@ const loadedSampler = async () => { return [ret, status] as const; }; +const rerenderableSampler = async () => { + const ret = renderHook( + (props: { url: string } | undefined) => useStanSampler(props?.url), + { initialProps: undefined }, + ); + const status = renderHook( + (sampler: StanSampler | undefined) => useSamplerStatus(sampler), + { + initialProps: ret.result.current.sampler, + }, + ); + + return [ret, status] as const; +}; + describe("useStanSampler", () => { test("empty URL should return undefined", () => { const { result } = renderHook(() => useStanSampler(undefined)); @@ -63,31 +80,24 @@ describe("useStanSampler", () => { }); test("other URLs are nonempty", async () => { - const { result } = renderHook(() => useStanSampler(fakeURL)); + const { result } = renderHook(() => useStanSampler(mockCompiledMainJsUrl)); expect(result.current.sampler).toBeDefined(); }); describe("useSamplerStatus", () => { test("loading changes status", async () => { - const { result, rerender } = renderHook( - (props: { url: string } | undefined) => useStanSampler(props?.url), - { initialProps: undefined }, - ); - - const { result: statusResult, rerender: rerenderStatus } = renderHook( - (sampler) => useSamplerStatus(sampler), - { initialProps: result.current.sampler }, - ); + const [ + { result, rerender }, + { result: statusResult, rerender: rerenderStatus }, + ] = await rerenderableSampler(); expect(statusResult.current.status).toBe(""); - rerender({ url: fakeURL }); + rerender({ url: mockCompiledMainJsUrl }); rerenderStatus(result.current.sampler); - await waitFor(() => { - expect(statusResult.current.status).toBe("loading"); - }); + expect(statusResult.current.status).toBe("loading"); await waitFor(() => { expect(statusResult.current.status).toBe("loaded"); @@ -96,19 +106,14 @@ describe("useStanSampler", () => { }); test("failing to load changes status", async () => { - const { result, rerender } = renderHook( - (props: { url: string } | undefined) => useStanSampler(props?.url), - { initialProps: undefined }, - ); - - const { result: statusResult, rerender: rerenderStatus } = renderHook( - (sampler) => useSamplerStatus(sampler), - { initialProps: result.current.sampler }, - ); + const [ + { result, rerender }, + { result: statusResult, rerender: rerenderStatus }, + ] = await rerenderableSampler(); expect(statusResult.current.status).toBe(""); - rerender({ url: erroringURL }); + rerender({ url: erroringCompiledMainJsUrl }); rerenderStatus(result.current.sampler); await waitFor(() => { @@ -116,9 +121,7 @@ describe("useStanSampler", () => { }); await waitFor(() => { - expect(mockedStderr).toHaveBeenCalledWith( - new Error("error for testing in load!"), - ); + expect(mockedStderr).toHaveBeenCalledWith("error for testing in load!"); }); act(() => { @@ -192,8 +195,7 @@ describe("useStanSampler", () => { }); await waitFor(() => { - expect(progress.current).toBeDefined(); - expect(progress.current?.iteration).toBe(123); + expect(progress.current).toEqual(mockedProgress); }); expect(mockedStderr).not.toHaveBeenCalled(); @@ -226,12 +228,9 @@ describe("useStanSampler", () => { }); await waitFor(() => { - expect(output.current.draws).toEqual([ - [1, 2], - [3, 4], - ]); - expect(output.current.paramNames).toEqual(["a", "b"]); - expect(output.current.numChains).toBe(4); + expect(output.current.draws).toEqual(mockedDraws); + expect(output.current.paramNames).toEqual(mockedParamNames); + expect(output.current.numChains).toBe(defaultSamplingOpts.num_chains); expect(output.current.computeTimeSec).toBeDefined(); }); From 878490daa34ed00cd404a0755ffe962d71f77bf9 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Wed, 10 Jul 2024 18:50:07 +0000 Subject: [PATCH 5/5] Simplify props --- gui/test/app/StanSampler/useStanSampler.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gui/test/app/StanSampler/useStanSampler.test.ts b/gui/test/app/StanSampler/useStanSampler.test.ts index 5974abb3..75495a54 100644 --- a/gui/test/app/StanSampler/useStanSampler.test.ts +++ b/gui/test/app/StanSampler/useStanSampler.test.ts @@ -58,10 +58,9 @@ const loadedSampler = async () => { }; const rerenderableSampler = async () => { - const ret = renderHook( - (props: { url: string } | undefined) => useStanSampler(props?.url), - { initialProps: undefined }, - ); + const ret = renderHook((url: string | undefined) => useStanSampler(url), { + initialProps: undefined, + }); const status = renderHook( (sampler: StanSampler | undefined) => useSamplerStatus(sampler), { @@ -94,7 +93,7 @@ describe("useStanSampler", () => { expect(statusResult.current.status).toBe(""); - rerender({ url: mockCompiledMainJsUrl }); + rerender(mockCompiledMainJsUrl); rerenderStatus(result.current.sampler); expect(statusResult.current.status).toBe("loading"); @@ -113,7 +112,7 @@ describe("useStanSampler", () => { expect(statusResult.current.status).toBe(""); - rerender({ url: erroringCompiledMainJsUrl }); + rerender(erroringCompiledMainJsUrl); rerenderStatus(result.current.sampler); await waitFor(() => {