Rename gentestutilsunion.py to gentest.py
gentest.py was nothing more than a wrapper around gentestutilsunion.py.
This CL deletes the former and renames the latter (keeping its content
otherwise unchanged).
Bug: 40207206
Change-Id: I58efcad63267ec459a66db9b59e737c130c2fcd7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6207155
Reviewed-by: Andres Ricardo Perez <andresrperez@chromium.org>
Commit-Queue: Jean-Philippe Gravel <jpgravel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1422936}
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentest.py b/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentest.py
index 8fb46b4..fb0f96d 100644
--- a/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentest.py
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentest.py
@@ -1,4 +1,16 @@
-"""Script invoking the old and the new Canvas test generator."""
+"""Generates Canvas tests from YAML file definitions."""
+# Current code status:
+#
+# This was originally written by Philip Taylor for use at
+# http://philip.html5.org/tests/canvas/suite/tests/
+#
+# It has been adapted for use with the Web Platform Test Suite suite at
+# https://github.com/web-platform-tests/wpt/
+#
+# The original version had a number of now-removed features (multiple versions
+# of each test case of varying verbosity, Mozilla mochitests, semi-automated
+# test harness). It also had a different directory structure.
+
# To use this script:
# -make a python virtual environment somewhere (it doesn't matter where)
# python3 -m venv venv
@@ -9,6 +21,1149 @@
# -change to the directory with this script and run it
# python3 gentest.py
-import gentestutilsunion
+# To update or add test cases:
+#
+# * Modify the tests*.yaml files.
+# - 'name' is an arbitrary hierarchical name to help categorise tests.
+# - 'desc' is a rough description of what behaviour the test aims to test.
+# - 'code' is JavaScript code to execute, with some special commands starting
+# with '@'.
+# - 'expected' is what the final canvas output should be: a string 'green' or
+# 'clear' (100x50 images in both cases), or a string 'size 100 50' (or any
+# other size) followed by Python code using Pycairo to generate the image.
+#
+# * Run "./build.sh".
+# This requires a few Python modules which might not be ubiquitous.
+# It will usually emit some warnings, which ideally should be fixed but can
+# generally be safely ignored.
+#
+# * Test the tests, add new ones to Git, remove deleted ones from Git, etc.
-gentestutilsunion.generate_test_files('name2dir.yaml')
+from typing import Any, Callable, Container, DefaultDict, FrozenSet
+from typing import List, Mapping, MutableMapping, Set, Tuple, Union
+
+import re
+import collections
+import copy
+import dataclasses
+import enum
+import importlib
+import math
+import os
+import pathlib
+import sys
+import textwrap
+
+import jinja2
+
+try:
+ import cairocffi as cairo # type: ignore
+except ImportError:
+ import cairo
+
+try:
+ # Compatible and lots faster.
+ import syck as yaml # type: ignore
+except ImportError:
+ import yaml
+
+
+class Error(Exception):
+ """Base class for all exceptions raised by this module"""
+
+
+class InvalidTestDefinitionError(Error):
+ """Raised on invalid test definition."""
+
+
+def _double_quote_escape(string: str) -> str:
+ return string.replace('\\', '\\\\').replace('"', '\\"')
+
+
+def _escape_js(string: str) -> str:
+ string = _double_quote_escape(string)
+ # Kind of an ugly hack, for nicer failure-message output.
+ string = re.sub(r'\[(\w+)\]', r'[\\""+(\1)+"\\"]', string)
+ return string
+
+
+def _expand_nonfinite(method: str, argstr: str, tail: str) -> str:
+ """
+ >>> print _expand_nonfinite('f', '<0 a>, <0 b>', ';')
+ f(a, 0);
+ f(0, b);
+ f(a, b);
+ >>> print _expand_nonfinite('f', '<0 a>, <0 b c>, <0 d>', ';')
+ f(a, 0, 0);
+ f(0, b, 0);
+ f(0, c, 0);
+ f(0, 0, d);
+ f(a, b, 0);
+ f(a, b, d);
+ f(a, 0, d);
+ f(0, b, d);
+ """
+ # argstr is "<valid-1 invalid1-1 invalid2-1 ...>, ..." (where usually
+ # 'invalid' is Infinity/-Infinity/NaN).
+ args = []
+ for arg in argstr.split(', '):
+ match = re.match('<(.*)>', arg)
+ if match is None:
+ raise InvalidTestDefinitionError(
+ f'Expected arg to match format "<(.*)>", but was: {arg}')
+ a = match.group(1)
+ args.append(a.split(' '))
+ calls = []
+ # Start with the valid argument list.
+ call = [args[j][0] for j in range(len(args))]
+ # For each argument alone, try setting it to all its invalid values:
+ for i, arg in enumerate(args):
+ for a in arg[1:]:
+ c2 = call[:]
+ c2[i] = a
+ calls.append(c2)
+ # For all combinations of >= 2 arguments, try setting them to their
+ # first invalid values. (Don't do all invalid values, because the
+ # number of combinations explodes.)
+ def f(c: List[str], start: int, depth: int) -> None:
+ for i in range(start, len(args)):
+ if len(args[i]) > 1:
+ a = args[i][1]
+ c2 = c[:]
+ c2[i] = a
+ if depth > 0:
+ calls.append(c2)
+ f(c2, i + 1, depth + 1)
+
+ f(call, 0, 0)
+
+ str_calls = (', '.join(c) for c in calls)
+ return '\n'.join(f'{method}({params}){tail}' for params in str_calls)
+
+
+def _get_test_sub_dir(name: str, name_to_sub_dir: Mapping[str, str]) -> str:
+ for prefix in sorted(name_to_sub_dir.keys(), key=len, reverse=True):
+ if name.startswith(prefix):
+ return name_to_sub_dir[prefix]
+ raise InvalidTestDefinitionError(
+ f'Test "{name}" has no defined target directory mapping')
+
+
+def _remove_extra_newlines(text: str) -> str:
+ """Remove newlines if a backslash is found at end of line."""
+ # Lines ending with '\' gets their newline character removed.
+ text = re.sub(r'\\\n', '', text, flags=re.MULTILINE | re.DOTALL)
+
+ # Lines ending with '\-' gets their newline and any leading white spaces on
+ # the following line removed.
+ text = re.sub(r'\\-\n\s*', '', text, flags=re.MULTILINE | re.DOTALL)
+ return text
+
+
+def _expand_test_code(code: str) -> str:
+ code = _remove_extra_newlines(code)
+
+ code = re.sub(r' @moz-todo', '', code)
+
+ code = re.sub(r'@moz-UniversalBrowserRead;', '', code)
+
+ code = re.sub(r'@nonfinite ([^(]+)\(([^)]+)\)(.*)', lambda m:
+ _expand_nonfinite(m.group(1), m.group(2), m.group(3)),
+ code) # Must come before '@assert throws'.
+
+ code = re.sub(r'@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);',
+ r'_assertPixel(canvas, \1, \2);', code)
+
+ code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);',
+ r'_assertPixelApprox(canvas, \1, \2, 2);', code)
+
+ code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+/- (\d+);',
+ r'_assertPixelApprox(canvas, \1, \2, \3);', code)
+
+ code = re.sub(r'@assert throws (\S+_ERR) (.*?);$',
+ r'assert_throws_dom("\1", function() { \2; });', code,
+ flags=re.MULTILINE | re.DOTALL)
+
+ code = re.sub(r'@assert throws (\S+Error) (.*?);$',
+ r'assert_throws_js(\1, function() { \2; });', code,
+ flags=re.MULTILINE | re.DOTALL)
+
+ code = re.sub(
+ r'@assert (.*) === (.*);', lambda m:
+ (f'_assertSame({m.group(1)}, {m.group(2)}, '
+ f'"{_escape_js(m.group(1))}", "{_escape_js(m.group(2))}");'), code)
+
+ code = re.sub(
+ r'@assert (.*) !== (.*);', lambda m:
+ (f'_assertDifferent({m.group(1)}, {m.group(2)}, '
+ f'"{_escape_js(m.group(1))}", "{_escape_js(m.group(2))}");'), code)
+
+ code = re.sub(
+ r'@assert (.*) =~ (.*);',
+ lambda m: f'assert_regexp_match({m.group(1)}, {m.group(2)});', code)
+
+ code = re.sub(
+ r'@assert (.*);',
+ lambda m: f'_assert({m.group(1)}, "{_escape_js(m.group(1))}");', code)
+
+ assert '@' not in code
+
+ return code
+
+
+_TestParams = Mapping[str, Any]
+_MutableTestParams = MutableMapping[str, Any]
+
+
+class _CanvasType(str, enum.Enum):
+ HTML_CANVAS = 'HtmlCanvas'
+ OFFSCREEN_CANVAS = 'OffscreenCanvas'
+ WORKER = 'Worker'
+
+
+class _TemplateType(str, enum.Enum):
+ REFERENCE = 'reference'
+ HTML_REFERENCE = 'html_reference'
+ CAIRO_REFERENCE = 'cairo_reference'
+ TESTHARNESS = 'testharness'
+
+
+class MutableDictLoader(jinja2.BaseLoader):
+ """Loads Jinja templates from a `dict` that can be updated.
+
+ This is essentially a version of `jinja2.DictLoader` whose content can be
+ changed. `jinja2.DictLoader` accepts a `dict` at construction time and that
+ `dict` cannot be changed. The templates served by `MutableDictLoader` on the
+ other hand can be updated by calling `set_templates(new_templates)`. This is
+ needed because we reuse the environment to render different tests and
+ variants, each of which will have different templates.
+ """
+
+ def __init__(self) -> None:
+ self._templates = dict() # type: Mapping[str, Any]
+
+ def set_templates(self, new_templates: Mapping[str, Any]) -> None:
+ """Changes the dict from which templates are loaded."""
+ self._templates = new_templates
+
+ def get_source(
+ self, environment: jinja2.Environment, template: str
+ ) -> Tuple[str, str, Callable[[], bool]]:
+ """Loads a template from the current template dict."""
+ del environment # Unused.
+ source = self._templates.get(template)
+ if source is None:
+ raise jinja2.TemplateNotFound(template)
+ if not isinstance(source, str):
+ raise InvalidTestDefinitionError(
+ f'Param "{template}" must be an str to be usable as Jinja '
+ 'template.')
+ return source, template, lambda: source == self._templates.get(template)
+
+
+class TemplateLoaderActivator:
+ """Helper class used to set a given params dict in a MutableDictLoader.
+
+ Jinja requires custom loaders to be registered in the environment and thus,
+ we can't dynamically change them. We would need this to allow different test
+ variants to have different templates. Using a `TemplateLoaderActivator`,
+ the code can "activate" the templates for a given variant before rendering
+ strings for that variant. For instance:
+
+ loader = MutableDictLoader()
+ jinja_env = jinja2.Environment(loader=[loader])
+
+ templates1 = {'macros': '{% macro foo() %}foo{% endmacro %}'}
+ activator1 = TemplateLoaderActivator(loader, templates1)
+
+ templates2 = {'macros': '{% macro foo() %}bar{% endmacro %}'}
+ activator2 = TemplateLoaderActivator(loader, templates2)
+
+ main_template = '''
+ {% import 'macros' as t %}
+ {{ t.foo() }}
+ '''
+
+ # Render `main_template`, loading 'macros' from `templates1.
+ activator1.activate()
+ jinja_env.from_string(main_template).render(params1))
+
+ # Render `main_template`, loading 'macros' from `templates2.
+ activator2.activate()
+ jinja_env.from_string(main_template).render(params2))
+
+ """
+
+ def __init__(self, loader: MutableDictLoader, params: _TestParams) -> None:
+ self._loader = loader
+ self._params = params
+
+ def activate(self):
+ self._loader.set_templates(self._params)
+
+
+class _LazyRenderedStr(collections.UserString):
+ """A custom str type that renders it's content with Jinja when accessed.
+
+ This is an str-like type, storing a Jinja template, but returning the
+ rendered version of that template when the string is accessed. The rendered
+ result is cached and returned on subsequent accesses.
+
+ This allows template parameters to be themselves templates. Template
+ parameters can then refer to each other and they'll be rendered in the right
+ order, in reverse order of access.
+
+ For instance:
+
+ params = {}
+ make_lazy = lambda value: _LazyRenderedStr(
+ jinja_env, loader_activator, params, value)
+
+ params.update({
+ 'expected_value': make_lazy('rgba({{ color | join(", ") }})'),
+ 'color': [0, 255, 0, make_lazy('{{ alpha }}')],
+ 'alpha': 0.5,
+ })
+
+ main_template = 'assert value == "{{ expected_value }}"'
+ result = jinja_env.from_string(main_template).render(params)
+
+ In this example, upon rendering `main_template`, Jinja will first read
+ `expected_value`, which reads `color`, which reads `alpha`. These will be
+ rendered in reverse order, with `color` resolving to `[0, 255, 0, '0.5']`,
+ `expected_value` resolving to 'rgba(0, 255, 0, 0.5)' and the final render
+ resolving to: 'assert value == "rgba(0, 255, 0, 0.5)"'
+ """
+
+ def __init__(self, jinja_env: jinja2.Environment,
+ loader_activator: TemplateLoaderActivator,
+ params: _TestParams, value: str):
+ # Don't call `super().__init__`, because we want to override `self.data`
+ # to be a property instead of a member variable.
+ # pylint: disable=super-init-not-called
+ self._jinja_env = jinja_env
+ self._loader_activator = loader_activator
+ self._params = params
+ self._value = value
+ self._rendered = None
+
+ @property
+ def data(self):
+ """Property returning the content of the `UserString`.
+
+ This `_LazyRenderedStr` will be rendered on the first access. The
+ rendered result is cached and returned directly on subsequent
+ accesses."""
+ if self._rendered is None:
+ self._loader_activator.activate()
+ self._rendered = (
+ self._jinja_env.from_string(self._value).render(self._params))
+ return self._rendered
+
+ @property
+ def __class__(self):
+ """Makes `UserString` return any newly created strings as `str` objects.
+
+ `UserString` functions returning a new string (e.g. `strip()`,
+ `lower()`, etc.) normally return a string of the same type as the input
+ `UserString`. It does do by using `__class__` to know the actual user
+ string type. In our case, the result of these operations will always
+ return a plain `str`, since any templating will have been rendered when
+ reading the input string via `self.data`."""
+ return str
+
+
+def _make_lazy_rendered(jinja_env: jinja2.Environment,
+ loader_activator: TemplateLoaderActivator,
+ params: _TestParams,
+ value: Any) -> Any:
+ """Recursively converts `value` to a _LazyRenderedStr.
+
+ If `value` is a data structure, this function recurses into that structure
+ and converts leaf objects. Any `str` found containing Jinja tags are
+ converted to _LazyRenderedStr.
+ """
+ if isinstance(value, str) and ('{{' in value or '{%' in value):
+ return _LazyRenderedStr(jinja_env, loader_activator, params, value)
+ if isinstance(value, list):
+ return [_make_lazy_rendered(jinja_env, loader_activator, params, v)
+ for v in value]
+ if isinstance(value, tuple):
+ return tuple(_make_lazy_rendered(jinja_env, loader_activator, params, v)
+ for v in value)
+ if isinstance(value, dict):
+ return {k: _make_lazy_rendered(jinja_env, loader_activator, params, v)
+ for k, v in value.items()}
+ return value
+
+
+def _ensure_rendered(value: Any) -> Any:
+ """Recursively makes sure that all _LazyRenderedStr in `value` are rendered.
+
+ If `value` is a data structure, this function recurses into that structure
+ and renders any _LazyRenderedStr found."""
+ if isinstance(value, _LazyRenderedStr):
+ return str(value)
+ if isinstance(value, list):
+ return [_ensure_rendered(v) for v in value]
+ if isinstance(value, tuple):
+ return tuple(_ensure_rendered(v) for v in value)
+ if isinstance(value, dict):
+ return {k: _ensure_rendered(v) for k, v in value.items()}
+ return value
+
+
+@dataclasses.dataclass
+class _OutputPaths:
+ element: pathlib.Path
+ offscreen: pathlib.Path
+
+ def sub_path(self, sub_dir: str):
+ """Create a new _OutputPaths that is a subpath of this _OutputPath."""
+ return _OutputPaths(
+ element=self.element / _ensure_rendered(sub_dir),
+ offscreen=self.offscreen / _ensure_rendered(sub_dir))
+
+ def path_for_canvas_type(self, canvas_type: _CanvasType) -> pathlib.Path:
+ return (self.element if canvas_type == _CanvasType.HTML_CANVAS
+ else self.offscreen)
+
+ def mkdir(self) -> None:
+ """Creates element and offscreen directories, if they don't exist."""
+ self.element.mkdir(parents=True, exist_ok=True)
+ self.offscreen.mkdir(parents=True, exist_ok=True)
+
+
+def _validate_test(test: _TestParams):
+ if test.get('expected', '') == 'green' and re.search(
+ r'@assert pixel .* 0,0,0,0;', test['code']):
+ print(f'Probable incorrect pixel test in {test["name"]}')
+
+ if 'size' in test and (not isinstance(test['size'], tuple)
+ or len(test['size']) != 2):
+ raise InvalidTestDefinitionError(
+ f'Invalid canvas size "{test["size"]}" in test {test["name"]}. '
+ 'Expected an array with two numbers.')
+
+ if test['template_type'] == _TemplateType.TESTHARNESS:
+ valid_test_types = {'sync', 'async', 'promise'}
+ else:
+ valid_test_types = {'promise'}
+
+ test_type = test.get('test_type')
+ if test_type is not None and test_type not in valid_test_types:
+ raise InvalidTestDefinitionError(
+ f'Invalid test_type: {test_type}. '
+ f'Valid values are: {valid_test_types}.')
+
+
+def _render(jinja_env: jinja2.Environment,
+ template_name: str,
+ params: _TestParams, output_file_name: str):
+ template = jinja_env.get_template(template_name)
+ file_content = template.render(params)
+ pathlib.Path(output_file_name).write_text(file_content, 'utf-8')
+
+
+def _write_cairo_images(pycairo_code: str, output_file: pathlib.Path) -> None:
+ """Creates a png from pycairo code and write it to `output_file`."""
+ full_code = (f'{pycairo_code}\n'
+ f'surface.write_to_png("{output_file}")\n')
+ eval(compile(full_code, '<string>', 'exec'), {
+ 'cairo': cairo,
+ 'math': math,
+ })
+
+
+class _Variant():
+
+ def __init__(self, params: _MutableTestParams) -> None:
+ # Raw parameters, as specified in YAML, defining this test variant.
+ self._params = params # type: _MutableTestParams
+ # Parameters rendered for each enabled canvas types.
+ self._canvas_type_params = {
+ } # type: MutableMapping[_CanvasType, _MutableTestParams]
+
+ @property
+ def params(self) -> _MutableTestParams:
+ """Returns this variant's raw param dict, as it's defined in YAML."""
+ return self._params
+
+ @property
+ def canvas_type_params(self) -> MutableMapping[_CanvasType,
+ _MutableTestParams]:
+ """Returns this variant's param dict for different canvas types."""
+ return self._canvas_type_params
+
+ @staticmethod
+ def create_with_defaults(test: _TestParams) -> '_Variant':
+ """Create a _Variant from the specified params.
+
+ Default values are added for certain parameters, if missing."""
+ params = {
+ 'enabled': 'true',
+ 'desc': '',
+ 'size': (100, 50),
+ # Test name, which ultimately is used as filename. File variant
+ # dimension names (i.e. the 'file_variant_names' property below) are
+ # appended to this to produce unique filenames.
+ 'name': '',
+ # List holding the the file variant dimension names.
+ 'file_variant_names': [],
+ # List of this variant grid dimension names. This uniquely
+ # identifies a single variant in a variant grid file.
+ 'grid_variant_names': [],
+ # List of this variant dimension names, including both file and grid
+ # dimensions.
+ 'variant_names': [],
+ # Same as `file_variant_names`, but concatenated into a single
+ # string. This is a useful to easily identify a variant file.
+ 'file_variant_name': '',
+ # Same as `grid_variant_names`, but concatenated into a single
+ # string. This is a useful to easily identify a variant in a grid.
+ 'grid_variant_name': '',
+ # Same as `variant_names`, but concatenated into a single string.
+ # This is a useful shorthand for tests having a single variant
+ # dimension.
+ 'variant_name': '',
+ 'images': [],
+ 'svgimages': [],
+ 'fonts': [],
+ }
+ params.update(test)
+ if 'variants' in params:
+ del params['variants']
+ return _Variant(params)
+
+ def merge_params(self, params: _TestParams) -> '_Variant':
+ """Returns a new `_Variant` that merges `self.params` and `params`."""
+ new_params = copy.deepcopy(self._params)
+ new_params.update(params)
+ return _Variant(new_params)
+
+ def _add_variant_name(self, name: str) -> None:
+ self._params['variant_name'] += (
+ ('.' if self.params['variant_name'] else '') + name)
+ self._params['variant_names'] += [name]
+
+ def with_grid_variant_name(self, name: str) -> '_Variant':
+ """Addend a variant name to include in the grid element label."""
+ self._add_variant_name(name)
+ self._params['grid_variant_name'] += (
+ ('.' if self.params['grid_variant_name'] else '') + name)
+ self._params['grid_variant_names'] += [name]
+ return self
+
+ def with_file_variant_name(self, name: str) -> '_Variant':
+ """Addend a variant name to include in the generated file name."""
+ self._add_variant_name(name)
+ self._params['file_variant_name'] += (
+ ('.' if self.params['file_variant_name'] else '') + name)
+ self._params['file_variant_names'] += [name]
+ if self.params.get('append_variants_to_name', True):
+ self._params['name'] += '.' + name
+ return self
+
+ def _get_file_name(self) -> str:
+ file_name = self.params['name']
+
+ if 'manual' in self.params:
+ file_name += '-manual'
+
+ return file_name
+
+ def _get_canvas_types(self) -> FrozenSet[_CanvasType]:
+ canvas_types = self.params.get('canvas_types', _CanvasType)
+ invalid_types = {
+ type
+ for type in canvas_types if type not in list(_CanvasType)
+ }
+ if invalid_types:
+ raise InvalidTestDefinitionError(
+ f'Invalid canvas_types: {list(invalid_types)}. '
+ f'Accepted values are: {[t.value for t in _CanvasType]}')
+ return frozenset(_CanvasType(t) for t in canvas_types)
+
+ def _get_template_type(self) -> _TemplateType:
+ 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, 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,
+ variant_id: int,
+ params_template_loader: MutableDictLoader) -> None:
+ """Finalize this variant by adding computed param fields."""
+ self._params['id'] = variant_id
+ self._params['file_name'] = self._get_file_name()
+ self._params['canvas_types'] = self._get_canvas_types()
+ self._params['template_type'] = self._get_template_type()
+
+ if isinstance(self._params['size'], list):
+ self._params['size'] = tuple(self._params['size'])
+
+ loader_activator = TemplateLoaderActivator(params_template_loader,
+ self._params)
+ for canvas_type in self.params['canvas_types']:
+ params = {'canvas_type': canvas_type}
+ params.update(
+ {k: _make_lazy_rendered(jinja_env, loader_activator, params, v)
+ for k, v in self._params.items()})
+ self._canvas_type_params[canvas_type] = params
+
+ for name in ('code', 'reference', 'html_reference',
+ 'cairo_reference'):
+ param = params.get(name)
+ if param is not None:
+ params[name] = _expand_test_code(_ensure_rendered(param))
+
+ _validate_test(self._params)
+
+ def generate_expected_image(self, output_dirs: _OutputPaths) -> None:
+ """Creates an expected image using Cairo and save filename in params."""
+ # Expected images are only needed for HTML canvas tests.
+ params = self._canvas_type_params.get(_CanvasType.HTML_CANVAS)
+ if not params:
+ return
+
+ expected = _ensure_rendered(params['expected'])
+
+ if expected == 'green':
+ params['expected_img'] = '/images/green-100x50.png'
+ return
+ if expected == 'clear':
+ params['expected_img'] = '/images/clear-100x50.png'
+ return
+ expected = re.sub(
+ r'^size (\d+) (\d+)',
+ r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)'
+ r'\ncr = cairo.Context(surface)', expected)
+
+ img_filename = f'{params["name"]}.png'
+ _write_cairo_images(expected, output_dirs.element / img_filename)
+ params['expected_img'] = img_filename
+
+
+class _VariantGrid:
+
+ def __init__(self, variants: List[_Variant], grid_width: int) -> None:
+ self._variants = variants
+ self._grid_width = grid_width
+
+ # Parameters rendered for each enabled canvas types.
+ self._canvas_type_params = {
+ } # type: Mapping[_CanvasType, _MutableTestParams]
+ self._enabled = None
+ self._file_name = None
+ self._canvas_types = None
+ self._template_type = None
+
+ @property
+ def variants(self) -> List[_Variant]:
+ """Read only getter for the list of variant in this grid."""
+ return self._variants
+
+ @property
+ def enabled(self) -> bool:
+ """File name to which this grid will be written."""
+ if self._enabled is None:
+ enabled_str = self._unique_param(_CanvasType, 'enabled')
+ self._enabled = (enabled_str.strip().lower() == 'true')
+ return self._enabled
+
+ @property
+ def file_name(self) -> str:
+ """File name to which this grid will be written."""
+ if self._file_name is None:
+ self._file_name = self._unique_param(_CanvasType, 'file_name')
+ return self._file_name
+
+ @property
+ def canvas_types(self) -> FrozenSet[_CanvasType]:
+ """Returns the set of all _CanvasType used by this grid's variants."""
+ if self._canvas_types is None:
+ self._canvas_types = self._param_set(_CanvasType, 'canvas_types')
+ return self._canvas_types
+
+ @property
+ def template_type(self) -> _TemplateType:
+ """Returns the type of Jinja template needed to render this grid."""
+ if self._template_type is None:
+ self._template_type = self._unique_param(_CanvasType,
+ 'template_type')
+ return self._template_type
+
+ def finalize(self, jinja_env: jinja2.Environment,
+ params_template_loader: MutableDictLoader):
+ """Finalize this grid's variants, adding computed params fields."""
+ for variant_id, variant in enumerate(self.variants):
+ variant.finalize_params(jinja_env, variant_id,
+ params_template_loader)
+
+ if len(self.variants) == 1:
+ self._canvas_type_params = self.variants[0].canvas_type_params
+ else:
+ self._canvas_type_params = self._get_grid_params()
+
+ def add_dimension(self, variants: Mapping[str,
+ _TestParams]) -> '_VariantGrid':
+ """Adds a variant dimension to this variant grid.
+
+ If the grid currently has N variants, adding a dimension with M variants
+ results in a grid containing N*M variants. Of course, we can't display
+ more than 2 dimensions on a 2D screen, so adding dimensions beyond 2
+ repeats all previous dimensions down vertically, with the grid width
+ set to the number of variants of the first dimension (unless overridden
+ by setting `grid_width`). For instance, a 3D variant space with
+ dimensions 3 x 2 x 2 will result in this layout:
+ 000 100 200
+ 010 110 210
+
+ 001 101 201
+ 011 111 211
+ """
+ new_variants = [
+ old_variant.merge_params(params or {}).with_grid_variant_name(name)
+ for name, params in variants.items()
+ for old_variant in self.variants
+ ]
+ # The first dimension dictates the grid-width, unless it was specified
+ # beforehand via the test params.
+ new_grid_width = (self._grid_width
+ if self._grid_width > 1 else len(variants))
+ return _VariantGrid(variants=new_variants, grid_width=new_grid_width)
+
+ def merge_params(self, name: str, params: _TestParams) -> '_VariantGrid':
+ """Merges the specified `params` into every variant of this grid."""
+ return _VariantGrid(variants=[
+ variant.merge_params(params).with_file_variant_name(name)
+ for variant in self.variants
+ ],
+ grid_width=self._grid_width)
+
+ def _variants_for_canvas_type(
+ self, canvas_type: _CanvasType) -> List[_TestParams]:
+ """Returns the variants of this grid enabled for `canvas_type`."""
+ return [
+ v.canvas_type_params[canvas_type]
+ for v in self.variants
+ if canvas_type in v.canvas_type_params
+ ]
+
+ def _unique_param(
+ self, canvas_types: Container[_CanvasType], name: str) -> Any:
+ """Returns the value of the `name` param for this grid.
+
+ All the variants for all canvas types in `canvas_types` of this grid
+ must agree on the same value for this parameter, or else an exception is
+ thrown."""
+ values = {_ensure_rendered(params.get(name))
+ for variant in self.variants
+ for type, params in variant.canvas_type_params.items()
+ if type in canvas_types}
+ if len(values) != 1:
+ raise InvalidTestDefinitionError(
+ 'All variants in a variant grid must use the same value '
+ f'for property "{name}". Got these values: {values}. '
+ 'Consider specifying the property outside of grid '
+ 'variants dimensions (in the base test definition or in a '
+ 'file variant dimension)')
+ return values.pop()
+
+ def _param_set(self, canvas_types: Container[_CanvasType], name: str):
+ """Returns the set of all values this grid has for the `name` param.
+
+ The `name` parameter of each variant is expected to be a sequence. These
+ are all accumulated in a set and returned. The values are accumulated
+ across all canvas types in `canvas_types`."""
+ return frozenset(sum([list(_ensure_rendered(params.get(name, [])))
+ for v in self.variants
+ for type, params in v.canvas_type_params.items()
+ if type in canvas_types],
+ []))
+
+ def _get_grid_params(self) -> Mapping[_CanvasType, _MutableTestParams]:
+ """Returns the params dict needed to render this grid with Jinja."""
+ grid_params = {}
+ for canvas_type in self.canvas_types:
+ params = grid_params[canvas_type] = {}
+ params.update({
+ 'variants': self._variants_for_canvas_type(canvas_type),
+ 'grid_width': self._grid_width,
+ 'name': self._unique_param([canvas_type], 'name'),
+ 'test_type': self._unique_param([canvas_type], 'test_type'),
+ 'fuzzy': self._unique_param([canvas_type], 'fuzzy'),
+ 'timeout': self._unique_param([canvas_type], 'timeout'),
+ 'notes': self._unique_param([canvas_type], 'notes'),
+ 'images': self._param_set([canvas_type], 'images'),
+ 'svgimages': self._param_set([canvas_type], 'svgimages'),
+ 'fonts': self._param_set([canvas_type], 'fonts'),
+ })
+ if self.template_type in (_TemplateType.REFERENCE,
+ _TemplateType.HTML_REFERENCE,
+ _TemplateType.CAIRO_REFERENCE):
+ params['desc'] = self._unique_param([canvas_type], 'desc')
+ return grid_params
+
+ def _write_reference_test(self, jinja_env: jinja2.Environment,
+ output_files: _OutputPaths):
+ grid = '_grid' if len(self.variants) > 1 else ''
+
+ # If variants don't all use the same offscreen and worker canvas types,
+ # the offscreen and worker grids won't be identical. The worker test
+ # therefore can't reuse the offscreen reference file.
+ offscreen_types = {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER}
+ needs_worker_reference = len({
+ variant.params['canvas_types'] & offscreen_types
+ for variant in self.variants
+ }) != 1
+
+ test_templates = {
+ _CanvasType.HTML_CANVAS: f'reftest_element{grid}.html',
+ _CanvasType.OFFSCREEN_CANVAS: f'reftest_offscreen{grid}.html',
+ _CanvasType.WORKER: f'reftest_worker{grid}.html',
+ }
+ ref_templates = {
+ _TemplateType.REFERENCE: f'reftest_element{grid}.html',
+ _TemplateType.HTML_REFERENCE: f'reftest{grid}.html',
+ _TemplateType.CAIRO_REFERENCE: f'reftest_img{grid}.html'
+ }
+ test_output_paths = {
+ _CanvasType.HTML_CANVAS: f'{output_files.element}.html',
+ _CanvasType.OFFSCREEN_CANVAS: f'{output_files.offscreen}.html',
+ _CanvasType.WORKER: f'{output_files.offscreen}.w.html',
+ }
+ ref_output_paths = {
+ _CanvasType.HTML_CANVAS: f'{output_files.element}-expected.html',
+ _CanvasType.OFFSCREEN_CANVAS:
+ f'{output_files.offscreen}-expected.html',
+ _CanvasType.WORKER: (
+ f'{output_files.offscreen}.w-expected.html'
+ if needs_worker_reference
+ else f'{output_files.offscreen}-expected.html'),
+ }
+ for canvas_type, params in self._canvas_type_params.items():
+ params['reference_file'] = pathlib.Path(
+ ref_output_paths[canvas_type]).name
+ _render(jinja_env, test_templates[canvas_type], params,
+ test_output_paths[canvas_type])
+
+ if canvas_type != _CanvasType.WORKER or needs_worker_reference:
+ params['is_test_reference'] = True
+ _render(jinja_env, ref_templates[self.template_type], params,
+ ref_output_paths[canvas_type])
+
+ def _write_testharness_test(self, jinja_env: jinja2.Environment,
+ output_files: _OutputPaths):
+ grid = '_grid' if len(self.variants) > 1 else ''
+
+ templates = {
+ _CanvasType.HTML_CANVAS: f'testharness_element{grid}.html',
+ _CanvasType.OFFSCREEN_CANVAS: f'testharness_offscreen{grid}.html',
+ _CanvasType.WORKER: f'testharness_worker{grid}.js',
+ }
+ test_output_files = {
+ _CanvasType.HTML_CANVAS: f'{output_files.element}.html',
+ _CanvasType.OFFSCREEN_CANVAS: f'{output_files.offscreen}.html',
+ _CanvasType.WORKER: f'{output_files.offscreen}.worker.js',
+ }
+
+ # Create test cases for canvas, offscreencanvas and worker.
+ for canvas_type, params in self._canvas_type_params.items():
+ _render(jinja_env, templates[canvas_type], params,
+ test_output_files[canvas_type])
+
+ def _generate_cairo_reference_grid(self,
+ canvas_type: _CanvasType,
+ 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.canvas_type_params[canvas_type].get('cairo_reference')
+ for v in self.variants):
+ return
+
+ width, height = self._unique_param([canvas_type], '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.canvas_type_params[canvas_type].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'
+ output_dir = output_dirs.path_for_canvas_type(canvas_type)
+ _write_cairo_images(cairo_code, output_dir / img_filename)
+ self._canvas_type_params[canvas_type]['img_reference'] = img_filename
+
+ def _generate_cairo_images(self, output_dirs: _OutputPaths) -> None:
+ """Generates the pycairo images found in the YAML test definition."""
+ # 'expected:' is only used for HTML_CANVAS tests.
+ has_expected = any(v.canvas_type_params
+ .get(_CanvasType.HTML_CANVAS, {})
+ .get('expected') for v in self._variants)
+ has_cairo_reference = any(
+ params.get('cairo_reference')
+ for v in self._variants
+ for params in v.canvas_type_params.values())
+
+ 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.')
+ if self.template_type != _TemplateType.TESTHARNESS:
+ raise InvalidTestDefinitionError(
+ 'Parameter "expected" is not supported in reference '
+ 'tests.')
+ self.variants[0].generate_expected_image(output_dirs)
+ elif has_cairo_reference:
+ for canvas_type in _CanvasType:
+ self._generate_cairo_reference_grid(canvas_type, output_dirs)
+
+ def generate_test(self, jinja_env: jinja2.Environment,
+ output_dirs: _OutputPaths) -> None:
+ """Generate the test files to the specified output dirs."""
+ self._generate_cairo_images(output_dirs)
+
+ output_files = output_dirs.sub_path(self.file_name)
+
+ if self.template_type in (_TemplateType.REFERENCE,
+ _TemplateType.HTML_REFERENCE,
+ _TemplateType.CAIRO_REFERENCE):
+ self._write_reference_test(jinja_env, output_files)
+ else:
+ self._write_testharness_test(jinja_env, output_files)
+
+
+class _VariantLayout(str, enum.Enum):
+ SINGLE_FILE = 'single_file'
+ MULTI_FILES = 'multi_files'
+
+
+@dataclasses.dataclass
+class _VariantDimension:
+ variants: Mapping[str, _TestParams]
+ layout: _VariantLayout
+
+
+def _get_variant_dimensions(params: _TestParams) -> List[_VariantDimension]:
+ variants = params.get('variants', [])
+ if not isinstance(variants, list):
+ raise InvalidTestDefinitionError(
+ textwrap.dedent("""
+ Variants must be specified as a list of variant dimensions, e.g.:
+ variants:
+ - dimension1-variant1:
+ param: ...
+ dimension1-variant2:
+ param: ...
+ - dimension2-variant1:
+ param: ...
+ dimension2-variant2:
+ param: ..."""))
+
+ variants_layout = params.get('variants_layout',
+ [_VariantLayout.MULTI_FILES] * len(variants))
+ if len(variants) != len(variants_layout):
+ raise InvalidTestDefinitionError(
+ 'variants and variants_layout must be lists of the same size')
+ invalid_layouts = [
+ l for l in variants_layout if l not in list(_VariantLayout)
+ ]
+ if invalid_layouts:
+ raise InvalidTestDefinitionError('Invalid variants_layout: ' +
+ ', '.join(invalid_layouts) +
+ '. Valid layouts are: ' +
+ ', '.join(_VariantLayout))
+
+ return [
+ _VariantDimension(z[0], z[1]) for z in zip(variants, variants_layout)
+ ]
+
+
+def _get_variant_grids(
+ test: _TestParams,
+ jinja_env: jinja2.Environment,
+ params_template_loader: MutableDictLoader
+) -> 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
+ if dimension.layout == _VariantLayout.MULTI_FILES:
+ grids = [
+ grid.merge_params(name, params)
+ for name, params in variants.items() for grid in grids
+ ]
+ else:
+ grids = [grid.add_dimension(variants) for grid in grids]
+
+ for grid in grids:
+ grid.finalize(jinja_env, params_template_loader)
+
+ return grids
+
+
+def _check_uniqueness(tested: DefaultDict[str, Set[_CanvasType]], name: str,
+ canvas_types: FrozenSet[_CanvasType]) -> None:
+ already_tested = tested[name].intersection(canvas_types)
+ if already_tested:
+ raise InvalidTestDefinitionError(
+ f'Test {name} is defined twice for types {already_tested}')
+ tested[name].update(canvas_types)
+
+
+def _indent_filter(s: str, width: Union[int, str] = 4,
+ first: bool = False, blank: bool = False) -> str:
+ """Returns a copy of the string with each line indented by the `width` str.
+
+ If `width` is a number, `s` is indented by that number of whitespaces. The
+ first line and blank lines are not indented by default, unless `first` or
+ `blank` are `True`, respectively.
+
+ This is a re-implementation of the default `indent` Jinja filter, preserving
+ line ending characters (\r, \n, \f, etc.) The default `indent` Jinja filter
+ incorrectly replaces all of these characters with newlines."""
+ is_first_line = True
+ def indent_needed(line):
+ nonlocal first, blank, is_first_line
+ is_blank = not line.strip()
+ need_indent = (not is_first_line or first) and (not is_blank or blank)
+ is_first_line = False
+ return need_indent
+
+ indentation = width if isinstance(width, str) else ' ' * width
+ return textwrap.indent(s, indentation, indent_needed)
+
+
+def generate_test_files(name_to_dir_file: str) -> None:
+ """Generate Canvas tests from YAML file definition."""
+ output_dirs = _OutputPaths(element=pathlib.Path('..') / 'element',
+ offscreen=pathlib.Path('..') / 'offscreen')
+
+ params_template_loader = MutableDictLoader()
+
+ jinja_env = jinja2.Environment(
+ loader=jinja2.ChoiceLoader([
+ jinja2.PackageLoader('gentest'),
+ params_template_loader,
+ ]),
+ keep_trailing_newline=True,
+ trim_blocks=True,
+ lstrip_blocks=True)
+
+ jinja_env.filters['double_quote_escape'] = _double_quote_escape
+ jinja_env.filters['indent'] = _indent_filter
+
+ # Run with --test argument to run unit tests.
+ if len(sys.argv) > 1 and sys.argv[1] == '--test':
+ doctest = importlib.import_module('doctest')
+ doctest.testmod()
+ sys.exit()
+
+ name_to_sub_dir = (yaml.safe_load(
+ pathlib.Path(name_to_dir_file).read_text(encoding='utf-8')))
+
+ tests = []
+ test_yaml_directory = 'yaml'
+ yaml_files = [
+ os.path.join(test_yaml_directory, f)
+ for f in os.listdir(test_yaml_directory) if f.endswith('.yaml')
+ ]
+ for t in sum([
+ yaml.safe_load(pathlib.Path(f).read_text(encoding='utf-8'))
+ for f in yaml_files
+ ], []):
+ if 'DISABLED' in t:
+ continue
+ if 'meta' in t:
+ eval(compile(t['meta'], '<meta test>', 'exec'), {},
+ {'tests': tests})
+ else:
+ tests.append(t)
+
+ for sub_dir in set(name_to_sub_dir.values()):
+ output_dirs.sub_path(sub_dir).mkdir()
+
+ used_filenames = collections.defaultdict(set)
+ used_variants = collections.defaultdict(set)
+ for test in tests:
+ print(test['name'])
+ for grid in _get_variant_grids(test, jinja_env, params_template_loader):
+ if not grid.enabled:
+ continue
+ if test['name'] != grid.file_name:
+ print(f' {grid.file_name}')
+
+ _check_uniqueness(used_filenames, grid.file_name,
+ grid.canvas_types)
+ for variant in grid.variants:
+ _check_uniqueness(
+ used_variants,
+ '.'.join([_ensure_rendered(grid.file_name)] +
+ variant.params['grid_variant_names']),
+ grid.canvas_types)
+
+ sub_dir = _get_test_sub_dir(grid.file_name, name_to_sub_dir)
+ grid.generate_test(jinja_env, output_dirs.sub_path(sub_dir))
+
+ print()
+
+
+if __name__ == '__main__':
+ generate_test_files('name2dir.yaml')
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentestutilsunion.py b/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentestutilsunion.py
deleted file mode 100644
index fb0f96d..0000000
--- a/third_party/blink/web_tests/external/wpt/html/canvas/tools/gentestutilsunion.py
+++ /dev/null
@@ -1,1169 +0,0 @@
-"""Generates Canvas tests from YAML file definitions."""
-# Current code status:
-#
-# This was originally written by Philip Taylor for use at
-# http://philip.html5.org/tests/canvas/suite/tests/
-#
-# It has been adapted for use with the Web Platform Test Suite suite at
-# https://github.com/web-platform-tests/wpt/
-#
-# The original version had a number of now-removed features (multiple versions
-# of each test case of varying verbosity, Mozilla mochitests, semi-automated
-# test harness). It also had a different directory structure.
-
-# To use this script:
-# -make a python virtual environment somewhere (it doesn't matter where)
-# python3 -m venv venv
-# -enter the virtual environment
-# source venv/bin/activate
-# -install required packages in the venv
-# pip3 install cairocffi jinja2 pyyaml
-# -change to the directory with this script and run it
-# python3 gentest.py
-
-# To update or add test cases:
-#
-# * Modify the tests*.yaml files.
-# - 'name' is an arbitrary hierarchical name to help categorise tests.
-# - 'desc' is a rough description of what behaviour the test aims to test.
-# - 'code' is JavaScript code to execute, with some special commands starting
-# with '@'.
-# - 'expected' is what the final canvas output should be: a string 'green' or
-# 'clear' (100x50 images in both cases), or a string 'size 100 50' (or any
-# other size) followed by Python code using Pycairo to generate the image.
-#
-# * Run "./build.sh".
-# This requires a few Python modules which might not be ubiquitous.
-# It will usually emit some warnings, which ideally should be fixed but can
-# generally be safely ignored.
-#
-# * Test the tests, add new ones to Git, remove deleted ones from Git, etc.
-
-from typing import Any, Callable, Container, DefaultDict, FrozenSet
-from typing import List, Mapping, MutableMapping, Set, Tuple, Union
-
-import re
-import collections
-import copy
-import dataclasses
-import enum
-import importlib
-import math
-import os
-import pathlib
-import sys
-import textwrap
-
-import jinja2
-
-try:
- import cairocffi as cairo # type: ignore
-except ImportError:
- import cairo
-
-try:
- # Compatible and lots faster.
- import syck as yaml # type: ignore
-except ImportError:
- import yaml
-
-
-class Error(Exception):
- """Base class for all exceptions raised by this module"""
-
-
-class InvalidTestDefinitionError(Error):
- """Raised on invalid test definition."""
-
-
-def _double_quote_escape(string: str) -> str:
- return string.replace('\\', '\\\\').replace('"', '\\"')
-
-
-def _escape_js(string: str) -> str:
- string = _double_quote_escape(string)
- # Kind of an ugly hack, for nicer failure-message output.
- string = re.sub(r'\[(\w+)\]', r'[\\""+(\1)+"\\"]', string)
- return string
-
-
-def _expand_nonfinite(method: str, argstr: str, tail: str) -> str:
- """
- >>> print _expand_nonfinite('f', '<0 a>, <0 b>', ';')
- f(a, 0);
- f(0, b);
- f(a, b);
- >>> print _expand_nonfinite('f', '<0 a>, <0 b c>, <0 d>', ';')
- f(a, 0, 0);
- f(0, b, 0);
- f(0, c, 0);
- f(0, 0, d);
- f(a, b, 0);
- f(a, b, d);
- f(a, 0, d);
- f(0, b, d);
- """
- # argstr is "<valid-1 invalid1-1 invalid2-1 ...>, ..." (where usually
- # 'invalid' is Infinity/-Infinity/NaN).
- args = []
- for arg in argstr.split(', '):
- match = re.match('<(.*)>', arg)
- if match is None:
- raise InvalidTestDefinitionError(
- f'Expected arg to match format "<(.*)>", but was: {arg}')
- a = match.group(1)
- args.append(a.split(' '))
- calls = []
- # Start with the valid argument list.
- call = [args[j][0] for j in range(len(args))]
- # For each argument alone, try setting it to all its invalid values:
- for i, arg in enumerate(args):
- for a in arg[1:]:
- c2 = call[:]
- c2[i] = a
- calls.append(c2)
- # For all combinations of >= 2 arguments, try setting them to their
- # first invalid values. (Don't do all invalid values, because the
- # number of combinations explodes.)
- def f(c: List[str], start: int, depth: int) -> None:
- for i in range(start, len(args)):
- if len(args[i]) > 1:
- a = args[i][1]
- c2 = c[:]
- c2[i] = a
- if depth > 0:
- calls.append(c2)
- f(c2, i + 1, depth + 1)
-
- f(call, 0, 0)
-
- str_calls = (', '.join(c) for c in calls)
- return '\n'.join(f'{method}({params}){tail}' for params in str_calls)
-
-
-def _get_test_sub_dir(name: str, name_to_sub_dir: Mapping[str, str]) -> str:
- for prefix in sorted(name_to_sub_dir.keys(), key=len, reverse=True):
- if name.startswith(prefix):
- return name_to_sub_dir[prefix]
- raise InvalidTestDefinitionError(
- f'Test "{name}" has no defined target directory mapping')
-
-
-def _remove_extra_newlines(text: str) -> str:
- """Remove newlines if a backslash is found at end of line."""
- # Lines ending with '\' gets their newline character removed.
- text = re.sub(r'\\\n', '', text, flags=re.MULTILINE | re.DOTALL)
-
- # Lines ending with '\-' gets their newline and any leading white spaces on
- # the following line removed.
- text = re.sub(r'\\-\n\s*', '', text, flags=re.MULTILINE | re.DOTALL)
- return text
-
-
-def _expand_test_code(code: str) -> str:
- code = _remove_extra_newlines(code)
-
- code = re.sub(r' @moz-todo', '', code)
-
- code = re.sub(r'@moz-UniversalBrowserRead;', '', code)
-
- code = re.sub(r'@nonfinite ([^(]+)\(([^)]+)\)(.*)', lambda m:
- _expand_nonfinite(m.group(1), m.group(2), m.group(3)),
- code) # Must come before '@assert throws'.
-
- code = re.sub(r'@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);',
- r'_assertPixel(canvas, \1, \2);', code)
-
- code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);',
- r'_assertPixelApprox(canvas, \1, \2, 2);', code)
-
- code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+/- (\d+);',
- r'_assertPixelApprox(canvas, \1, \2, \3);', code)
-
- code = re.sub(r'@assert throws (\S+_ERR) (.*?);$',
- r'assert_throws_dom("\1", function() { \2; });', code,
- flags=re.MULTILINE | re.DOTALL)
-
- code = re.sub(r'@assert throws (\S+Error) (.*?);$',
- r'assert_throws_js(\1, function() { \2; });', code,
- flags=re.MULTILINE | re.DOTALL)
-
- code = re.sub(
- r'@assert (.*) === (.*);', lambda m:
- (f'_assertSame({m.group(1)}, {m.group(2)}, '
- f'"{_escape_js(m.group(1))}", "{_escape_js(m.group(2))}");'), code)
-
- code = re.sub(
- r'@assert (.*) !== (.*);', lambda m:
- (f'_assertDifferent({m.group(1)}, {m.group(2)}, '
- f'"{_escape_js(m.group(1))}", "{_escape_js(m.group(2))}");'), code)
-
- code = re.sub(
- r'@assert (.*) =~ (.*);',
- lambda m: f'assert_regexp_match({m.group(1)}, {m.group(2)});', code)
-
- code = re.sub(
- r'@assert (.*);',
- lambda m: f'_assert({m.group(1)}, "{_escape_js(m.group(1))}");', code)
-
- assert '@' not in code
-
- return code
-
-
-_TestParams = Mapping[str, Any]
-_MutableTestParams = MutableMapping[str, Any]
-
-
-class _CanvasType(str, enum.Enum):
- HTML_CANVAS = 'HtmlCanvas'
- OFFSCREEN_CANVAS = 'OffscreenCanvas'
- WORKER = 'Worker'
-
-
-class _TemplateType(str, enum.Enum):
- REFERENCE = 'reference'
- HTML_REFERENCE = 'html_reference'
- CAIRO_REFERENCE = 'cairo_reference'
- TESTHARNESS = 'testharness'
-
-
-class MutableDictLoader(jinja2.BaseLoader):
- """Loads Jinja templates from a `dict` that can be updated.
-
- This is essentially a version of `jinja2.DictLoader` whose content can be
- changed. `jinja2.DictLoader` accepts a `dict` at construction time and that
- `dict` cannot be changed. The templates served by `MutableDictLoader` on the
- other hand can be updated by calling `set_templates(new_templates)`. This is
- needed because we reuse the environment to render different tests and
- variants, each of which will have different templates.
- """
-
- def __init__(self) -> None:
- self._templates = dict() # type: Mapping[str, Any]
-
- def set_templates(self, new_templates: Mapping[str, Any]) -> None:
- """Changes the dict from which templates are loaded."""
- self._templates = new_templates
-
- def get_source(
- self, environment: jinja2.Environment, template: str
- ) -> Tuple[str, str, Callable[[], bool]]:
- """Loads a template from the current template dict."""
- del environment # Unused.
- source = self._templates.get(template)
- if source is None:
- raise jinja2.TemplateNotFound(template)
- if not isinstance(source, str):
- raise InvalidTestDefinitionError(
- f'Param "{template}" must be an str to be usable as Jinja '
- 'template.')
- return source, template, lambda: source == self._templates.get(template)
-
-
-class TemplateLoaderActivator:
- """Helper class used to set a given params dict in a MutableDictLoader.
-
- Jinja requires custom loaders to be registered in the environment and thus,
- we can't dynamically change them. We would need this to allow different test
- variants to have different templates. Using a `TemplateLoaderActivator`,
- the code can "activate" the templates for a given variant before rendering
- strings for that variant. For instance:
-
- loader = MutableDictLoader()
- jinja_env = jinja2.Environment(loader=[loader])
-
- templates1 = {'macros': '{% macro foo() %}foo{% endmacro %}'}
- activator1 = TemplateLoaderActivator(loader, templates1)
-
- templates2 = {'macros': '{% macro foo() %}bar{% endmacro %}'}
- activator2 = TemplateLoaderActivator(loader, templates2)
-
- main_template = '''
- {% import 'macros' as t %}
- {{ t.foo() }}
- '''
-
- # Render `main_template`, loading 'macros' from `templates1.
- activator1.activate()
- jinja_env.from_string(main_template).render(params1))
-
- # Render `main_template`, loading 'macros' from `templates2.
- activator2.activate()
- jinja_env.from_string(main_template).render(params2))
-
- """
-
- def __init__(self, loader: MutableDictLoader, params: _TestParams) -> None:
- self._loader = loader
- self._params = params
-
- def activate(self):
- self._loader.set_templates(self._params)
-
-
-class _LazyRenderedStr(collections.UserString):
- """A custom str type that renders it's content with Jinja when accessed.
-
- This is an str-like type, storing a Jinja template, but returning the
- rendered version of that template when the string is accessed. The rendered
- result is cached and returned on subsequent accesses.
-
- This allows template parameters to be themselves templates. Template
- parameters can then refer to each other and they'll be rendered in the right
- order, in reverse order of access.
-
- For instance:
-
- params = {}
- make_lazy = lambda value: _LazyRenderedStr(
- jinja_env, loader_activator, params, value)
-
- params.update({
- 'expected_value': make_lazy('rgba({{ color | join(", ") }})'),
- 'color': [0, 255, 0, make_lazy('{{ alpha }}')],
- 'alpha': 0.5,
- })
-
- main_template = 'assert value == "{{ expected_value }}"'
- result = jinja_env.from_string(main_template).render(params)
-
- In this example, upon rendering `main_template`, Jinja will first read
- `expected_value`, which reads `color`, which reads `alpha`. These will be
- rendered in reverse order, with `color` resolving to `[0, 255, 0, '0.5']`,
- `expected_value` resolving to 'rgba(0, 255, 0, 0.5)' and the final render
- resolving to: 'assert value == "rgba(0, 255, 0, 0.5)"'
- """
-
- def __init__(self, jinja_env: jinja2.Environment,
- loader_activator: TemplateLoaderActivator,
- params: _TestParams, value: str):
- # Don't call `super().__init__`, because we want to override `self.data`
- # to be a property instead of a member variable.
- # pylint: disable=super-init-not-called
- self._jinja_env = jinja_env
- self._loader_activator = loader_activator
- self._params = params
- self._value = value
- self._rendered = None
-
- @property
- def data(self):
- """Property returning the content of the `UserString`.
-
- This `_LazyRenderedStr` will be rendered on the first access. The
- rendered result is cached and returned directly on subsequent
- accesses."""
- if self._rendered is None:
- self._loader_activator.activate()
- self._rendered = (
- self._jinja_env.from_string(self._value).render(self._params))
- return self._rendered
-
- @property
- def __class__(self):
- """Makes `UserString` return any newly created strings as `str` objects.
-
- `UserString` functions returning a new string (e.g. `strip()`,
- `lower()`, etc.) normally return a string of the same type as the input
- `UserString`. It does do by using `__class__` to know the actual user
- string type. In our case, the result of these operations will always
- return a plain `str`, since any templating will have been rendered when
- reading the input string via `self.data`."""
- return str
-
-
-def _make_lazy_rendered(jinja_env: jinja2.Environment,
- loader_activator: TemplateLoaderActivator,
- params: _TestParams,
- value: Any) -> Any:
- """Recursively converts `value` to a _LazyRenderedStr.
-
- If `value` is a data structure, this function recurses into that structure
- and converts leaf objects. Any `str` found containing Jinja tags are
- converted to _LazyRenderedStr.
- """
- if isinstance(value, str) and ('{{' in value or '{%' in value):
- return _LazyRenderedStr(jinja_env, loader_activator, params, value)
- if isinstance(value, list):
- return [_make_lazy_rendered(jinja_env, loader_activator, params, v)
- for v in value]
- if isinstance(value, tuple):
- return tuple(_make_lazy_rendered(jinja_env, loader_activator, params, v)
- for v in value)
- if isinstance(value, dict):
- return {k: _make_lazy_rendered(jinja_env, loader_activator, params, v)
- for k, v in value.items()}
- return value
-
-
-def _ensure_rendered(value: Any) -> Any:
- """Recursively makes sure that all _LazyRenderedStr in `value` are rendered.
-
- If `value` is a data structure, this function recurses into that structure
- and renders any _LazyRenderedStr found."""
- if isinstance(value, _LazyRenderedStr):
- return str(value)
- if isinstance(value, list):
- return [_ensure_rendered(v) for v in value]
- if isinstance(value, tuple):
- return tuple(_ensure_rendered(v) for v in value)
- if isinstance(value, dict):
- return {k: _ensure_rendered(v) for k, v in value.items()}
- return value
-
-
-@dataclasses.dataclass
-class _OutputPaths:
- element: pathlib.Path
- offscreen: pathlib.Path
-
- def sub_path(self, sub_dir: str):
- """Create a new _OutputPaths that is a subpath of this _OutputPath."""
- return _OutputPaths(
- element=self.element / _ensure_rendered(sub_dir),
- offscreen=self.offscreen / _ensure_rendered(sub_dir))
-
- def path_for_canvas_type(self, canvas_type: _CanvasType) -> pathlib.Path:
- return (self.element if canvas_type == _CanvasType.HTML_CANVAS
- else self.offscreen)
-
- def mkdir(self) -> None:
- """Creates element and offscreen directories, if they don't exist."""
- self.element.mkdir(parents=True, exist_ok=True)
- self.offscreen.mkdir(parents=True, exist_ok=True)
-
-
-def _validate_test(test: _TestParams):
- if test.get('expected', '') == 'green' and re.search(
- r'@assert pixel .* 0,0,0,0;', test['code']):
- print(f'Probable incorrect pixel test in {test["name"]}')
-
- if 'size' in test and (not isinstance(test['size'], tuple)
- or len(test['size']) != 2):
- raise InvalidTestDefinitionError(
- f'Invalid canvas size "{test["size"]}" in test {test["name"]}. '
- 'Expected an array with two numbers.')
-
- if test['template_type'] == _TemplateType.TESTHARNESS:
- valid_test_types = {'sync', 'async', 'promise'}
- else:
- valid_test_types = {'promise'}
-
- test_type = test.get('test_type')
- if test_type is not None and test_type not in valid_test_types:
- raise InvalidTestDefinitionError(
- f'Invalid test_type: {test_type}. '
- f'Valid values are: {valid_test_types}.')
-
-
-def _render(jinja_env: jinja2.Environment,
- template_name: str,
- params: _TestParams, output_file_name: str):
- template = jinja_env.get_template(template_name)
- file_content = template.render(params)
- pathlib.Path(output_file_name).write_text(file_content, 'utf-8')
-
-
-def _write_cairo_images(pycairo_code: str, output_file: pathlib.Path) -> None:
- """Creates a png from pycairo code and write it to `output_file`."""
- full_code = (f'{pycairo_code}\n'
- f'surface.write_to_png("{output_file}")\n')
- eval(compile(full_code, '<string>', 'exec'), {
- 'cairo': cairo,
- 'math': math,
- })
-
-
-class _Variant():
-
- def __init__(self, params: _MutableTestParams) -> None:
- # Raw parameters, as specified in YAML, defining this test variant.
- self._params = params # type: _MutableTestParams
- # Parameters rendered for each enabled canvas types.
- self._canvas_type_params = {
- } # type: MutableMapping[_CanvasType, _MutableTestParams]
-
- @property
- def params(self) -> _MutableTestParams:
- """Returns this variant's raw param dict, as it's defined in YAML."""
- return self._params
-
- @property
- def canvas_type_params(self) -> MutableMapping[_CanvasType,
- _MutableTestParams]:
- """Returns this variant's param dict for different canvas types."""
- return self._canvas_type_params
-
- @staticmethod
- def create_with_defaults(test: _TestParams) -> '_Variant':
- """Create a _Variant from the specified params.
-
- Default values are added for certain parameters, if missing."""
- params = {
- 'enabled': 'true',
- 'desc': '',
- 'size': (100, 50),
- # Test name, which ultimately is used as filename. File variant
- # dimension names (i.e. the 'file_variant_names' property below) are
- # appended to this to produce unique filenames.
- 'name': '',
- # List holding the the file variant dimension names.
- 'file_variant_names': [],
- # List of this variant grid dimension names. This uniquely
- # identifies a single variant in a variant grid file.
- 'grid_variant_names': [],
- # List of this variant dimension names, including both file and grid
- # dimensions.
- 'variant_names': [],
- # Same as `file_variant_names`, but concatenated into a single
- # string. This is a useful to easily identify a variant file.
- 'file_variant_name': '',
- # Same as `grid_variant_names`, but concatenated into a single
- # string. This is a useful to easily identify a variant in a grid.
- 'grid_variant_name': '',
- # Same as `variant_names`, but concatenated into a single string.
- # This is a useful shorthand for tests having a single variant
- # dimension.
- 'variant_name': '',
- 'images': [],
- 'svgimages': [],
- 'fonts': [],
- }
- params.update(test)
- if 'variants' in params:
- del params['variants']
- return _Variant(params)
-
- def merge_params(self, params: _TestParams) -> '_Variant':
- """Returns a new `_Variant` that merges `self.params` and `params`."""
- new_params = copy.deepcopy(self._params)
- new_params.update(params)
- return _Variant(new_params)
-
- def _add_variant_name(self, name: str) -> None:
- self._params['variant_name'] += (
- ('.' if self.params['variant_name'] else '') + name)
- self._params['variant_names'] += [name]
-
- def with_grid_variant_name(self, name: str) -> '_Variant':
- """Addend a variant name to include in the grid element label."""
- self._add_variant_name(name)
- self._params['grid_variant_name'] += (
- ('.' if self.params['grid_variant_name'] else '') + name)
- self._params['grid_variant_names'] += [name]
- return self
-
- def with_file_variant_name(self, name: str) -> '_Variant':
- """Addend a variant name to include in the generated file name."""
- self._add_variant_name(name)
- self._params['file_variant_name'] += (
- ('.' if self.params['file_variant_name'] else '') + name)
- self._params['file_variant_names'] += [name]
- if self.params.get('append_variants_to_name', True):
- self._params['name'] += '.' + name
- return self
-
- def _get_file_name(self) -> str:
- file_name = self.params['name']
-
- if 'manual' in self.params:
- file_name += '-manual'
-
- return file_name
-
- def _get_canvas_types(self) -> FrozenSet[_CanvasType]:
- canvas_types = self.params.get('canvas_types', _CanvasType)
- invalid_types = {
- type
- for type in canvas_types if type not in list(_CanvasType)
- }
- if invalid_types:
- raise InvalidTestDefinitionError(
- f'Invalid canvas_types: {list(invalid_types)}. '
- f'Accepted values are: {[t.value for t in _CanvasType]}')
- return frozenset(_CanvasType(t) for t in canvas_types)
-
- def _get_template_type(self) -> _TemplateType:
- 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, 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,
- variant_id: int,
- params_template_loader: MutableDictLoader) -> None:
- """Finalize this variant by adding computed param fields."""
- self._params['id'] = variant_id
- self._params['file_name'] = self._get_file_name()
- self._params['canvas_types'] = self._get_canvas_types()
- self._params['template_type'] = self._get_template_type()
-
- if isinstance(self._params['size'], list):
- self._params['size'] = tuple(self._params['size'])
-
- loader_activator = TemplateLoaderActivator(params_template_loader,
- self._params)
- for canvas_type in self.params['canvas_types']:
- params = {'canvas_type': canvas_type}
- params.update(
- {k: _make_lazy_rendered(jinja_env, loader_activator, params, v)
- for k, v in self._params.items()})
- self._canvas_type_params[canvas_type] = params
-
- for name in ('code', 'reference', 'html_reference',
- 'cairo_reference'):
- param = params.get(name)
- if param is not None:
- params[name] = _expand_test_code(_ensure_rendered(param))
-
- _validate_test(self._params)
-
- def generate_expected_image(self, output_dirs: _OutputPaths) -> None:
- """Creates an expected image using Cairo and save filename in params."""
- # Expected images are only needed for HTML canvas tests.
- params = self._canvas_type_params.get(_CanvasType.HTML_CANVAS)
- if not params:
- return
-
- expected = _ensure_rendered(params['expected'])
-
- if expected == 'green':
- params['expected_img'] = '/images/green-100x50.png'
- return
- if expected == 'clear':
- params['expected_img'] = '/images/clear-100x50.png'
- return
- expected = re.sub(
- r'^size (\d+) (\d+)',
- r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)'
- r'\ncr = cairo.Context(surface)', expected)
-
- img_filename = f'{params["name"]}.png'
- _write_cairo_images(expected, output_dirs.element / img_filename)
- params['expected_img'] = img_filename
-
-
-class _VariantGrid:
-
- def __init__(self, variants: List[_Variant], grid_width: int) -> None:
- self._variants = variants
- self._grid_width = grid_width
-
- # Parameters rendered for each enabled canvas types.
- self._canvas_type_params = {
- } # type: Mapping[_CanvasType, _MutableTestParams]
- self._enabled = None
- self._file_name = None
- self._canvas_types = None
- self._template_type = None
-
- @property
- def variants(self) -> List[_Variant]:
- """Read only getter for the list of variant in this grid."""
- return self._variants
-
- @property
- def enabled(self) -> bool:
- """File name to which this grid will be written."""
- if self._enabled is None:
- enabled_str = self._unique_param(_CanvasType, 'enabled')
- self._enabled = (enabled_str.strip().lower() == 'true')
- return self._enabled
-
- @property
- def file_name(self) -> str:
- """File name to which this grid will be written."""
- if self._file_name is None:
- self._file_name = self._unique_param(_CanvasType, 'file_name')
- return self._file_name
-
- @property
- def canvas_types(self) -> FrozenSet[_CanvasType]:
- """Returns the set of all _CanvasType used by this grid's variants."""
- if self._canvas_types is None:
- self._canvas_types = self._param_set(_CanvasType, 'canvas_types')
- return self._canvas_types
-
- @property
- def template_type(self) -> _TemplateType:
- """Returns the type of Jinja template needed to render this grid."""
- if self._template_type is None:
- self._template_type = self._unique_param(_CanvasType,
- 'template_type')
- return self._template_type
-
- def finalize(self, jinja_env: jinja2.Environment,
- params_template_loader: MutableDictLoader):
- """Finalize this grid's variants, adding computed params fields."""
- for variant_id, variant in enumerate(self.variants):
- variant.finalize_params(jinja_env, variant_id,
- params_template_loader)
-
- if len(self.variants) == 1:
- self._canvas_type_params = self.variants[0].canvas_type_params
- else:
- self._canvas_type_params = self._get_grid_params()
-
- def add_dimension(self, variants: Mapping[str,
- _TestParams]) -> '_VariantGrid':
- """Adds a variant dimension to this variant grid.
-
- If the grid currently has N variants, adding a dimension with M variants
- results in a grid containing N*M variants. Of course, we can't display
- more than 2 dimensions on a 2D screen, so adding dimensions beyond 2
- repeats all previous dimensions down vertically, with the grid width
- set to the number of variants of the first dimension (unless overridden
- by setting `grid_width`). For instance, a 3D variant space with
- dimensions 3 x 2 x 2 will result in this layout:
- 000 100 200
- 010 110 210
-
- 001 101 201
- 011 111 211
- """
- new_variants = [
- old_variant.merge_params(params or {}).with_grid_variant_name(name)
- for name, params in variants.items()
- for old_variant in self.variants
- ]
- # The first dimension dictates the grid-width, unless it was specified
- # beforehand via the test params.
- new_grid_width = (self._grid_width
- if self._grid_width > 1 else len(variants))
- return _VariantGrid(variants=new_variants, grid_width=new_grid_width)
-
- def merge_params(self, name: str, params: _TestParams) -> '_VariantGrid':
- """Merges the specified `params` into every variant of this grid."""
- return _VariantGrid(variants=[
- variant.merge_params(params).with_file_variant_name(name)
- for variant in self.variants
- ],
- grid_width=self._grid_width)
-
- def _variants_for_canvas_type(
- self, canvas_type: _CanvasType) -> List[_TestParams]:
- """Returns the variants of this grid enabled for `canvas_type`."""
- return [
- v.canvas_type_params[canvas_type]
- for v in self.variants
- if canvas_type in v.canvas_type_params
- ]
-
- def _unique_param(
- self, canvas_types: Container[_CanvasType], name: str) -> Any:
- """Returns the value of the `name` param for this grid.
-
- All the variants for all canvas types in `canvas_types` of this grid
- must agree on the same value for this parameter, or else an exception is
- thrown."""
- values = {_ensure_rendered(params.get(name))
- for variant in self.variants
- for type, params in variant.canvas_type_params.items()
- if type in canvas_types}
- if len(values) != 1:
- raise InvalidTestDefinitionError(
- 'All variants in a variant grid must use the same value '
- f'for property "{name}". Got these values: {values}. '
- 'Consider specifying the property outside of grid '
- 'variants dimensions (in the base test definition or in a '
- 'file variant dimension)')
- return values.pop()
-
- def _param_set(self, canvas_types: Container[_CanvasType], name: str):
- """Returns the set of all values this grid has for the `name` param.
-
- The `name` parameter of each variant is expected to be a sequence. These
- are all accumulated in a set and returned. The values are accumulated
- across all canvas types in `canvas_types`."""
- return frozenset(sum([list(_ensure_rendered(params.get(name, [])))
- for v in self.variants
- for type, params in v.canvas_type_params.items()
- if type in canvas_types],
- []))
-
- def _get_grid_params(self) -> Mapping[_CanvasType, _MutableTestParams]:
- """Returns the params dict needed to render this grid with Jinja."""
- grid_params = {}
- for canvas_type in self.canvas_types:
- params = grid_params[canvas_type] = {}
- params.update({
- 'variants': self._variants_for_canvas_type(canvas_type),
- 'grid_width': self._grid_width,
- 'name': self._unique_param([canvas_type], 'name'),
- 'test_type': self._unique_param([canvas_type], 'test_type'),
- 'fuzzy': self._unique_param([canvas_type], 'fuzzy'),
- 'timeout': self._unique_param([canvas_type], 'timeout'),
- 'notes': self._unique_param([canvas_type], 'notes'),
- 'images': self._param_set([canvas_type], 'images'),
- 'svgimages': self._param_set([canvas_type], 'svgimages'),
- 'fonts': self._param_set([canvas_type], 'fonts'),
- })
- if self.template_type in (_TemplateType.REFERENCE,
- _TemplateType.HTML_REFERENCE,
- _TemplateType.CAIRO_REFERENCE):
- params['desc'] = self._unique_param([canvas_type], 'desc')
- return grid_params
-
- def _write_reference_test(self, jinja_env: jinja2.Environment,
- output_files: _OutputPaths):
- grid = '_grid' if len(self.variants) > 1 else ''
-
- # If variants don't all use the same offscreen and worker canvas types,
- # the offscreen and worker grids won't be identical. The worker test
- # therefore can't reuse the offscreen reference file.
- offscreen_types = {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER}
- needs_worker_reference = len({
- variant.params['canvas_types'] & offscreen_types
- for variant in self.variants
- }) != 1
-
- test_templates = {
- _CanvasType.HTML_CANVAS: f'reftest_element{grid}.html',
- _CanvasType.OFFSCREEN_CANVAS: f'reftest_offscreen{grid}.html',
- _CanvasType.WORKER: f'reftest_worker{grid}.html',
- }
- ref_templates = {
- _TemplateType.REFERENCE: f'reftest_element{grid}.html',
- _TemplateType.HTML_REFERENCE: f'reftest{grid}.html',
- _TemplateType.CAIRO_REFERENCE: f'reftest_img{grid}.html'
- }
- test_output_paths = {
- _CanvasType.HTML_CANVAS: f'{output_files.element}.html',
- _CanvasType.OFFSCREEN_CANVAS: f'{output_files.offscreen}.html',
- _CanvasType.WORKER: f'{output_files.offscreen}.w.html',
- }
- ref_output_paths = {
- _CanvasType.HTML_CANVAS: f'{output_files.element}-expected.html',
- _CanvasType.OFFSCREEN_CANVAS:
- f'{output_files.offscreen}-expected.html',
- _CanvasType.WORKER: (
- f'{output_files.offscreen}.w-expected.html'
- if needs_worker_reference
- else f'{output_files.offscreen}-expected.html'),
- }
- for canvas_type, params in self._canvas_type_params.items():
- params['reference_file'] = pathlib.Path(
- ref_output_paths[canvas_type]).name
- _render(jinja_env, test_templates[canvas_type], params,
- test_output_paths[canvas_type])
-
- if canvas_type != _CanvasType.WORKER or needs_worker_reference:
- params['is_test_reference'] = True
- _render(jinja_env, ref_templates[self.template_type], params,
- ref_output_paths[canvas_type])
-
- def _write_testharness_test(self, jinja_env: jinja2.Environment,
- output_files: _OutputPaths):
- grid = '_grid' if len(self.variants) > 1 else ''
-
- templates = {
- _CanvasType.HTML_CANVAS: f'testharness_element{grid}.html',
- _CanvasType.OFFSCREEN_CANVAS: f'testharness_offscreen{grid}.html',
- _CanvasType.WORKER: f'testharness_worker{grid}.js',
- }
- test_output_files = {
- _CanvasType.HTML_CANVAS: f'{output_files.element}.html',
- _CanvasType.OFFSCREEN_CANVAS: f'{output_files.offscreen}.html',
- _CanvasType.WORKER: f'{output_files.offscreen}.worker.js',
- }
-
- # Create test cases for canvas, offscreencanvas and worker.
- for canvas_type, params in self._canvas_type_params.items():
- _render(jinja_env, templates[canvas_type], params,
- test_output_files[canvas_type])
-
- def _generate_cairo_reference_grid(self,
- canvas_type: _CanvasType,
- 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.canvas_type_params[canvas_type].get('cairo_reference')
- for v in self.variants):
- return
-
- width, height = self._unique_param([canvas_type], '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.canvas_type_params[canvas_type].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'
- output_dir = output_dirs.path_for_canvas_type(canvas_type)
- _write_cairo_images(cairo_code, output_dir / img_filename)
- self._canvas_type_params[canvas_type]['img_reference'] = img_filename
-
- def _generate_cairo_images(self, output_dirs: _OutputPaths) -> None:
- """Generates the pycairo images found in the YAML test definition."""
- # 'expected:' is only used for HTML_CANVAS tests.
- has_expected = any(v.canvas_type_params
- .get(_CanvasType.HTML_CANVAS, {})
- .get('expected') for v in self._variants)
- has_cairo_reference = any(
- params.get('cairo_reference')
- for v in self._variants
- for params in v.canvas_type_params.values())
-
- 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.')
- if self.template_type != _TemplateType.TESTHARNESS:
- raise InvalidTestDefinitionError(
- 'Parameter "expected" is not supported in reference '
- 'tests.')
- self.variants[0].generate_expected_image(output_dirs)
- elif has_cairo_reference:
- for canvas_type in _CanvasType:
- self._generate_cairo_reference_grid(canvas_type, output_dirs)
-
- def generate_test(self, jinja_env: jinja2.Environment,
- output_dirs: _OutputPaths) -> None:
- """Generate the test files to the specified output dirs."""
- self._generate_cairo_images(output_dirs)
-
- output_files = output_dirs.sub_path(self.file_name)
-
- if self.template_type in (_TemplateType.REFERENCE,
- _TemplateType.HTML_REFERENCE,
- _TemplateType.CAIRO_REFERENCE):
- self._write_reference_test(jinja_env, output_files)
- else:
- self._write_testharness_test(jinja_env, output_files)
-
-
-class _VariantLayout(str, enum.Enum):
- SINGLE_FILE = 'single_file'
- MULTI_FILES = 'multi_files'
-
-
-@dataclasses.dataclass
-class _VariantDimension:
- variants: Mapping[str, _TestParams]
- layout: _VariantLayout
-
-
-def _get_variant_dimensions(params: _TestParams) -> List[_VariantDimension]:
- variants = params.get('variants', [])
- if not isinstance(variants, list):
- raise InvalidTestDefinitionError(
- textwrap.dedent("""
- Variants must be specified as a list of variant dimensions, e.g.:
- variants:
- - dimension1-variant1:
- param: ...
- dimension1-variant2:
- param: ...
- - dimension2-variant1:
- param: ...
- dimension2-variant2:
- param: ..."""))
-
- variants_layout = params.get('variants_layout',
- [_VariantLayout.MULTI_FILES] * len(variants))
- if len(variants) != len(variants_layout):
- raise InvalidTestDefinitionError(
- 'variants and variants_layout must be lists of the same size')
- invalid_layouts = [
- l for l in variants_layout if l not in list(_VariantLayout)
- ]
- if invalid_layouts:
- raise InvalidTestDefinitionError('Invalid variants_layout: ' +
- ', '.join(invalid_layouts) +
- '. Valid layouts are: ' +
- ', '.join(_VariantLayout))
-
- return [
- _VariantDimension(z[0], z[1]) for z in zip(variants, variants_layout)
- ]
-
-
-def _get_variant_grids(
- test: _TestParams,
- jinja_env: jinja2.Environment,
- params_template_loader: MutableDictLoader
-) -> 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
- if dimension.layout == _VariantLayout.MULTI_FILES:
- grids = [
- grid.merge_params(name, params)
- for name, params in variants.items() for grid in grids
- ]
- else:
- grids = [grid.add_dimension(variants) for grid in grids]
-
- for grid in grids:
- grid.finalize(jinja_env, params_template_loader)
-
- return grids
-
-
-def _check_uniqueness(tested: DefaultDict[str, Set[_CanvasType]], name: str,
- canvas_types: FrozenSet[_CanvasType]) -> None:
- already_tested = tested[name].intersection(canvas_types)
- if already_tested:
- raise InvalidTestDefinitionError(
- f'Test {name} is defined twice for types {already_tested}')
- tested[name].update(canvas_types)
-
-
-def _indent_filter(s: str, width: Union[int, str] = 4,
- first: bool = False, blank: bool = False) -> str:
- """Returns a copy of the string with each line indented by the `width` str.
-
- If `width` is a number, `s` is indented by that number of whitespaces. The
- first line and blank lines are not indented by default, unless `first` or
- `blank` are `True`, respectively.
-
- This is a re-implementation of the default `indent` Jinja filter, preserving
- line ending characters (\r, \n, \f, etc.) The default `indent` Jinja filter
- incorrectly replaces all of these characters with newlines."""
- is_first_line = True
- def indent_needed(line):
- nonlocal first, blank, is_first_line
- is_blank = not line.strip()
- need_indent = (not is_first_line or first) and (not is_blank or blank)
- is_first_line = False
- return need_indent
-
- indentation = width if isinstance(width, str) else ' ' * width
- return textwrap.indent(s, indentation, indent_needed)
-
-
-def generate_test_files(name_to_dir_file: str) -> None:
- """Generate Canvas tests from YAML file definition."""
- output_dirs = _OutputPaths(element=pathlib.Path('..') / 'element',
- offscreen=pathlib.Path('..') / 'offscreen')
-
- params_template_loader = MutableDictLoader()
-
- jinja_env = jinja2.Environment(
- loader=jinja2.ChoiceLoader([
- jinja2.PackageLoader('gentest'),
- params_template_loader,
- ]),
- keep_trailing_newline=True,
- trim_blocks=True,
- lstrip_blocks=True)
-
- jinja_env.filters['double_quote_escape'] = _double_quote_escape
- jinja_env.filters['indent'] = _indent_filter
-
- # Run with --test argument to run unit tests.
- if len(sys.argv) > 1 and sys.argv[1] == '--test':
- doctest = importlib.import_module('doctest')
- doctest.testmod()
- sys.exit()
-
- name_to_sub_dir = (yaml.safe_load(
- pathlib.Path(name_to_dir_file).read_text(encoding='utf-8')))
-
- tests = []
- test_yaml_directory = 'yaml'
- yaml_files = [
- os.path.join(test_yaml_directory, f)
- for f in os.listdir(test_yaml_directory) if f.endswith('.yaml')
- ]
- for t in sum([
- yaml.safe_load(pathlib.Path(f).read_text(encoding='utf-8'))
- for f in yaml_files
- ], []):
- if 'DISABLED' in t:
- continue
- if 'meta' in t:
- eval(compile(t['meta'], '<meta test>', 'exec'), {},
- {'tests': tests})
- else:
- tests.append(t)
-
- for sub_dir in set(name_to_sub_dir.values()):
- output_dirs.sub_path(sub_dir).mkdir()
-
- used_filenames = collections.defaultdict(set)
- used_variants = collections.defaultdict(set)
- for test in tests:
- print(test['name'])
- for grid in _get_variant_grids(test, jinja_env, params_template_loader):
- if not grid.enabled:
- continue
- if test['name'] != grid.file_name:
- print(f' {grid.file_name}')
-
- _check_uniqueness(used_filenames, grid.file_name,
- grid.canvas_types)
- for variant in grid.variants:
- _check_uniqueness(
- used_variants,
- '.'.join([_ensure_rendered(grid.file_name)] +
- variant.params['grid_variant_names']),
- grid.canvas_types)
-
- sub_dir = _get_test_sub_dir(grid.file_name, name_to_sub_dir)
- grid.generate_test(jinja_env, output_dirs.sub_path(sub_dir))
-
- print()
-
-
-if __name__ == '__main__':
- generate_test_files('name2dir.yaml')