Skip to content

Commit

Permalink
Merge pull request #237 from mrc-ide/mrc-6023-test
Browse files Browse the repository at this point in the history
tests done
  • Loading branch information
M-Kusumgar authored Jan 9, 2025
2 parents 4534546 + 2f742f2 commit fea255f
Show file tree
Hide file tree
Showing 18 changed files with 593 additions and 178 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,7 @@ Use the `./scripts/run-version.sh` script setting the branch references for the
This repo has two main parts: `app/server` (Express server) and `app/static` (Vue frontend). The normal way to spin up the app without hot reloading is build frontend, copy build files into public folder in the server (`npm run build --prefix=app/static` builds the frontend and copies the files to the server) and run the server (`npm run serve --prefix=app/server`). The html file will then look for its javascript src `wodin.js` and css src `wodin.css` in the `app/server/public` directory and load the app from these.

The hot reloading setup is different. The `server.js` file (from building the server) takes in a `--hot-reload` boolean option which tells it to instead look for the javascript src at `http://localhost:5173/src/wodin.ts` since `wodin.ts` is our entrypoint (`http://localhost:5173` is the default url for `vite` dev mode and it serves the files, after transpiling to javascript, based on your folder structure) and css src at `http://localhost:5173/src/scss/style.scss`. To run it this way we run `npm run dev --prefix=app/static` to start the vite dev server and then we run `npm run serve-hot --prefix=app/server`.

## Static build

For details about the static build please read the README [here](./config-static/README.md)
43 changes: 43 additions & 0 deletions app/server/src/static-site-builder/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import path from "path";
import fs from "fs";
import { readFile } from "../configReader";
import axios from "axios";

export const buildWodinStaticSite = async (configPath: string, destPath: string) => {
const storesPath = path.resolve(configPath, "stores")
const stores = fs.readdirSync(storesPath, { recursive: false }) as string[];

// bootstrapping and making sure the folder structure is correct
if (!fs.existsSync(destPath)) {
fs.mkdirSync(destPath);
}
const destStoresPath = path.resolve(destPath, "stores");
if (fs.existsSync(destStoresPath)) {
fs.rmSync(destStoresPath, { recursive: true });
}
fs.mkdirSync(destStoresPath);

// copy in their config specific files (assets to come soon)
fs.cpSync(path.resolve(configPath, "index.html"), path.resolve(destPath, "index.html"));

// get runners
const runnerOdeResponse = await axios.get("http://localhost:8001/support/runner-ode");
fs.writeFileSync(path.resolve(destStoresPath, "runnerOde.js"), runnerOdeResponse.data.data);
const runnerDiscreteResponse = await axios.get("http://localhost:8001/support/runner-discrete");
fs.writeFileSync(path.resolve(destStoresPath, "runnerDiscrete.js"), runnerDiscreteResponse.data.data);

// make folder per store with config.json (their config + default code) and
// model.json (model response from odin.api)
stores.forEach(async store => {
const destStorePath = path.resolve(destStoresPath, store);
fs.mkdirSync(destStorePath);
const model = readFile(path.resolve(storesPath, store, "model.R")).split("\n");

const config = JSON.parse(readFile(path.resolve(storesPath, store, "config.json"))) as { appType: string };
fs.writeFileSync(path.resolve(destStorePath, "config.json"), JSON.stringify({ ...config, defaultCode: model }));

const timeType = config.appType === "stochastic" ? "discrete" : "continuous";
const modelResponse = await axios.post("http://localhost:8001/compile", { model, requirements: { timeType } });
fs.writeFileSync(path.resolve(destStorePath, `model.json`), JSON.stringify(modelResponse.data.data));
});
};
48 changes: 2 additions & 46 deletions app/server/src/static-site-builder/wodinBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,6 @@
import path from "path";
import fs from "fs";
import { readFile } from "../configReader";
import axios from "axios";
import { processArgs } from "./args";

const mkdirForce = (path: string) => {
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
};
import { buildWodinStaticSite } from "./builder";

const { configPath, destPath } = processArgs();

const storesPath = path.resolve(configPath, "stores")
const stores = fs.readdirSync(storesPath, { recursive: false }) as string[];

mkdirForce(destPath);
const destStoresPath = path.resolve(destPath, "stores");
if (fs.existsSync(destStoresPath)) {
fs.rmSync(destStoresPath, { recursive: true });
}
fs.mkdirSync(destStoresPath);

fs.cpSync(path.resolve(configPath, "index.html"), path.resolve(destPath, "index.html"));

const getRunners = async () => {
const runnerOdeResponse = await axios.get("http://localhost:8001/support/runner-ode");
fs.writeFileSync(path.resolve(destStoresPath, "runnerOde.js"), runnerOdeResponse.data.data);

const runnerDiscreteResponse = await axios.get("http://localhost:8001/support/runner-discrete");
fs.writeFileSync(path.resolve(destStoresPath, "runnerDiscrete.js"), runnerDiscreteResponse.data.data);
}
getRunners();

stores.forEach(async store => {
const destStorePath = path.resolve(destStoresPath, store);
fs.mkdirSync(destStorePath);

const model = readFile(path.resolve(storesPath, store, "model.R")).split("\n");

const config = JSON.parse(readFile(path.resolve(storesPath, store, "config.json"))) as { appType: string };
fs.writeFileSync(path.resolve(destStorePath, "config.json"), JSON.stringify({ ...config, defaultCode: model }));

const modelResponse = await axios.post("http://localhost:8001/compile", {
model,
requirements: { timeType: config.appType === "stochastic" ? "discrete" : "continuous" }
});
fs.writeFileSync(path.resolve(destStorePath, `model.json`), JSON.stringify(modelResponse.data.data));
});
buildWodinStaticSite(configPath, destPath);
29 changes: 29 additions & 0 deletions app/server/tests/static-site-builder/args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { processArgs } from "../../src/static-site-builder/args";

describe("args", () => {
const realArgs = process.argv;
afterEach(() => {
process.argv = realArgs;
});

const builderPath = "/builder",
cfgPath = "/configPath",
destPath = "/destPath";

it("throws error if no config path or dest path arg", () => {
const argv = ["node", builderPath];
expect(() => { processArgs(argv); })
.toThrow("Usage:");

const argv1 = ["node", builderPath, cfgPath];
expect(() => { processArgs(argv1); })
.toThrow("Usage:");
});

it("returns config path and dest path", () => {
const argv = ["node", builderPath, cfgPath, destPath];
const args = processArgs(argv);
expect(args.configPath).toBe(cfgPath);
expect(args.destPath).toBe(destPath);
});
});
90 changes: 90 additions & 0 deletions app/server/tests/static-site-builder/builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import path from "path";
import fs from "fs";
import { tmpdirTest } from "./tempDirTestHelper"
import { buildWodinStaticSite } from "../../src/static-site-builder/builder"

const buildSite = async (tmpdir: string) => {
await buildWodinStaticSite("../../config-static", tmpdir);
}

const {
mockRunnerOde, mockRunnerDiscrete,
mockModel, mockGet, mockPost
} = vi.hoisted(() => {
const mockRunnerOde = "var odinRunnerOde;";
const mockRunnerDiscrete = "var odinRunnerDiscrete;";
const mockModel = "class odinModel {};";
const mockGet = vi.fn().mockImplementation((url: string) => {
if (url === "http://localhost:8001/support/runner-ode") {
return { data: { data: mockRunnerOde } };
} else if (url === "http://localhost:8001/support/runner-discrete") {
return { data: { data: mockRunnerDiscrete } };
}
});
const mockPost = vi.fn().mockImplementation((url: string) => {
if (url === "http://localhost:8001/compile") {
return { data: { data: mockModel } };
}
});
return {
mockRunnerOde, mockRunnerDiscrete,
mockModel, mockGet, mockPost
};
});

vi.mock("axios", () => {
return {
default: {
get: mockGet,
post: mockPost
}
}
});

const p = path.resolve;

const expectPath = (...paths: string[]) => {
expect(fs.existsSync(p(...paths))).toBe(true);
};

const expectPathContent = (content: string, ...paths: string[]) => {
expect(fs.readFileSync(p(...paths)).toString()).toBe(content);
}

const expectPathContentContains = (content: string, ...paths: string[]) => {
expect(fs.readFileSync(p(...paths)).toString()).toContain(content);
}

describe("Wodin builder", () => {
tmpdirTest("creates dest dir if it doesn't exist", async ({ tmpdir }) => {
fs.rmdirSync(tmpdir);
await buildSite(tmpdir);
expectPath(tmpdir);
});

tmpdirTest("generates site with correct files and folder structure", async ({ tmpdir }) => {
const storesPath = p(tmpdir, "stores");
await buildSite(tmpdir);

expectPath(tmpdir, "index.html");

expectPath(storesPath, "runnerOde.js");
expectPathContent(mockRunnerOde, storesPath, "runnerOde.js");
expectPath(storesPath, "runnerDiscrete.js");
expectPathContent(mockRunnerDiscrete, storesPath, "runnerDiscrete.js");

expectPath(storesPath, "basic", "model.json");
expectPathContentContains(mockModel, storesPath, "basic", "model.json");
expectPath(storesPath, "basic", "config.json");
});

tmpdirTest("overwrites existing stores folder", async ({ tmpdir }) => {
const storesPath = p(tmpdir, "stores");
fs.mkdirSync(storesPath);
fs.writeFileSync(p(storesPath, "trash.txt"), "whats up");

await buildSite(tmpdir);

expect(fs.existsSync(p(storesPath, "trash.txt"))).toBe(false)
});
})
22 changes: 22 additions & 0 deletions app/server/tests/static-site-builder/tempDirTestHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test } from "vitest";
import os from "node:os";
import path from "path";
import fs from "fs";

type TmpDirTestFixture = { tmpdir: string; }

const createTempDir = () => {
const ostmpdir = os.tmpdir();
const tmpdir = path.resolve(ostmpdir, "static-site-builder-tests-");
return fs.mkdtempSync(tmpdir);
};

export const tmpdirTest = test.extend<TmpDirTestFixture>({
// eslint-disable-next-line no-empty-pattern
tmpdir: async ({}, use) => {
const dir = createTempDir();
await use(dir);

fs.rmSync(dir, { recursive: true })
}
});
2 changes: 1 addition & 1 deletion app/server/vitest/vitest.unit.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default mergeConfig(
coverage: {
provider: "istanbul",
include: ["src"],
exclude: ["**/tests/**", "src/server/server.ts"]
exclude: ["**/tests/**", "src/server/server.ts", "src/static-site-builder/wodinBuilder.ts"]
}
}
})
Expand Down
21 changes: 19 additions & 2 deletions app/static/src/externalScriptSrc.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
const externalScripts = [
/*
Originally dynamic wodin returned a mustache template from the express server
which had a
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
tag in it but with the introduction of the wodin static build, we no longer
control the html that is shipped to the user (the scientists write the html
research papers with the static build).
We could enforce that they add this script tag manually otherwise mathjax
(latex style rendering in html) won't work but for the sake of the researchers'
experience lets make it easier and less prone to errors by just programmatically
adding the script tag.
To load more external scripts, just append the src to the externalScripts array.
*/
export const externalScripts = [
"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
];

export const mountScriptTags = () => {
export const loadThirdPartyCDNScripts = () => {
externalScripts.forEach(src => {
const script = document.createElement("script");
script.defer = true;
Expand Down
18 changes: 0 additions & 18 deletions app/static/src/mainUtils.ts

This file was deleted.

Loading

0 comments on commit fea255f

Please sign in to comment.