Spracherkennung für: .md vermutete Sprache: SPS {SPS[68] Delphi[60] VDM[51]} [Methode: maximale Elemente, drei Dimensionen]
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.
9
cd html/canvas/tools
pyenv local
3.
9
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
<title>Canvas test: {{ name }}</title>
```
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
<title>Canvas test:
2d.sample.draws-red</title>
```
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 `<script>` tag (
not showing the rest
of the HTML file).
``` JavaScript
test(t => {
var canvas = document.getElementById(
'c');
var ctx = canvas.getContext(
'2d');
assert_regexp_match(canvas.toDataURL(), /^data:/);
},
"Example synchronous test");
```
To test asynchronous code, `test_type: async` can be use as
in:
```yaml
- name:
2d.sample.async-test
desc: Example asynchronous test
canvas_types: [
'HtmlCanvas']
test_type: async
code: |
canvas.toBlob(t.step_func_done(blob => {
assert_greater_than(blob.size,
0);
}));
```
``` JavaScript
async_test(t => {
var canvas = document.getElementById(
'c');
var ctx = canvas.getContext(
'2d');
canvas.toBlob(t.step_func_done(blob => {
assert_greater_than(blob.size,
0);
}));
},
"Example asynchronous test");
```
Promise-based APIs would use `test_type: promise`,
for instance:
```yaml
- name:
2d.sample.promise-test
desc: Example promise test
canvas_types: [
'OffscreenCanvas']
test_type: promise
code: |
const blob = await canvas.convertToBlob();
assert_greater_than(blob.size,
0);
```
```JavaScript
promise_test(async t => {
var canvas = new OffscreenCanvas(
100,
50);
var ctx = canvas.getContext(
'2d');
const blob = await canvas.convertToBlob();
assert_greater_than(blob.size,
0);
},
"Example promise test");
```
To maintain compatibility
with old tests (
until they are updated), the test
generator will use a legacy test harness
if the `test_type` is omitted. This
test harness is a quirky hybrid between `sync`
and `async` tests. These tests
are implemented
with an `async_test()` fixture, but the generator automatically
invokes `t.done()` after the body, making is behave like a synchronous test.
To
implement actual async test, the test body must call `deferTest();`
and manually
call `t.done()`
to finish the test. Newer test should prefer specifying
`test_type`
and using standard `testharness.js` APIs instead
of these
canvas-specific ones.
## Reftests
The code generator can also be used
to generate [reftests](
https:
//web-platform-tests.org/writing-tests/reftests.html). These tests are
comprised
of a test HTML page
and a reference HTML page. Both are rendered
by
the test runner
and the results are compared pixel
by pixel. These tests
do not
use `testharness.js`
and thus cannot use assertions.
### Writing reftests
To write a reftest, use a `reference:` key
in the YAML config:
```yaml
- name:
2d.sample.reftest
desc: Example reftest
code: |
ctx.scale(
2,
2);
ctx.fillRect(
5,
5,
15,
15);
reference: |
ctx.fillRect(
10,
10,
30,
30);
```
This will produce a test file `
2d.sample.reftest.html`
and a reference file
`
2d.sample.reftest-expected.html`,
for HTMLCanvasElement
and main thread/worker
OffscreenCanvas.
### Reftest fuzzy matching
By default, the test will fail
if the test runner sees
any rendering difference
between the test
and reference page.
If some difference is expected,
fuzzy-matching can be used
by using the `fuzzy:` config, as
in:
```yaml
- name:
2d.sample.reftest.fuzzy
desc: Example reftest using fuzzy matching
fuzzy: maxDifference=
0-
1; totalPixels=
0-
100
code: |
ctx.fillStyle =
'rgba(0, 255, 0, 0.5)';
ctx.fillRect(
5,
5,
10,
10);
reference: |
ctx.fillStyle =
'rgba(128, 255, 128, 1)';
// Should it be 127 or 128?
ctx.fillRect(
5,
5,
10,
10);
```
### Types
of reference files
The code generator supports three types
of reference files that can be used
to
validated the test results.
- `reference:` generates a reference file using JavaScript code writing
to a
canvas, similarly
to what the `code:` block
of the test does.
For instance:
```yaml
- name:
2d.sample.reftest.reference
desc: Reftest comparing one canvas drawing
with another.
code: |
ctx.scale(
2,
2);
ctx.fillRect(
5,
5,
15,
15);
reference: |
ctx.fillRect(
10,
10,
30,
30);
```
- `html_reference:` can be useful
to compare canvas rendering against HTML+CSS,
or
against SVG.
For instance:
```yaml
- name:
2d.sample.reftest.html_reference
desc: Example reftest using an html_reference
fuzzy: maxDifference=
0-
1; totalPixels=
0-
24
code: |
ctx.filter =
'blur(5px)';
ctx.fillRect(
5,
5,
10,
10);
html_reference: |
<svg xmlns=
"https://www.w3.org/2000/svg"
width=
"{{ size[0] }}" height=
"{{ size[1] }}">
<filter id=
"filter" x=
"-100%" y=
"-100%" width=
"300%" height=
"300%">
<feGaussianBlur stdDeviation=
"5" />
</filter>
<g filter=
"url(#filter)">
<rect x=
"5" y=
"5" width=
"10" height=
"10"></rect>
</g>
</svg>
```
- `cairo_reference:` produces a reftest using a reference image generated
from
Python code using the `pycairo` library. The variable `cr` provides a
`cairo.Context` instance that can be used
to draw the reference image.
For
instance:
```yaml
- name:
2d.sample.reftest.cairo_reference
desc: Example reftest using a cairo_reference
code: |
ctx.fillStyle =
'rgb(255, 128, 64)';
ctx.fillRect(
5,
5,
50,
30);
ctx.globalCompositeOperation =
'color-dodge';
ctx.fillStyle =
'rgb(128, 200, 128)';
ctx.fillRect(
15,
15,
50,
30);
cairo_reference: |
cr.set_source_rgb(
255/
255,
128/
255,
64/
255)
cr.rectangle(
5,
5,
50,
30)
cr.fill()
cr.set_operator(cairo.OPERATOR_COLOR_DODGE)
cr.set_source_rgb(
128/
255,
200/
255,
128/
255)
cr.rectangle(
15,
15,
50,
30)
cr.fill()
```
- `img_reference:` produces a reftest using a pre-generated image file. This can
be useful
for trivial images (e.g. a solid color), but avoid using this
for
non-trivial images as it
's important to be able to know how these images are
generated, so we could inspect
or modify them later
if needed. Example:
```yaml
- name:
2d.sample.reftest.img_reference
desc: Example reftest using an img_reference
size: [
100,
50]
code: |
ctx.fillStyle =
'rgb(0, 255, 0)';
ctx.fillRect(
0,
0, {{ size[
0] }}, {{ size[
1] }});
img_reference: /images/green-
100x50.png
```
### Promise reftests
As explained [
in the WPT documentation](
https:
//web-platform-tests.org/writing-tests/reftests.html), the test runner
screenshots reftests after the page is loaded
and pending paints are completed.
To support asynchronous calls
or promises, we must let the test runner know when
the test is done. This can be done
by using `test_type: promise`,
for instance:
```yaml
- name:
2d.sample.reftest.promise
desc: Example
of a reftest using promises
test_type: promise
code: |
ctx.beginLayer();
ctx.fillRect(
5,
5,
10,
10);
// Checks that layers survive frame boundaries.
await new Promise(resolve => requestAnimationFrame(resolve));
ctx.endLayer();
reference:
ctx.fillRect(
5,
5,
10,
10);
```
## Test variants
Test parameterization is a very useful tool, allowing a test
to be exercised
on
a number
of different inputs
with minimal boilerplate. The test generator
supports this via the `variants:` parameter. Let
's begin by showing a motivating
example. Say you would like
to compare canvas rendering
with SVG. You might want
to implement these two tests:
```yaml
- name:
2d.compare-canvas-
and-svg.draws-red
color:
'red'
code: |
ctx.fillStyle =
'{{ color }}';
ctx.fillRect(
0,
0,
10,
10);
html_reference: |
<svg xmlns=
"https://www.w3.org/2000/svg"
width=
"{{ size[0] }}" height=
"{{ size[1] }}">
<rect x=
"0" y=
"0" width=
"10" height=
"10" fill=
"{{ color }}"/>
</svg>
- name:
2d.compare-canvas-
and-svg.draws-green
color:
'green'
code: |
ctx.fillStyle =
'{{ color }}';
ctx.fillRect(
0,
0,
10,
10);
html_reference: |
<svg xmlns=
"https://www.w3.org/2000/svg"
width=
"{{ size[0] }}" height=
"{{ size[1] }}">
<rect x=
"0" y=
"0" width=
"10" height=
"10" fill=
"{{ color }}"/>
</svg>
```
This works, but there
's a lot of duplication and it would not scale for testing
with a larger number
of inputs. You could cut down
on the duplication
by using
YAML `&` anchors
and `*` aliases,
for instance:
```yaml
- name:
2d.compare-canvas-
and-svg.draws-red
fill_style:
'red'
code: &draw-rect-
with-canvas |
ctx.fillStyle =
'{{ fill_style }}';
ctx.fillRect(
0,
0,
10,
10);
html_reference: &draw-rect-
with-svg |
<svg xmlns=
"https://www.w3.org/2000/svg"
width=
"{{ size[0] }}" height=
"{{ size[1] }}">
<rect x=
"0" y=
"0" width=
"10" height=
"10" fill=
"{{ fill_style }}"/>
</svg>
- name:
2d.compare-canvas-
and-svg.draws-green
fill_style:
'green'
code: *draw-rect-
with-canvas
html_reference: *draw-rect-
with-svg
```
This however adds indirections making the test hard
to read. Test must still be
duplicated, but they now mostly contain boilerplate
and this would still
not
scale
for a larger number
of test inputs.
With variants, you could write the
test like this:
```yaml
- name:
2d.compare-canvas-
and-svg
code: |
ctx.fillStyle =
'{{ fill_style }}';
ctx.fillRect(
0,
0,
10,
10);
html_reference: |
<svg xmlns=
"https://www.w3.org/2000/svg"
width=
"{{ size[0] }}" height=
"{{ size[1] }}">
<rect x=
"0" y=
"0" width=
"10" height=
"10" fill=
"{{ fill_style }}"/>
</svg>
variants:
- draws-red: {fill_style: red}
draws-green: {fill_style: green}
```
Now, this has minimal boilerplate
and easily scales
to many more test inputs.
Variant are defined as a list
of dictionaries
of dictionaries.
In the above
examples, the `-` before `draws-red:` denotes a list item, which is a dictionary
with two keys: `draws-red`
and `draws-green`. That dictionary defines a variant
dimension. A test file will be generated
for each items
in this dictionary. The
`draws-red`
and `draws-green` strings are the variant names, which are appended
to the generated test name. The value associated
with these keys (
for instance
`{fill_style: red}`) are parameters that get merged over the base test
parameters
for that test instance.
If the `variants:` list has more than one dictionary, the test generator will
generate tests
with the cross product
of all variant dimensions.
For instance,
this config:
```yaml
- name:
2d.grid
code: ctx.{{ variant_names[
0] }} =
'{{ color }}';
variants:
- fillStyle:
shadowColor:
- red: {color:
'#F00'}
blue: {color:
'#00F'}
```
Will generate:
- Test name: `
2d.grid.fillStyle.red`,
with code: `ctx.fillStyle =
'#F00';`
- Test name: `
2d.grid.fillStyle.blue`,
with code: `ctx.fillStyle =
'#00F';`
- Test name: `
2d.grid.shadowColor.red`,
with code: `ctx.shadowColor =
'#F00';`
- Test name: `
2d.grid.shadowColor.blue`,
with code: `ctx.shadowColor =
'#00F';`
This last example uses the variable `{{ variant_names[
0] }}`
to avoid having
to
write `fillStyle`
and `shadowColor` twice, once
in the variant name
and once
on
the variant params. `variant_names` is a special variable which holds a list
of
the variant names
for that particular test instance. When generating the test
`
2d.grid.fillStyle.red`
for instance, `variant_names` would be set
to
`[
'fillStyle',
'red']`.
## Grid tests
Variants are really useful
to easily validate many test inputs, but this can
lead
to a large number
of test files being generated.
For instance, the
2d
canvas compositing pipeline is affected
by these states (
and more):
- Drawing
with `fillRect`, `drawImage`, `CanvasPattern`.
- globalAlpha
to `
1.
0`
or non `
1.
0`.
- All
26 `globalCompositeOperation`
-
With shadow enabled
or disabled.
- Using a filter
or not.
-
With transforms
or not.
-
With an opaque
or transparent canvas.
-
For HTMLCanvasElement, main thread OffscreenCanvas
and worker
OffscreenCanvas.
In practice, we have found bugs
in the canvas what only happen
for very specific
combinations
of states, like, using a specific `globalCompositeOperation`,
with
transforms
and shadows enabled, but only when `drawImage` is used. We might want
be thorough
and test as many combinations
of these states as we can, but testing
all permutations would require $`
3*
2*
26*
2*
2*
2*
2*
3 =
7488`$ tests. Putting all
of
these
in different files would be unmanageable. Comes variant grids
to the
rescue!
If we generated test files each testing all composite operations
in a
grid, we
'd only need a much more reasonable $`3*2*2*2*2*2*3 = 288`$ files.
To generate a variant grid, use the `variants_layout:` config. This has
to be a
list
of the same lengths as the `variants:` list (as long as there are variant
dimensions).
For each dimension, `variants_layout:` must hold either the
string
`multi_files`
or `single_file`. Variant dimensions marked as `single_file` will
be expanded
in the same file.
For instance:
```yaml
- name: grid-example
variants:
- A1:
A2:
- B1:
B2:
- C1:
C2:
- D1:
D2:
- E1:
E2:
variants_layout:
- single_file
- multi_files
- single_file
- multi_files
- single_file
```
Because this test has
2 `multi_files` dimension
with two variants each,
4 files
would be generated:
- grid-example.B1.D1
- grid-example.B1.D2
- grid-example.B2.D1
- grid-example.B2.D2
Then, the
3 `single_file` dimensions would produce $`
2*
2*
2 =
8`$ tests
in each
of these files.
For JavaScript tests, each
of these test would be generated
in
sequence, each
with their own `test()`, `async_test()`
or `promise_test()`
fixture. Reftest
on the other hand would produce a
2x2x2 grid, as follows:
```
A1.C1.E1 A2.C1.E1
A1.C2.E1 A2.C2.E1
A1.C1.E2 A2.C1.E2
A1.C2.E2 A2.C2.E2
```