Encapsulate all code related to variants into a _Variant class (#45547)

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

Co-authored-by: Jean-Philippe Gravel <jpgravel@chromium.org>
diff --git a/html/canvas/tools/gentestutilsunion.py b/html/canvas/tools/gentestutilsunion.py
index 44124dc..80f677d 100644
--- a/html/canvas/tools/gentestutilsunion.py
+++ b/html/canvas/tools/gentestutilsunion.py
@@ -320,188 +320,197 @@
     return code
 
 
-def _add_default_params(test: _TestParams) -> _TestParams:
-    params = {
-        'desc': '',
-        'size': [100, 50],
-        'variant_names': [],
-    }
-    params.update(test)
-    return params
+class _Variant():
 
+    def __init__(self, params: _MutableTestParams) -> None:
+        self._params = params
 
-def _get_variant_name(jinja_env: jinja2.Environment,
-                      params: _TestParams) -> str:
-    name = params['name']
-    if params.get('append_variants_to_name', True):
-        name = '.'.join([name] + params['variant_names'])
+    @property
+    def params(self) -> _TestParams:
+        """Read-only getter for this variant's param dict."""
+        return self._params
 
-    name = jinja_env.from_string(name).render(params)
-    return name
+    @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 = {
+            'desc': '',
+            'size': [100, 50],
+            'variant_names': [],
+        }
+        params.update(test)
+        return _Variant(params)
 
-def _get_file_name(params: _TestParams) -> str:
-    file_name = params['name']
-    if 'manual' in params:
-        file_name += '-manual'
-    return file_name
+    def _get_variant_name(self, jinja_env: jinja2.Environment) -> str:
+        name = self.params['name']
+        if self.params.get('append_variants_to_name', True):
+            name = '.'.join([name] + self.params['variant_names'])
 
+        name = jinja_env.from_string(name).render(self.params)
+        return name
 
-def _get_canvas_types(params: _TestParams) -> FrozenSet[_CanvasType]:
-    canvas_types = 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_file_name(self) -> str:
+        file_name = self.params['name']
 
+        if 'manual' in self.params:
+            file_name += '-manual'
 
-def _get_template_type(params: _TestParams) -> _TemplateType:
-    if 'reference' in params and 'html_reference' in params:
-        raise InvalidTestDefinitionError(
-            f'Test {params["name"]} is invalid, "reference" and '
-            '"html_reference" can\'t both be specified at the same time.')
+        return file_name
 
-    if 'reference' in params:
-        return _TemplateType.REFERENCE
-    if 'html_reference' in params:
-        return _TemplateType.HTML_REFERENCE
-    return _TemplateType.TESTHARNESS
+    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:
+        if 'reference' in self.params and 'html_reference' in self.params:
+            raise InvalidTestDefinitionError(
+                f'Test {self.params["name"]} is invalid, "reference" and '
+                '"html_reference" can\'t both be specified at the same time.')
 
-def _finalize_params(jinja_env: jinja2.Environment,
-                     params: _MutableTestParams) -> None:
-    params['name'] = _get_variant_name(jinja_env, params)
-    params['file_name'] = _get_file_name(params)
-    params['canvas_types'] = _get_canvas_types(params)
-    params['template_type'] = _get_template_type(params)
+        if 'reference' in self.params:
+            return _TemplateType.REFERENCE
+        if 'html_reference' in self.params:
+            return _TemplateType.HTML_REFERENCE
+        return _TemplateType.TESTHARNESS
 
-    if 'reference' in params:
-        params['reference'] = _preprocess_code(jinja_env, params['reference'],
-                                               params)
+    def finalize_params(self, jinja_env: jinja2.Environment) -> None:
+        """Finalize this variant by adding computed param fields."""
+        self._params['name'] = self._get_variant_name(jinja_env)
+        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 'html_reference' in params:
-        params['html_reference'] = _preprocess_code(jinja_env,
-                                                    params['html_reference'],
-                                                    params)
+        if 'reference' in self._params:
+            self._params['reference'] = _preprocess_code(
+                jinja_env, self._params['reference'], self._params)
 
-    code_params = dict(params)
-    if _CanvasType.HTML_CANVAS in params['canvas_types']:
-        code_params['canvas_type'] = _CanvasType.HTML_CANVAS.value
-        params['code_element'] = _preprocess_code(jinja_env, params['code'],
-                                                  code_params)
+        if 'html_reference' in self._params:
+            self._params['html_reference'] = _preprocess_code(
+                jinja_env, self._params['html_reference'], self._params)
 
-    if _CanvasType.OFFSCREEN_CANVAS in params['canvas_types']:
-        code_params['canvas_type'] = _CanvasType.OFFSCREEN_CANVAS.value
-        params['code_offscreen'] = _preprocess_code(jinja_env, params['code'],
-                                                    code_params)
+        code_params = dict(self.params)
+        if _CanvasType.HTML_CANVAS in self.params['canvas_types']:
+            code_params['canvas_type'] = _CanvasType.HTML_CANVAS.value
+            self._params['code_element'] = _preprocess_code(
+                jinja_env, self._params['code'], code_params)
 
-    if _CanvasType.WORKER in params['canvas_types']:
-        code_params['canvas_type'] = _CanvasType.WORKER.value
-        params['code_worker'] = _preprocess_code(jinja_env, params['code'],
-                                                 code_params)
+        if _CanvasType.OFFSCREEN_CANVAS in self.params['canvas_types']:
+            code_params['canvas_type'] = _CanvasType.OFFSCREEN_CANVAS.value
+            self._params['code_offscreen'] = _preprocess_code(
+                jinja_env, self._params['code'], code_params)
 
+        if _CanvasType.WORKER in self.params['canvas_types']:
+            code_params['canvas_type'] = _CanvasType.WORKER.value
+            self._params['code_worker'] = _preprocess_code(
+                jinja_env, self._params['code'], code_params)
 
-def _write_reference_test(jinja_env: jinja2.Environment, params: _TestParams,
-                          output_files: _OutputPaths):
-    if _CanvasType.HTML_CANVAS in params['canvas_types']:
-        pathlib.Path(f'{output_files.element}.html').write_text(
-            _render(jinja_env, 'reftest_element.html', params), 'utf-8')
-    if _CanvasType.OFFSCREEN_CANVAS in params['canvas_types']:
-        pathlib.Path(f'{output_files.offscreen}.html').write_text(
-            _render(jinja_env, 'reftest_offscreen.html', params), 'utf-8')
-    if _CanvasType.WORKER in params['canvas_types']:
-        pathlib.Path(f'{output_files.offscreen}.w.html').write_text(
-            _render(jinja_env, 'reftest_worker.html', params), 'utf-8')
+    def _write_reference_test(self, jinja_env: jinja2.Environment,
+                              output_files: _OutputPaths):
+        params = dict(self.params)
+        if _CanvasType.HTML_CANVAS in params['canvas_types']:
+            pathlib.Path(f'{output_files.element}.html').write_text(
+                _render(jinja_env, 'reftest_element.html', params), 'utf-8')
+        if _CanvasType.OFFSCREEN_CANVAS in params['canvas_types']:
+            pathlib.Path(f'{output_files.offscreen}.html').write_text(
+                _render(jinja_env, 'reftest_offscreen.html', params), 'utf-8')
+        if _CanvasType.WORKER in params['canvas_types']:
+            pathlib.Path(f'{output_files.offscreen}.w.html').write_text(
+                _render(jinja_env, 'reftest_worker.html', params), 'utf-8')
 
-    params = dict(params)
-    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']:
-        pathlib.Path(f'{output_files.element}-expected.html').write_text(
-            _render(jinja_env, ref_template, params), 'utf-8')
-    if {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER
-        } & params['canvas_types']:
-        pathlib.Path(f'{output_files.offscreen}-expected.html').write_text(
-            _render(jinja_env, ref_template, params), 'utf-8')
+        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']:
+            pathlib.Path(f'{output_files.element}-expected.html').write_text(
+                _render(jinja_env, ref_template, params), 'utf-8')
+        if {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER
+            } & params['canvas_types']:
+            pathlib.Path(f'{output_files.offscreen}-expected.html').write_text(
+                _render(jinja_env, ref_template, params), 'utf-8')
 
+    def _write_testharness_test(self, jinja_env: jinja2.Environment,
+                                output_files: _OutputPaths):
+        # Create test cases for canvas and offscreencanvas.
+        if _CanvasType.HTML_CANVAS in self.params['canvas_types']:
+            pathlib.Path(f'{output_files.element}.html').write_text(
+                _render(jinja_env, 'testharness_element.html', self.params),
+                'utf-8')
 
-def _write_testharness_test(jinja_env: jinja2.Environment, params: _TestParams,
-                            output_files: _OutputPaths):
-    # Create test cases for canvas and offscreencanvas.
-    if _CanvasType.HTML_CANVAS in params['canvas_types']:
-        pathlib.Path(f'{output_files.element}.html').write_text(
-            _render(jinja_env, 'testharness_element.html', params), 'utf-8')
+        if _CanvasType.OFFSCREEN_CANVAS in self.params['canvas_types']:
+            pathlib.Path(f'{output_files.offscreen}.html').write_text(
+                _render(jinja_env, 'testharness_offscreen.html', self.params),
+                'utf-8')
 
-    if _CanvasType.OFFSCREEN_CANVAS in params['canvas_types']:
-        pathlib.Path(f'{output_files.offscreen}.html').write_text(
-            _render(jinja_env, 'testharness_offscreen.html', params), 'utf-8')
+        if _CanvasType.WORKER in self.params['canvas_types']:
+            pathlib.Path(f'{output_files.offscreen}.worker.js').write_text(
+                _render(jinja_env, 'testharness_worker.js', self.params),
+                'utf-8')
 
-    if _CanvasType.WORKER in params['canvas_types']:
-        pathlib.Path(f'{output_files.offscreen}.worker.js').write_text(
-            _render(jinja_env, 'testharness_worker.js', params), 'utf-8')
+    def generate_expected_image(self, output_dirs: _OutputPaths) -> None:
+        """Creates a reference image using Cairo and save filename in params."""
+        if 'expected' not in self.params:
+            return
 
+        expected = self.params['expected']
+        name = self.params['name']
 
-def _generate_expected_image(params: _MutableTestParams,
-                             output_dirs: _OutputPaths) -> None:
-    """Creates a reference image using Cairo and save filename in params."""
-    if 'expected' not in params:
-        return
+        if expected == 'green':
+            self._params['expected_img'] = '/images/green-100x50.png'
+            return
+        if expected == 'clear':
+            self._params['expected_img'] = '/images/clear-100x50.png'
+            return
+        if ';' in expected:
+            print(f'Found semicolon in {name}')
+        expected = re.sub(
+            r'^size (\d+) (\d+)',
+            r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)'
+            r'\ncr = cairo.Context(surface)', expected)
 
-    expected = params['expected']
-    name = params['name']
+        output_paths = output_dirs.sub_path(name)
+        if _CanvasType.HTML_CANVAS in self.params['canvas_types']:
+            expected_canvas = (
+                f'{expected}\n'
+                f'surface.write_to_png("{output_paths.element}.png")\n')
+            eval(compile(expected_canvas, f'<test {name}>', 'exec'), {},
+                 {'cairo': cairo})
 
-    if expected == 'green':
-        params['expected_img'] = '/images/green-100x50.png'
-        return
-    if expected == 'clear':
-        params['expected_img'] = '/images/clear-100x50.png'
-        return
-    if ';' in expected:
-        print(f'Found semicolon in {name}')
-    expected = re.sub(
-        r'^size (\d+) (\d+)',
-        r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)'
-        r'\ncr = cairo.Context(surface)', expected)
+        if {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER
+            } & self.params['canvas_types']:
+            expected_offscreen = (
+                f'{expected}\n'
+                f'surface.write_to_png("{output_paths.offscreen}.png")\n')
+            eval(compile(expected_offscreen, f'<test {name}>', 'exec'), {},
+                 {'cairo': cairo})
 
-    output_paths = output_dirs.sub_path(name)
-    if _CanvasType.HTML_CANVAS in params['canvas_types']:
-        expected_canvas = (
-            f'{expected}\n'
-            f'surface.write_to_png("{output_paths.element}.png")\n')
-        eval(compile(expected_canvas, f'<test {name}>', 'exec'), {},
-             {'cairo': cairo})
+        self._params['expected_img'] = f'{name}.png'
 
-    if {_CanvasType.OFFSCREEN_CANVAS, _CanvasType.WORKER
-        } & params['canvas_types']:
-        expected_offscreen = (
-            f'{expected}\n'
-            f'surface.write_to_png("{output_paths.offscreen}.png")\n')
-        eval(compile(expected_offscreen, f'<test {name}>', 'exec'), {},
-             {'cairo': cairo})
+    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'])
 
-    params['expected_img'] = f'{name}.png'
-
-
-def _generate_test(params: _TestParams, jinja_env: jinja2.Environment,
-                   output_dirs: _OutputPaths) -> None:
-    output_files = output_dirs.sub_path(params['file_name'])
-    if params['template_type'] in (_TemplateType.REFERENCE,
-                                   _TemplateType.HTML_REFERENCE):
-        _write_reference_test(jinja_env, params, output_files)
-    else:
-        _write_testharness_test(jinja_env, params, output_files)
+        if self.params['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[_TestParams]):
+                                     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)
@@ -512,7 +521,7 @@
         # 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(test)
+        test_variants.append(_Variant.create_with_defaults(test))
     else:
         # Continue the recursion with each possible selection for the current
         # variant.
@@ -524,7 +533,7 @@
             current_selection.pop()
 
 
-def _get_variants(test: _TestParams) -> List[_MutableTestParams]:
+def _get_variants(test: _TestParams) -> List[_Variant]:
     current_selection = []
     test_variants = []
     variants = test.get('variants', [])
@@ -609,18 +618,18 @@
     for test in tests:
         print(test['name'])
         _validate_test(test)
-        test = _add_default_params(test)
         for variant in _get_variants(test):
-            _finalize_params(jinja_env, variant)
-            if test['name'] != variant['name']:
-                print(f'  {variant["name"]}')
+            variant.finalize_params(jinja_env)
+            if test['name'] != variant.params['name']:
+                print(f'  {variant.params["name"]}')
 
-            sub_dir = _get_test_sub_dir(variant['file_name'], name_to_sub_dir)
+            sub_dir = _get_test_sub_dir(variant.params['file_name'],
+                                        name_to_sub_dir)
             output_sub_dirs = output_dirs.sub_path(sub_dir)
 
-            _check_uniqueness(used_tests, variant['name'],
-                              variant['canvas_types'])
-            _generate_expected_image(variant, output_sub_dirs)
-            _generate_test(variant, jinja_env, output_sub_dirs)
+            _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)
 
     print()