Canvas test generator (gentest.sh) ================================== The script gentest.sh is used to generate canvas WPT tests, found under wpt/html/canvas. # Purpose for generating canvas tests Generating tests for the canvas API has multiple advantages. It allows generating lots of tests with minimal boilerplate and configuration. In particular: - Canvas tests all have common boilerplate, like defining a whole HTML page, creating a canvas and reading back pixels. The code we care about is usually only a few lines of JavaScript. By using a test generator, we can write tests focussing on these few relevant lines, abstracting away all of the boilerplate needed to run these lines. - Canvas exists in multiple flavors (HTMLCanvasElement, OffscreenCanvas) and can run in different environments (main thread, worker). Using a code generator allows tests to be implemented only once and run then in all the flavors or environment we need test coverage for. - Canvas rendering can be affected by a large number of states. Implementations can have different code paths for different permutations of these states. For instance, simply testing that a rectangle is correctly drawn requires validating different permutations of whether the canvas has an alpha channel, whether layers are used, whether the context uses a globalAlpha, which globalCompositeOperation is used, which filters are used, whether shadows are enabled, and so on. Bugs occurring only for some specific combinations of these states have been found. A test generator allows for easily creating a large number of tests, or tests validating a large number of variant permutations, all with minimal boilerplate. # Running gentest.sh You can generate canvas tests by running `wpt update-built --include canvas`, or by running `gentest.sh` directly: - Make a python virtual environment somewhere (it doesn't matter where): `python3 -m venv venv` - Enter the virtual environment: `source venv/bin/activate` - This script depends on the `cairocffi`, `jinja2` and `pyyaml` Python packages. You can install them using [`requirements_build.txt`]( https://github.com/web-platform-tests/wpt/blob/master/tools/ci/requirements_build.txt): `python3 -m pip install -r tools/ci/requirements_build.txt` - Change to the directory with this script and run it: `python3 gentest.py` See [WPT documentation]( https://web-platform-tests.org/running-tests/from-local-system.html#system-setup ) for the current minimal Python version required. If you modify `gentest.py`, it's recommended to use that exact Python version to avoid accidentally using new Python features that aren't be supported by that minimal version. [pyenv](https://github.com/pyenv/pyenv) can be used instead of the `venv` approach described above, to pin the `html/canvas/tools` folder to that exact Python version, without impacting the rest of the system. For instance: ```shell pyenv install 3.8 cd html/canvas/tools pyenv local 3.8 python3 -m pip install -r $WPT_CHECKOUT/tools/ci/requirements_build.txt python3 gentest.py ``` # Canvas test definition The tests are defined in YAML files, found in `wpt/html/canvas/tools/yaml`. The YAML definition files consists of a sequence of dictionaries, each with at a minimum the keys `name:` and `code:`. For instance: ```yaml - name: 2d.sample.draws-red code: | ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 10, 10); @assert pixel 5,5 == 255,0,0,255; - name: 2d.sample.draws-green code: | ctx.fillStyle = 'green'; ctx.fillRect(0, 0, 10, 10); @assert pixel 5,5 == 0,255,0,255; ``` From this configuration, the test generator would produce multiple test files and fill-in the boilerplate needed to run these JavaScript lines. See the constants `_TEST_DEFINITION_PARAMS` and `_GENERATED_PARAMS` in the `gentest.sh` for a full list and description of the available parameters. ## Jinja templating The test generator uses Jinja templates to generate the different test files it produces. The templates can be found under `wpt/html/canvas/tools/templates`. When rendering templates, Jinja uses a dictionary of parameters to lookup variables referred to by the template. In the test generator, this dictionary is actually the YAML dictionary defining the test itself. Take for instance the test: ```yaml - name: 2d.sample.draws-red code: | ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 10, 10); @assert pixel 5,5 == 255,0,0,255; ``` In the template `.../templates/testharness_element.html`, the title of the generated HTML is defined as: ```html Canvas test: {{ name }} ``` When rendering this template, Jinja looks-up the `name:` key from the YAML test definition, which in the example above would be `2d.sample.draws-red`, producing this HTML result: ```html Canvas test: 2d.sample.draws-red ``` Now, more interestingly, all the parameter values in the test definition are also Jinja templates. They get rendered on demand, before being used by Jinja into another template. Since all of these use the test's YAML definition as param dictionary, test parameters can refer to each others: ```yaml - name: 2d.sample.draws-red expected_color: 255,0,0,{{ alpha }} alpha: 255 code: | ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 10, 10); @assert pixel 5,5 == {{ expected_color }}; ``` All the test parameters are also registered as templates loadable from other Jinja templates, with `{% import ... %}` statements for instance. This can be useful to organize the test definition and allow reuse of Jinja statements. For instance: ```yaml - name: 2d.sample.macros macros: | {% macro rgba_format(color) %} {% set r, g, b, a = color -%} rgba{{ (r, g, b, a) -}} {% endmacro %} {% macro assert_format(color) %} {% set r, g, b, a = color -%} {{- '%d,%d,%d,%d' | format(r, g, b, a * 255) -}} {% endmacro %} code: | {% import 'macros' as m %} ctx.fillStyle = '{{ m.rgba_format(color) }}'; ctx.fillRect(0, 0, 10, 10); @assert pixel 5,5 == {{ m.assert_format(color) }}; color: [64, 128, 192, 1.0] ``` These types of parameterization might seem strange and overkill in toy examples like these, but it's in fact really useful when using the `variants:` feature ([more on this below](#test-variants)). ## Canvas Types By default, the generator produces three flavors of each tests, one for each of three different canvas types: - An `HTMLCanvasElement`. - An `OffscreenCanvas`, used in a main thread script. - An `OffscreenCanvas`, used in a worker. `HTMLCanvasElement` tests get generated into the folder `.../canvas/element`, while the main thread and worker `OffscreenCanvas` tests are generated in the folder `.../canvas/offscreen`. Some tests are specific to certain canvas types. The canvas types to be generated can be specified by setting the `canvas_types:` config to a list with one or many of the following strings: - `'HtmlCanvas'` - `'OffscreenCanvas'` - `'Worker'` For instance: ```yaml - name: 2d.sample.offscreen-specific canvas_types: ['OffscreenCanvas', 'Worker'] code: | assert_not_equals(canvas.convertToBlob(), null); ``` ## JavaScript tests (testharness.js) The test generator can generate both JavaScript tests (`testharness.js`), or Reftests. By default, the generator produces [JavaScript tests]( https://web-platform-tests.org/writing-tests/testharness.html). These are implemented with the [testharness.js library]( https://web-platform-tests.org/writing-tests/testharness-api.html). Assertions must be used to determine whether they succeed. Standard assertions provided by `testharness.js` can be used, like `assert_true`, `assert_equals`, etc. ### Canvas specific helpers Canvas tests also have access to additional assertion types and other helpers defined in `wpt/html/canvas/resources/canvas-tests.js`. Most of these however are private and meant to be used via macros provided by this test generator (denoted by the character "@"). Note that these "@" macros are implemented as regexp-replace, so their syntax is very strict (e.g. they don't tolerate extra whitespaces and some reserve `;` as terminating character). - `@assert pixel x,y == r,g,b,a;` Asserts that the color at the pixel position `[x, y]` exactly equals the RGBA values `[r, g, b, a]`. - `@assert pixel x,y ==~ r,g,b,a;` Asserts that the color at the pixel position `[x, y]` approximately equals the RGBA values `[r, g, b, a]`, within +/- 2. - `@assert pixel x,y ==~ r,g,b,a +/- t;` Asserts that the color at the pixel position `[x, y]` approximately equals the RGBA values `[r, g, b, a]`, within +/- `t` for each individual channel. - `@assert throws *_ERR code;` Shorthand for `assert_throws_dom`, running `code` and verifying that it throws a DOM exception `*_ERR` (e.g. `INDEX_SIZE_ERR`). - `@assert throws *Error code;` Shorthand for `assert_throws_js`, running `code` and verifying that it throws a JavaScript exception `*Error` (e.g. `TypeError`). - `@assert actual === expected;` Shorthand for `assert_equals`, asserting that `actual` is the same as `expected`. - `@assert actual !== expected;` Shorthand for `assert_not_equals`, asserting that `actual` is different than `expected`. - `@assert actual =~ expected;` Shorthand for `assert_regexp_match`, asserting that `actual` matches the regular expression `expected`. - `@assert cond;` Shorthand for `assert_true`, but evaluating `cond` as a boolean by prefixing it with `!!`. ### JavaScript test types `testharness.js` allows the creation of synchronous, asynchronous or promise tests (see [here]( https://web-platform-tests.org/writing-tests/testharness-api.html#defining-tests ) for details). To choose what test types to generate, set the `test_type` parameter to one of: - `sync` - `async` - `promise` For instance, a synchronous test would use `test_type: sync`: ```yaml - name: 2d.sample.sync-test desc: Example synchronous test canvas_types: ['HtmlCanvas'] test_type: sync code: | assert_regexp_match(canvas.toDataURL(), /^data:/); ``` Given this config, the code generator would generate an `HTMLCanvasElement` test with the following `