diff options
Diffstat (limited to 'testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py')
-rw-r--r-- | testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py | 387 |
1 files changed, 302 insertions, 85 deletions
diff --git a/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py b/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py index 57077f6057..415090a14a 100644 --- a/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py +++ b/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py @@ -345,17 +345,43 @@ class _Variant(): 'desc': '', 'size': [100, 50], 'variant_names': [], + 'grid_variant_names': [], + 'images': [], + 'svgimages': [], } params.update(test) return _Variant(params) - def _get_variant_name(self, jinja_env: jinja2.Environment) -> str: - name = self.params['name'] + def merge_params(self, params: _TestParams) -> '_Variant': + """Returns a new `_Variant` that merges `self.params` and `params`.""" + new_params = {} + new_params.update(self.params) + new_params.update(params) + return _Variant(new_params) + + def with_grid_variant_name(self, name: str) -> '_Variant': + """Addend a variant name to include in the grid element label.""" + self._params.update({ + 'variant_names': (self.params['variant_names'] + [name]), + 'grid_variant_names': (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._params.update({ + 'variant_names': (self.params['variant_names'] + [name]), + }) if self.params.get('append_variants_to_name', True): - name = '.'.join([name] + self.params['variant_names']) + self._params['name'] = self.params['name'] + '.' + name + return self + + def _render_param(self, jinja_env: jinja2.Environment, + param_name: str) -> str: + """Get the specified variant parameter and render it with Jinja.""" + value = self.params[param_name] + return jinja_env.from_string(value).render(self.params) - name = jinja_env.from_string(name).render(self.params) - return name def _get_file_name(self) -> str: file_name = self.params['name'] @@ -389,9 +415,12 @@ class _Variant(): return _TemplateType.HTML_REFERENCE return _TemplateType.TESTHARNESS - def finalize_params(self, jinja_env: jinja2.Environment) -> None: + def finalize_params(self, jinja_env: jinja2.Environment, + variant_id: int) -> None: """Finalize this variant by adding computed param fields.""" - self._params['name'] = self._get_variant_name(jinja_env) + self._params['id'] = variant_id + self._params['name'] = self._render_param(jinja_env, 'name') + self._params['desc'] = self._render_param(jinja_env, 'desc') self._params['file_name'] = self._get_file_name() self._params['canvas_types'] = self._get_canvas_types() self._params['template_type'] = self._get_template_type() @@ -461,103 +490,282 @@ class _Variant(): self._params['expected_img'] = f'{name}.png' + +class _VariantGrid: + + def __init__(self, variants: List[_Variant], grid_width: int) -> None: + self._variants = variants + self._grid_width = grid_width + + self._file_name = None + self._canvas_types = None + self._template_type = None + self._params = None + + @property + def variants(self) -> List[_Variant]: + """Read only getter for the list of variant in this grid.""" + return self._variants + + @property + def file_name(self): + """File name to which this grid will be written.""" + if self._file_name is None: + self._file_name = self._unique_param('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('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('template_type') + return self._template_type + + @property + def params(self) -> _TestParams: + """Returns this grid's param dict, used to render Jinja templates.""" + if self._params is None: + if len(self.variants) == 1: + self._params = dict(self.variants[0].params) + else: + self._params = self._get_grid_params() + return self._params + + def finalize(self, jinja_env: jinja2.Environment): + """Finalize this grid's variants, adding computed params fields.""" + for variant_id, variant in enumerate(self.variants): + variant.finalize_params(jinja_env, variant_id) + + 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.params for v in self.variants + if canvas_type in v.params['canvas_types'] + ] + + def _unique_param(self, name: str) -> Any: + """Returns the value of the `name` param for this grid. + + All the variants in this grid must agree on the same value for this + parameter, or else an exception is thrown.""" + values = {variant.params.get(name) for variant in self.variants} + 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, 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.""" + return frozenset(sum([list(v.params[name]) for v in self.variants], + [])) + + def _get_grid_params(self) -> _TestParams: + """Returns the params dict needed to render this grid with Jinja.""" + filter_variant = self._variants_for_canvas_type + grid_params = { + 'element_variants': filter_variant(_CanvasType.HTML_CANVAS), + 'offscreen_variants': filter_variant(_CanvasType.OFFSCREEN_CANVAS), + 'worker_variants': filter_variant(_CanvasType.WORKER), + 'grid_width': self._grid_width, + 'name': self._unique_param('name'), + 'test_type': self._unique_param('test_type'), + 'fuzzy': self._unique_param('fuzzy'), + 'timeout': self._unique_param('timeout'), + 'notes': self._unique_param('notes'), + 'images': self._param_set('images'), + 'svgimages': self._param_set('svgimages'), + } + if self.template_type in (_TemplateType.REFERENCE, + _TemplateType.HTML_REFERENCE): + grid_params['desc'] = self._unique_param('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 + params = dict(self.params) - if _CanvasType.HTML_CANVAS in params['canvas_types']: - _render(jinja_env, 'reftest_element.html', params, + params['reference_file'] = f'{params["name"]}-expected.html' + if _CanvasType.HTML_CANVAS in self.canvas_types: + _render(jinja_env, f'reftest_element{grid}.html', params, f'{output_files.element}.html') - if _CanvasType.OFFSCREEN_CANVAS in params['canvas_types']: - _render(jinja_env, 'reftest_offscreen.html', params, + if _CanvasType.OFFSCREEN_CANVAS in self.canvas_types: + _render(jinja_env, f'reftest_offscreen{grid}.html', params, f'{output_files.offscreen}.html') - if _CanvasType.WORKER in params['canvas_types']: - _render(jinja_env, 'reftest_worker.html', params, + if _CanvasType.WORKER in self.canvas_types: + if needs_worker_reference: + params['reference_file'] = f'{params["name"]}.w-expected.html' + _render(jinja_env, f'reftest_worker{grid}.html', params, f'{output_files.offscreen}.w.html') params['is_test_reference'] = True - is_html_ref = params['template_type'] == _TemplateType.HTML_REFERENCE - ref_template = 'reftest.html' if is_html_ref else 'reftest_element.html' - if _CanvasType.HTML_CANVAS in params['canvas_types']: - _render(jinja_env, ref_template, params, + 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') + + if _CanvasType.HTML_CANVAS in self.canvas_types: + _render(jinja_env, ref_template_name, params, f'{output_files.element}-expected.html') - if {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER - } & params['canvas_types']: - _render(jinja_env, ref_template, params, + + if self.canvas_types & offscreen_types: + # We use the same template for all reference files, so we need to + # assign the variant definition to the variable expected by the + # template. + params['element_variants'] = params.get('offscreen_variants') + _render(jinja_env, ref_template_name, params, f'{output_files.offscreen}-expected.html') + if needs_worker_reference: + params['element_variants'] = params.get('worker_variants') + _render(jinja_env, ref_template_name, params, + f'{output_files.offscreen}.w-expected.html') def _write_testharness_test(self, jinja_env: jinja2.Environment, output_files: _OutputPaths): + grid = '_grid' if len(self.variants) > 1 else '' + # Create test cases for canvas and offscreencanvas. - if _CanvasType.HTML_CANVAS in self.params['canvas_types']: - _render(jinja_env, 'testharness_element.html', self.params, + if _CanvasType.HTML_CANVAS in self.canvas_types: + _render(jinja_env, f'testharness_element{grid}.html', self.params, f'{output_files.element}.html') - - if _CanvasType.OFFSCREEN_CANVAS in self.params['canvas_types']: - _render(jinja_env, 'testharness_offscreen.html', self.params, - f'{output_files.offscreen}.html') - - if _CanvasType.WORKER in self.params['canvas_types']: - _render(jinja_env, 'testharness_worker.js', self.params, + if _CanvasType.OFFSCREEN_CANVAS in self.canvas_types: + _render(jinja_env, f'testharness_offscreen{grid}.html', + self.params, f'{output_files.offscreen}.html') + if _CanvasType.WORKER in self.canvas_types: + _render(jinja_env, f'testharness_worker{grid}.js', self.params, f'{output_files.offscreen}.worker.js') def generate_test(self, jinja_env: jinja2.Environment, output_dirs: _OutputPaths) -> None: """Generate the test files to the specified output dirs.""" - output_files = output_dirs.sub_path(self.params['file_name']) + output_files = output_dirs.sub_path(self.file_name) - if self.params['template_type'] in (_TemplateType.REFERENCE, - _TemplateType.HTML_REFERENCE): + if self.template_type in (_TemplateType.REFERENCE, + _TemplateType.HTML_REFERENCE): self._write_reference_test(jinja_env, output_files) else: self._write_testharness_test(jinja_env, output_files) -def _recursive_expand_variant_matrix(original_test: _TestParams, - variant_matrix: List[_TestParams], - current_selection: List[Tuple[str, Any]], - test_variants: List[_Variant]): - if len(current_selection) == len(variant_matrix): - # Selection for each variant is done, so add a new test to test_list. - test = dict(original_test) - variant_name_list = [] - for variant_name, variant_params in current_selection: - test.update(variant_params) - variant_name_list.append(variant_name) - # Expose variant names as a list so they can be used from the yaml - # files, which helps with better naming of tests. - test.update({'variant_names': variant_name_list}) - test_variants.append(_Variant.create_with_defaults(test)) - else: - # Continue the recursion with each possible selection for the current - # variant. - variant = variant_matrix[len(current_selection)] - for variant_options in variant.items(): - current_selection.append(variant_options) - _recursive_expand_variant_matrix(original_test, variant_matrix, - current_selection, test_variants) - current_selection.pop() - - -def _get_variants(test: _TestParams) -> List[_Variant]: - current_selection = [] - test_variants = [] - variants = test.get('variants', []) +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: ...""")) - _recursive_expand_variant_matrix(test, variants, current_selection, - test_variants) - return test_variants + 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: Mapping[str, Any]) -> List[_VariantGrid]: + base_variant = _Variant.create_with_defaults(test) + grid_width = base_variant.params.get('grid_width', 1) + 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] + return grids def _check_uniqueness(tested: DefaultDict[str, Set[_CanvasType]], name: str, @@ -619,21 +827,30 @@ def generate_test_files(name_to_dir_file: str) -> None: except FileExistsError: pass # Ignore if it already exists, - used_tests = collections.defaultdict(set) + used_filenames = collections.defaultdict(set) + used_variants = collections.defaultdict(set) for test in tests: print(test['name']) - for variant in _get_variants(test): - variant.finalize_params(jinja_env) - if test['name'] != variant.params['name']: - print(f' {variant.params["name"]}') + for grid in _get_variant_grids(test): + + grid.finalize(jinja_env) + if test['name'] != grid.file_name: + print(f' {grid.file_name}') - sub_dir = _get_test_sub_dir(variant.params['file_name'], - name_to_sub_dir) + sub_dir = _get_test_sub_dir(grid.file_name, name_to_sub_dir) output_sub_dirs = output_dirs.sub_path(sub_dir) - _check_uniqueness(used_tests, variant.params['name'], - variant.params['canvas_types']) - variant.generate_expected_image(output_sub_dirs) - variant.generate_test(jinja_env, output_sub_dirs) + _check_uniqueness(used_filenames, grid.file_name, + grid.canvas_types) + for variant in grid.variants: + _check_uniqueness( + used_variants, + '.'.join([grid.file_name] + + variant.params['grid_variant_names']), + grid.canvas_types) + + for variant in grid.variants: + variant.generate_expected_image(output_sub_dirs) + grid.generate_test(jinja_env, output_sub_dirs) print() |