glcheck v0.3.5
glcheck
glcheck is a WebGL-focused testing framework. It runs unit and render tests using puppeteer which allows it to run automated tests and generate coverage reports for both WebGL 1 and 2 applications.
Usage
To install, simply run:
npm i -D glcheckRun using:
npx glcheckBy default, glcheck will read configuration from glcheck.config.json in the current directory, from which it will read the following options:
- unitTests (default:
[]): List of JavaScript files to run as unit tests. - unitTestDir (default:
"glcheck-tests/unit-tests/"): Directory to output unit test results into. This includes an HTML page that will be run by puppeteer, but it can also simply be opened in a browser. - assetDir (default:
null): Directory to load assets from. Contents from this directory will be available to unit tests in the subdirectoryassets/. - renderTests (default:
[]): List of HTML files to run as render tests. - referenceImageDir (default:
"glcheck-tests/reference-images/"): Directory containing render test reference images. - renderTestThreshold (default:
0.99): Match threshold between 0 and 1 for render tests. - renderTimeout (default:
5000): Timeout for each render test in milliseconds. - saveRenderFailures (default:
false): Whether to save render failure and diff images for render tests. - renderFailureDir (default:
"glcheck-tests/render-failures/"): Where to save render failure and diff images for render tests. - serverPort (default:
7171): Port to run the local server on for puppeteer. - headless (default:
true): Whether to run headless. - coverage (default:
true): Whether to generate coverage results that are consumable by Istanbul. - coverageFiles (default:
[]): Files to include in coverage results. - only (default:
null): Only run the provided test file (can be a glob pattern to run multiple files).
Full glcheck command line usage is as follows:
glcheck [--help] [--version] [--config PATH] [--unit-test-dir PATH] [--asset-dir PATH] [--reference-image-dir PATH] [--render-test-threshold VAL] [--render-timeout TIME] [--save-render-failures {true/false}] [--render-failure-dir PATH] [--server-port PORT] [--coverage {true/false}] [--headless {true/false}] [--only PATH]Command line arguments will always override options from the config file:
- --help: Show a help message and exit.
- --version: Show version number and exit.
- --config (default:
"glcheck.config.json"): Path to config file. - --unit-test-dir (default:
"glcheck-results/"): Directory to output unit test results into. This includes an HTML page that will be run by puppeteer, but it can also simply be opened in a browser. - --asset-dir (default:
null): Directory to load assets from. Contents from this directory will be available to unit tests in the subdirectoryassets/. - --reference-image-dir (default:
"glcheck-tests/reference-images/"): Directory containing render test reference images. - --render-test-threshold (default:
0.99): Match threshold between 0 and 1 for render tests. - --render-timeout (default:
5000): Timeout for each render test in milliseconds. - --save-render-failures (default:
false): Whether to save render failure and diff images for render tests. - --render-failure-dir (default:
"glcheck-tests/render-failures/"): Where to save render failure and diff images for render tests. - --server-port (default:
7171): Port to run the local server on for puppeteer. - --headless (default:
true): Whether to run headless. - --coverage (default:
true): Whether to generate coverage results that are consumable by Istanbul. - --only (default:
null): Only run the provided test file (can be a glob pattern to run multiple files).
Unit Tests
A simple unit test suite using glcheck might look like the following:
glcheck("Test myApp", (t, canvas) => {
const gl = canvas.createContext("webgl2");
gl.enable(gl.DEPTH_TEST);
t.parameterEqual(gl, gl.DEPTH_TEST, true, "Depth test enabled");
t.parameterNotEqual(gl, gl.DEPTH_TEST, false, "Depth test not disabled");
gl.clearColor(1, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
t.pixelEqual(gl, [255, 0, 0, 255], "Framebuffer is red");
t.pixelNotEqual(gl, [0, 0, 255, 255], "Framebuffer is not blue");
// Buffer tests are WebGL 2-only
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 2, 3, 4]), gl.STATIC_READ);
t.bufferEqual(gl, gl.ARRAY_BUFFER, [1, 2, 3, 4], "Buffer data counts up");
t.bufferNotEqual(gl, gl.ARRAY_BUFFER, [4, 3, 2, 1], "Buffer data does not count down");
t.done();
});Unit tests are defined using the glcheck function. The general structure is as follows:
glcheck("My test", (t, canvas) => {
// Write some tests
t.done();
});The arguments to the test function are a tester object (described below) and a DOM canvas element. Each test will create a fresh canvas for testing and tear it down afterwards.
Test functions can also be async:
glcheck("My test", async (t, canvas) => {
const data = await getAsyncData();
// Write some tests
t.done();
});The tester object's done method indicates that the test has completed and can also be used in async contexts:
t.done(): Indicate that a test has completed.
glcheck("Basic", async (t, canvas) => {
t.ok(true, "ok");
t.done();
});
glcheck("Async", async (t, canvas) => {
setTimeout(() => {
t.ok(true, "ok");
t.done();
}, 50);
});The tester object exposes the following basic assertions:
t.ok(actual, message): Check the truthiness ofactual.t.notOk(actual, message): Check the falsiness ofactual.t.equal(actual, expected, message): Check thatactualandexpectedare shallowly equal.t.notEqual(actual, expected, message): Check thatactualandexpectedare not shallowly equal.t.deepEqual(actual, expected, message): Check thatactualandexpectedare deeply equal (e.g. for objects and arrays).t.notDeepEqual(actual, expected, message): Check thatactualandexpectedare not deeply equal (e.g. for objects and arrays).t.throws(fn, message): Check thatfnthrows an exception.t.doesNotThrow(fn, message): Check thatfndoes not throw an exception.
glcheck("Basic assertions", (t, canvas) => {
t.ok(true, "ok");
t.equal(1, 1, "equal");
t.deepEqual({a: 1, b: 2}, {a: 1, b: 2}, "deepEqual");
// deepEqual considers all array types equivalent
t.deepEqual([1, 2, 3, 4], new Float32Array([1, 2, 3, 4]), "deepEqual different array types");
t.throws(() => {throw "Throw";}, "throws");
t.done();
});The tester object also exposes WebGL-specific assertions:
t.parameterEqual(gl, parameter, expected, message): Check if the WebGLparameter(passed togl.getParameter) matchesexpected.t.parameterNotEqual(gl, parameter, expected, message): Check if the WebGLparameter(passed togl.getParameter) does not matchexpected.t.pixelEqual(gl,[ uv=[0.5, 0.5],] expected, message): Check if the currently bound framebuffer has the valueexpectedat the pixel indicated byuv.uvis a two-element array with[0, 0]indicating the bottom-left of the canvas, and[1, 1]indicating the top-right.t.pixelNotEqual(gl,[ uv=[0.5, 0.5],] expected, message): Check if the currently bound framebuffer does not have the valueexpectedat the pixel indicated byuv.uvis a two-element array with[0, 0]indicating the bottom-left of the canvas, and[1, 1]indicating the top-right.t.bufferEqual(gl, binding, expected, message)(WebGL 2-only): Check if the buffer bound tobindingcontains the values inexpected. Matching will be done based on the array type ofexpectedand will default toFloat32Array.t.bufferNotEqual(gl, binding, expected, message)(WebGL 2-only): Check if the buffer bound tobindingdoes not contain the values inexpected. Matching will be done based on the array type ofexpectedand will default toFloat32Array.
glcheck("GL assertions", (t, canvas) => {
const gl = canvas.getContext("webgl2");
t.parameterEqual(gl, gl.DEPTH_TEST, true, "parameterEqual");
t.parameterEqual(gl, gl.VIEWPORT, [10, 20, 30, 40], "parameterEqual array");
t.pixelEqual(gl, [255, 0, 0, 255], "pixelEqual center");
t.pixelEqual(gl, [0.25, 0.75], [255, 0, 0, 255], "pixelEqual upper-left");
// Buffer assertions are WebGL 2-only
const floatBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, floatBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 2, 3, 4]), gl.STATIC_READ);
t.bufferEqual(gl, gl.ARRAY_BUFFER, [1, 2, 3, 4], "bufferEqual");
const byteBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, byteBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Uint8Array([5, 6, 7, 8]), gl.STATIC_READ);
// bufferEqual will respect the type of the "expected" array passed
t.bufferEqual(gl, gl.ARRAY_BUFFER, new Uint8Array([5, 6, 7, 8]), "bufferEqual bytes");
t.done();
});Finally, the tester object exposes the async helper loopUntil for tests that require asynchrony:
t.loopUntil(fn): Returns a promise that starts arequestAnimationFrameloop, callingfnon each frame and resolving when it returns true.
glcheck("loopUntil helper", async (t, canvas) => {
const gl = canvas.getContext("webgl2");
const query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, query);
// ...
gl.endQuery(query);
await t.loopUntil(() => gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE));
t.equal(gl.getQueryParameter(query, GL.QUERY_RESULT), expected, "Query results");
t.done();
});Render Tests
Render tests are run by providing a list of HTML files to render in the configuration file:
{
"renderTests": "test/render/*.html"
}To be usable as a render test, a page must simply indicate when it has completed rendering by setting the global glcheck_renderDone to true:
window.glcheck_renderDone = true;NOTE: It recommended to stop animations once glcheck_renderDone is set to ensure consistent results.
glcheck also exposes a helper function glcheck_setRAFCount to pages loaded as render tests to simplify controlling animations and signaling that a render is complete.
glcheck_setRAFCount(n): InstrumentrequestAnimationFrameto only loopntimes and setglcheck_renderDoneafterwards.
This can be helpful in instrumenting a page to stop rendering when used as a render test, but render normally otherwise.
if (window.glcheck_setRAFCount) {
window.glcheck_setRAFCount(10);
}
requestAnimationFrame(function draw() {
requestAnimationFrame(draw);
// Will loop 10 times when run by glcheck,
// normally otherwise.
});