Skip to content

Commit b6b5b30

Browse files
Merge pull request #57 from ClearCalcs/add-static-diagram-tester
Add static diagram tester
2 parents 84cc209 + d10db1e commit b6b5b30

11 files changed

+247
-3
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ output/
66
.DS_Store
77
*.log
88
package-lock.json
9-
yarn.lock
9+
yarn.lock
10+
11+
# Test files
12+
test/out/

Dockerfile

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM ubuntu:24.04
2+
3+
ARG FUNCTION_DIR="/var/task"
4+
WORKDIR ${FUNCTION_DIR}
5+
RUN mkdir -p ${FUNCTION_DIR}
6+
7+
ENV DEBIAN_FRONTEND=noninteractive
8+
9+
RUN apt-get update && \
10+
# NodeJS
11+
apt-get install -y wget curl && \
12+
(curl -fsSL https://deb.nodesource.com/setup_18.x | bash) && \
13+
apt-get install -y nodejs
14+
15+
COPY ./test ${FUNCTION_DIR}
16+
RUN npm install
17+
COPY ./output ${FUNCTION_DIR}/output

docker-compose.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
render:
3+
build:
4+
context: .
5+
command: node -e 'import("./test.js").then(m => m.render())'
6+
volumes:
7+
- ./test/out:/var/task/out
8+
9+
params:
10+
build:
11+
context: .
12+
command: node -e 'import("./test.js").then(m => m.params())'
13+
volumes:
14+
- ./test/out:/var/task/out

docs/_sidebar.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
- [Core](/interactive-diagram-core "Understand the underlying technology and API for rendering the interactive diagrams")
1010
- [Rendering](/interactive-diagram-rendering "How interactive diagrams are rendered in the sheet")
1111
- [Best Practices](/interactive-diagram-best-practices "Best Practices for using interactive diagram")
12-
- [Using Test Runner](/interactive-diagram-test-runner "How to use the test runner")
1312
- Global
1413
- [Global Capabilities](/global-capabilities "Time saving features")
14+
- Testing
15+
- [Interactive](/interactive-diagram-test-runner "How to use the test runner")
16+
- [Static](/static-diagram-test-runner "How to use the test runner")
1517
- Contributing
1618
- [Add To Docs](/contributing-to-docs "How to contribute to docs")

docs/static-diagram-test-runner.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Using Test Runner
2+
3+
## Introduction
4+
5+
The Test Runner allows visualisation of the custom static diagram and how it responds to each function that is user implemented such as `render`, `params`. It enables quick, iterative development of the custom diagram in an environment that mimics the ClearCalcs app for maximum compatibility when it is deployed in a real calculator.
6+
7+
It allows this compatibility by running the diagram code within a fake Dockerfile that has the same capabilities and restrictions. The following diagram functionality may be tested:
8+
9+
### Supported Features
10+
11+
- renders SVG with parameters
12+
- returns params
13+
14+
## Set up
15+
16+
1. Install Docker https://www.docker.com/get-started/
17+
18+
2. [Optional] Add params to `test/test.js`
19+
20+
```javascript
21+
const inputParams = {
22+
circleFill: "red",
23+
rectFill: "blue",
24+
};
25+
const storedParams = {...}
26+
```
27+
28+
3. Run Test Render and view output
29+
30+
```bash
31+
npm run test-render
32+
cd test/out
33+
# Open diagram.svg
34+
```
35+
36+
4. Run Test Params and view output
37+
38+
```bash
39+
npm run test-params
40+
# stdout
41+
Params [
42+
{ key: 'circleFill', type: 'string' },
43+
{ key: 'rectFill', type: 'string' },
44+
{ key: 'triangleFill', type: 'string' }
45+
]
46+
# Or go to test/out/params.json
47+
```

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
"docs": "cd docs && npm install && docsify serve -p 4444",
3333
"compile-static": "node static-build.js",
3434
"compile-static-fonts": "node svgdomPatches/scripts/compileFont.js",
35-
"compile-interactive-example": "EXAMPLE_PATH=examples/${EXAMPLE}/interactive/interface.ts parcel build examples/${EXAMPLE}/interactive/**/index.html"
35+
"compile-interactive-example": "EXAMPLE_PATH=examples/${EXAMPLE}/interactive/interface.ts parcel build examples/${EXAMPLE}/interactive/**/index.html",
36+
"test-render": "npm run compile-static && docker compose build && docker compose up render",
37+
"test-params": "npm run compile-static && docker compose build && docker compose up params"
3638
},
3739
"type": "module"
3840
}

test/index.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { runInSandbox, UserError } from "./sandbox.js";
2+
import * as fs from "fs";
3+
4+
export const params = withErrorHandling(async () => {
5+
const userCode = fs.readFileSync("output/compiled.js", {
6+
encoding: "utf8",
7+
flag: "r",
8+
});
9+
10+
return runInSandbox({
11+
userCode,
12+
resultCode: "return params()",
13+
params: [],
14+
});
15+
});
16+
17+
export const render = withErrorHandling(async ({ params }) => {
18+
const userCode = fs.readFileSync("output/compiled.js", {
19+
encoding: "utf8",
20+
flag: "r",
21+
});
22+
let storedParams, inParams;
23+
try {
24+
({ storedParams, ...inParams } = params);
25+
} catch (e) {
26+
// User has called render with no params or not with an object
27+
// We will pass down the original params and handle error in user code
28+
}
29+
30+
return runInSandbox({
31+
userCode,
32+
resultCode: "return render($0,$1)",
33+
params: inParams ? [inParams, storedParams] : [params, {}],
34+
});
35+
});
36+
37+
// Wrap result in object with {success: false/true}
38+
function withErrorHandling(fn) {
39+
return async (...args) => {
40+
try {
41+
const result = await fn(...args);
42+
43+
return {
44+
success: true,
45+
result,
46+
};
47+
} catch (ex) {
48+
if (ex instanceof UserError) {
49+
return {
50+
success: false,
51+
error: {
52+
message: ex.message,
53+
stack: ex.userStack,
54+
},
55+
};
56+
} else {
57+
throw ex;
58+
}
59+
}
60+
};
61+
}

test/out/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Example render and params will be outputted here

test/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "custom_diagram",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"author": "",
7+
"license": "UNLICENSED",
8+
"dependencies": {
9+
"isolated-vm": "^5.0.3"
10+
},
11+
"type": "module"
12+
}

test/sandbox.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import pkg from "isolated-vm";
2+
const { Isolate } = pkg;
3+
4+
const MEMORY_LIMIT_MB = 256;
5+
// Current svgdom bundle is large and slow, so we need to give it more time to solve
6+
// TODO: Reduce once performance improved
7+
const TIMEOUT_MS = 2000;
8+
9+
export class UserError extends Error {
10+
userStack = [];
11+
}
12+
13+
export function runInSandbox({ userCode, resultCode, params }) {
14+
const isolate = new Isolate({ memoryLimit: MEMORY_LIMIT_MB });
15+
const context = isolate.createContextSync();
16+
const jail = context.global;
17+
jail.setSync("global", jail.derefInto());
18+
// Example of exposing a custom function on the context's global:
19+
/*
20+
jail.setSync("log", function (...args) {
21+
console.log(...args);
22+
});
23+
*/
24+
25+
// Evaluate untrusted user code in isolated context
26+
try {
27+
context.evalSync(userCode, {
28+
timeout: TIMEOUT_MS,
29+
filename: "user.js",
30+
});
31+
32+
return context.evalClosureSync(resultCode, params, {
33+
timeout: TIMEOUT_MS,
34+
arguments: { copy: true },
35+
result: { copy: true },
36+
});
37+
} catch (ex) {
38+
throw handleUserError(ex);
39+
}
40+
}
41+
42+
function handleUserError(ex) {
43+
const userError = new UserError(`${ex.name}: ${ex.message}`);
44+
if (ex.stack) {
45+
userError.userStack = ex.stack.split("\n").filter((line) => line.match(/^ at .*user\.js/));
46+
}
47+
return userError;
48+
}

test/test.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { writeFile } from "node:fs/promises";
2+
import { render as renderInternal, params as paramsInternal } from "./index.js";
3+
4+
// EDIT YOUR PARAMS HERE
5+
const inputParams = {};
6+
7+
// EDIT YOUR STORED PARAMS HERE
8+
const storedParams = {};
9+
10+
export async function render() {
11+
const response = await renderInternal({
12+
params: { ...inputParams, storedParams },
13+
});
14+
15+
if (!response.success || !response.result) {
16+
// TODO: We do send back errors into the browser
17+
// Look at what the index.js returns and you can capture these
18+
console.log(response?.error?.message);
19+
throw new Error(`Failed to render`);
20+
}
21+
22+
await writeFile("out/diagram.svg", response.result);
23+
console.log("Successfully written svg");
24+
}
25+
26+
export async function params() {
27+
const response = await paramsInternal();
28+
29+
if (!response.success || !response.result) {
30+
// TODO: We do send back errors into the browser
31+
// Look at what the index.js returns and you can capture these
32+
throw new Error("Failed to render");
33+
}
34+
35+
await writeFile("out/params.json", JSON.stringify(response.result));
36+
console.log("Params", response.result);
37+
}

0 commit comments

Comments
 (0)