Skip to content

Commit

Permalink
Bug 1921829 [wpt PR 48382] - Add a new canvas WPT template using pyca…
Browse files Browse the repository at this point in the history
…iro-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 <[email protected]>
Commit-Queue: Jean-Philippe Gravel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1369396}

--

wpt-commits: 2af17a84e98cd28940d2882c68ff824d6e98f4b7
wpt-pr: 48382
  • Loading branch information
graveljp authored and moz-wptsync-bot committed Oct 18, 2024
1 parent 66f4afa commit 1694c02
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 17 deletions.
112 changes: 95 additions & 17 deletions testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import enum
import importlib
import itertools
import math
import os
import pathlib
import sys
Expand Down Expand Up @@ -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'


Expand Down Expand Up @@ -452,15 +454,21 @@ def _get_canvas_types(self) -> FrozenSet[_CanvasType]:
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,
Expand All @@ -476,13 +484,10 @@ def finalize_params(self, jinja_env: jinja2.Environment,
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']:
Expand All @@ -503,7 +508,7 @@ def finalize_params(self, jinja_env: jinja2.Environment,
_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':
Expand Down Expand Up @@ -660,7 +665,8 @@ def _get_grid_params(self) -> _MutableTestParams:
'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

Expand Down Expand Up @@ -692,9 +698,12 @@ def _write_reference_test(self, jinja_env: jinja2.Environment,
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,
Expand Down Expand Up @@ -727,9 +736,72 @@ def _write_testharness_test(self, jinja_env: jinja2.Environment,
_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.')
Expand All @@ -738,6 +810,8 @@ def _generate_cairo_images(self, output_dirs: _OutputPaths) -> None:
'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:
Expand All @@ -747,7 +821,8 @@ def generate_test(self, jinja_env: jinja2.Environment,
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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }}">
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit 1694c02

Please sign in to comment.