Bug 1921829 [wpt PR 48382] - Add a new canvas WPT template using pycairo-generated reference images, a=testonly

Automatic update from web-platform-tests
Add a new canvas WPT template using pycairo-generated reference images

Using the "cairo_reference:" key in the YAML config, we can now express
the expected result of a test using pycairo code. Contrary to the
existing "expected:" key which produces an image that is only used for
"informational" purpose, "cairo_reference:" produces a reference test,
meaning that the test runner automatically compares the test result
with that generated image and fails the test if the result differs.

Both single variant tests (non-variant or tests with a single variant
per file) and variant grids are supported. For single variant, the
generated reference file is an HMTL page with a single <img> tag
pointing to the PNG generated with pycairo. To cut down on the number of
files generated for variant grids, all the reference images for all of
the cells of the grid are packed into a single PNG file,
3d-model-texture-style. The reference HTML file is a grid of <img> tags,
each framing a different portion of the PNG image.

Bug: 364549423
Change-Id: Icb3c246b22347054b7cc9b5e5cc40d21b30565bd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5894774
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1369396}

--

wpt-commits: 2af17a84e98cd28940d2882c68ff824d6e98f4b7
wpt-pr: 48382
This commit is contained in:
Jean-Philippe Gravel 2024-10-16 22:19:27 +00:00 committed by moz-wptsync-bot
parent a965622d9c
commit 19fc588c80
3 changed files with 130 additions and 17 deletions

View File

@ -39,6 +39,7 @@ import dataclasses
import enum
import importlib
import itertools
import math
import os
import pathlib
import sys
@ -257,6 +258,7 @@ class _CanvasType(str, enum.Enum):
class _TemplateType(str, enum.Enum):
REFERENCE = 'reference'
HTML_REFERENCE = 'html_reference'
CAIRO_REFERENCE = 'cairo_reference'
TESTHARNESS = 'testharness'
@ -452,15 +454,21 @@ class _Variant():
return frozenset(_CanvasType(t) for t in canvas_types)
def _get_template_type(self) -> _TemplateType:
if 'reference' in self.params and 'html_reference' in self.params:
reference_types = (('reference' in self.params) +
('html_reference' in self.params) +
('cairo_reference' in self.params))
if reference_types > 1:
raise InvalidTestDefinitionError(
f'Test {self.params["name"]} is invalid, "reference" and '
'"html_reference" can\'t both be specified at the same time.')
f'Test {self.params["name"]} is invalid, only one of '
'"reference", "html_reference" or "cairo_reference" can be '
'specified at the same time.')
if 'reference' in self.params:
return _TemplateType.REFERENCE
if 'html_reference' in self.params:
return _TemplateType.HTML_REFERENCE
if 'cairo_reference' in self.params:
return _TemplateType.CAIRO_REFERENCE
return _TemplateType.TESTHARNESS
def finalize_params(self, jinja_env: jinja2.Environment,
@ -476,13 +484,10 @@ class _Variant():
if isinstance(self._params['size'], list):
self._params['size'] = tuple(self._params['size'])
if 'reference' in self._params:
self._params['reference'] = _preprocess_code(
jinja_env, self._params['reference'], self._params)
if 'html_reference' in self._params:
self._params['html_reference'] = _preprocess_code(
jinja_env, self._params['html_reference'], self._params)
for ref_type in {'reference', 'html_reference', 'cairo_reference'}:
if ref_type in self._params:
self._params[ref_type] = _preprocess_code(
jinja_env, self._params[ref_type], self._params)
code_params = dict(self.params)
if _CanvasType.HTML_CANVAS in self.params['canvas_types']:
@ -503,7 +508,7 @@ class _Variant():
_validate_test(self._params)
def generate_expected_image(self, output_dirs: _OutputPaths) -> None:
"""Creates a reference image using Cairo and save filename in params."""
"""Creates an expected image using Cairo and save filename in params."""
expected = self.params['expected']
if expected == 'green':
@ -660,7 +665,8 @@ class _VariantGrid:
'fonts': self._param_set('fonts'),
}
if self.template_type in (_TemplateType.REFERENCE,
_TemplateType.HTML_REFERENCE):
_TemplateType.HTML_REFERENCE,
_TemplateType.CAIRO_REFERENCE):
grid_params['desc'] = self._unique_param('desc')
return grid_params
@ -692,9 +698,12 @@ class _VariantGrid:
f'{output_files.offscreen}.w.html')
params['is_test_reference'] = True
is_html_ref = self.template_type == _TemplateType.HTML_REFERENCE
ref_template_name = (f'reftest{grid}.html'
if is_html_ref else f'reftest_element{grid}.html')
templates = {
_TemplateType.REFERENCE: f'reftest_element{grid}.html',
_TemplateType.HTML_REFERENCE: f'reftest{grid}.html',
_TemplateType.CAIRO_REFERENCE: f'reftest_img{grid}.html'
}
ref_template_name = templates[self.template_type]
if _CanvasType.HTML_CANVAS in self.canvas_types:
_render(jinja_env, ref_template_name, params,
@ -727,9 +736,72 @@ class _VariantGrid:
_render(jinja_env, f'testharness_worker{grid}.js', self.params,
f'{output_files.offscreen}.worker.js')
def _generate_cairo_reference_grid(self,
output_dirs: _OutputPaths) -> None:
"""Generate this grid's expected image from Cairo code, if needed.
In order to cut on the number of files generated, the expected image
of all the variants in this grid are packed into a single PNG. The
expected HTML then contains a grid of <img> tags, each showing a portion
of the PNG file."""
if not any(v.params.get('cairo_reference') for v in self.variants):
return
width, height = self._unique_param('size')
cairo_code = ''
# First generate a function producing a Cairo surface with the expected
# image for each variant in the grid. The function is needed to provide
# a scope isolating the variant code from each other.
for idx, variant in enumerate(self._variants):
cairo_ref = variant.params.get('cairo_reference')
if not cairo_ref:
raise InvalidTestDefinitionError(
'When used, "cairo_reference" must be specified for all '
'test variants.')
cairo_code += textwrap.dedent(f'''\
def draw_ref{idx}():
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, {width}, {height})
cr = cairo.Context(surface)
{{}}
return surface
''').format(textwrap.indent(cairo_ref, ' '))
# Write all variant images into the final surface.
surface_width = width * self._grid_width
surface_height = (height *
math.ceil(len(self._variants) / self._grid_width))
cairo_code += textwrap.dedent(f'''\
surface = cairo.ImageSurface(
cairo.FORMAT_ARGB32, {surface_width}, {surface_height})
cr = cairo.Context(surface)
''')
for idx, variant in enumerate(self._variants):
x_pos = int(idx % self._grid_width) * width
y_pos = int(idx / self._grid_width) * height
cairo_code += textwrap.dedent(f'''\
cr.set_source_surface(draw_ref{idx}(), {x_pos}, {y_pos})
cr.paint()
''')
img_filename = f'{self.file_name}.png'
_write_cairo_images(cairo_code, output_dirs.sub_path(img_filename),
self.canvas_types)
self._params['img_reference'] = img_filename
def _generate_cairo_images(self, output_dirs: _OutputPaths) -> None:
"""Generates the pycairo images found in the YAML test definition."""
if any(v.params.get('expected') for v in self._variants):
has_expected = any(v.params.get('expected') for v in self._variants)
has_cairo_reference = any(
v.params.get('cairo_reference') for v in self._variants)
if has_expected and has_cairo_reference:
raise InvalidTestDefinitionError(
'Parameters "expected" and "cairo_reference" can\'t be both '
'used at the same time.')
if has_expected:
if len(self.variants) != 1:
raise InvalidTestDefinitionError(
'Parameter "expected" is not supported for variant grids.')
@ -738,6 +810,8 @@ class _VariantGrid:
'Parameter "expected" is not supported in reference '
'tests.')
self.variants[0].generate_expected_image(output_dirs)
elif has_cairo_reference:
self._generate_cairo_reference_grid(output_dirs)
def generate_test(self, jinja_env: jinja2.Environment,
output_dirs: _OutputPaths) -> None:
@ -747,7 +821,8 @@ class _VariantGrid:
output_files = output_dirs.sub_path(self.file_name)
if self.template_type in (_TemplateType.REFERENCE,
_TemplateType.HTML_REFERENCE):
_TemplateType.HTML_REFERENCE,
_TemplateType.CAIRO_REFERENCE):
self._write_reference_test(jinja_env, output_files)
else:
self._write_testharness_test(jinja_env, output_files)
@ -803,6 +878,9 @@ def _get_variant_grids(test: Mapping[str, Any],
jinja_env: jinja2.Environment) -> List[_VariantGrid]:
base_variant = _Variant.create_with_defaults(test)
grid_width = base_variant.params.get('grid_width', 1)
if not isinstance(grid_width, int):
raise InvalidTestDefinitionError('"grid_width" must be an integer.')
grids = [_VariantGrid([base_variant], grid_width=grid_width)]
for dimension in _get_variant_dimensions(test):
variants = dimension.variants

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<title>Canvas test: {{ name }}</title>
<h1>{{ name }}</h1>
<p>{{ desc }}</p>
{% if notes %}<p>{{ notes }}</p>{% endif %}
<img src="{{ img_reference }}">

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<title>Canvas test: {{ name }}</title>
<h1 style="font-size: 20px;">{{ name }}</h1>
<p>{{ desc }}</p>
{% if notes %}<p>{{ notes }}</p>{% endif %}
<div style="display: grid; grid-gap: 4px;
grid-template-columns: repeat({{ grid_width }}, max-content);
font-size: 13px; text-align: center;">
{% for variant in element_variants %}
<span>
{% for variant_name in variant.grid_variant_names %}
<div>{{ variant_name }}</div>
{% endfor %}
{% set x_pos = ((loop.index0 % grid_width) | int) * variant.size[0] %}
{% set y_pos = ((loop.index0 / grid_width) | int) * variant.size[1] %}
<img src="{{ img_reference }}"
style="outline: 1px solid;
width: {{ variant.size[0] }}px;
height: {{ variant.size[1] }}px;
object-position: {{ -x_pos }}px {{ -y_pos }}px;
object-fit: none;">
</span>
{% endfor %}
</div>