summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py')
-rw-r--r--testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py617
1 files changed, 617 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py b/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
new file mode 100644
index 0000000000..bf5fdeee50
--- /dev/null
+++ b/testing/web-platform/tests/html/canvas/tools/gentestutilsunion.py
@@ -0,0 +1,617 @@
+# 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 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, List, Mapping, MutableMapping, Optional, Tuple
+
+import re
+import collections
+import dataclasses
+import enum
+import importlib
+import itertools
+import os
+import pathlib
+import sys
+import textwrap
+
+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 _simpleEscapeJS(string: str) -> str:
+ return string.replace('\\', '\\\\').replace('"', '\\"')
+
+
+def _escapeJS(string: str) -> str:
+ string = _simpleEscapeJS(string)
+ # Kind of an ugly hack, for nicer failure-message output.
+ string = re.sub(r'\[(\w+)\]', r'[\\""+(\1)+"\\"]', string)
+ return string
+
+
+def _unroll(text: str) -> str:
+ """Unrolls text with all possible permutations of the parameter lists.
+
+ Example:
+ >>> print _unroll('f = {<a | b>: <1 | 2 | 3>};')
+ // a
+ f = {a: 1};
+ f = {a: 2};
+ f = {a: 3};
+ // b
+ f = {b: 1};
+ f = {b: 2};
+ f = {b: 3};
+ """
+ patterns = [] # type: List[Tuple[str, List[str]]]
+ while match := re.search(r'<([^>]+)>', text):
+ key = f'@unroll_pattern_{len(patterns)}'
+ values = text[match.start(1):match.end(1)]
+ text = text[:match.start(0)] + key + text[match.end(0):]
+ patterns.append((key, [value.strip() for value in values.split('|')]))
+
+ def unroll_patterns(text: str,
+ patterns: List[Tuple[str, List[str]]],
+ label: Optional[str] = None) -> List[str]:
+ if not patterns:
+ return [text]
+ patterns = patterns.copy()
+ key, values = patterns.pop(0)
+ return (['// ' + label] if label else []) + list(
+ itertools.chain.from_iterable(
+ unroll_patterns(text.replace(key, value), patterns, value)
+ for value in values))
+
+ result = '\n'.join(unroll_patterns(text, patterns))
+ return result
+
+
+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 in range(len(args)):
+ for a in args[i][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)
+
+ return '\n'.join('%s(%s)%s' % (method, ', '.join(c), tail) for c in 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(
+ 'Test "%s" has no defined target directory mapping' % name)
+
+
+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)
+
+ # Unroll expressions with a cross-product-style parameter expansion.
+ code = re.sub(r'@unroll ([^;]*;)', lambda m: _unroll(m.group(1)), 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)
+
+ code = re.sub(r'@assert throws (\S+Error) (.*);',
+ r'assert_throws_js(\1, function() { \2; });', code)
+
+ code = re.sub(
+ r'@assert (.*) === (.*);', lambda m: '_assertSame(%s, %s, "%s", "%s");'
+ % (m.group(1), m.group(2), _escapeJS(m.group(1)), _escapeJS(m.group(2))
+ ), code)
+
+ code = re.sub(
+ r'@assert (.*) !== (.*);', lambda m:
+ '_assertDifferent(%s, %s, "%s", "%s");' % (m.group(1), m.group(
+ 2), _escapeJS(m.group(1)), _escapeJS(m.group(2))), code)
+
+ code = re.sub(
+ r'@assert (.*) =~ (.*);', lambda m: 'assert_regexp_match(%s, %s);' % (
+ m.group(1), m.group(2)), code)
+
+ code = re.sub(
+ r'@assert (.*);', lambda m: '_assert(%s, "%s");' % (m.group(
+ 1), _escapeJS(m.group(1))), code)
+
+ code = re.sub(r' @moz-todo', '', code)
+
+ code = re.sub(r'@moz-UniversalBrowserRead;', '', code)
+
+ assert ('@' not in code)
+
+ return code
+
+
+class CanvasType(str, enum.Enum):
+ HTML_CANVAS = 'htmlcanvas'
+ OFFSCREEN_CANVAS = 'offscreencanvas'
+
+
+def _get_enabled_canvas_types(test: Mapping[str, Any]) -> List[CanvasType]:
+ return [CanvasType(t.lower()) for t in test.get('canvasType', CanvasType)]
+
+
+@dataclasses.dataclass
+class TestConfig:
+ out_dir: str
+ image_out_dir: str
+ enabled: bool
+
+
+_CANVAS_SIZE_REGEX = re.compile(r'(?P<width>.*), (?P<height>.*)',
+ re.MULTILINE | re.DOTALL)
+
+
+def _get_canvas_size(test: Mapping[str, Any]):
+ size = test.get('size', '100, 50')
+ match = _CANVAS_SIZE_REGEX.match(size)
+ if not match:
+ raise InvalidTestDefinitionError(
+ 'Invalid canvas size "%s" in test %s. Expected a string matching '
+ 'this pattern: "%%s, %%s" %% (width, height)' %
+ (size, test['name']))
+ return match.group('width'), match.group('height')
+
+
+def _write_reference_test(is_js_ref: bool, templates: Mapping[str, str],
+ template_params: MutableMapping[str, str],
+ ref_code: str, canvas_path: Optional[str],
+ offscreen_path: Optional[str]):
+ ref_code = ref_code.strip()
+ ref_code = textwrap.indent(ref_code, ' ') if is_js_ref else ref_code
+ ref_template_name = 'element_ref_test' if is_js_ref else 'html_ref_test'
+
+ code = template_params['code']
+ template_params['code'] = textwrap.indent(code, ' ')
+ if canvas_path:
+ pathlib.Path(f'{canvas_path}.html').write_text(
+ templates['element_ref_test'] % template_params, 'utf-8')
+ if offscreen_path:
+ pathlib.Path(f'{offscreen_path}.html').write_text(
+ templates['offscreen_ref_test'] % template_params, 'utf-8')
+ template_params['code'] = textwrap.indent(code, ' ')
+ pathlib.Path(f'{offscreen_path}.w.html').write_text(
+ templates['worker_ref_test'] % template_params, 'utf-8')
+
+ template_params['code'] = ref_code
+ template_params['links'] = ''
+ template_params['fuzzy'] = ''
+ if canvas_path:
+ pathlib.Path(f'{canvas_path}-expected.html').write_text(
+ templates[ref_template_name] % template_params, 'utf-8')
+ if offscreen_path:
+ pathlib.Path(f'{offscreen_path}-expected.html').write_text(
+ templates[ref_template_name] % template_params, 'utf-8')
+
+
+def _write_testharness_test(templates: Mapping[str, str],
+ template_params: MutableMapping[str, str],
+ canvas_path: Optional[str],
+ offscreen_path: Optional[str]):
+ # Create test cases for canvas and offscreencanvas.
+ code = template_params['code']
+ template_params['code'] = textwrap.indent(code, ' ')
+ if canvas_path:
+ pathlib.Path(f'{canvas_path}.html').write_text(
+ templates['element'] % template_params, 'utf-8')
+
+ if offscreen_path:
+ offscreen_template = templates['offscreen']
+ worker_template = templates['worker']
+
+ if ('then(t_pass, t_fail);' in code):
+ offscreen_template = offscreen_template.replace('t.done();\n', '')
+ worker_template = worker_template.replace('t.done();\n', '')
+
+ pathlib.Path(f'{offscreen_path}.html').write_text(
+ offscreen_template % template_params, 'utf-8')
+ pathlib.Path(f'{offscreen_path}.worker.js').write_text(
+ worker_template % template_params, 'utf-8')
+
+
+def _expand_template(template: str, template_params: Mapping[str, str]) -> str:
+ # Remove whole line comments.
+ template = re.sub(r'^ *#.*?\n', '', template, flags=re.MULTILINE)
+ # Remove trailing line comments.
+ template = re.sub(r' *#.*?$', '', template, flags=re.MULTILINE)
+
+ # Unwrap lines ending with a backslash.
+ template = _remove_extra_newlines(template)
+
+ content_without_nested_if = r'((?:(?!{%\s*(?:if|else|endif)[^%]*%}).)*?)'
+
+ # Resolve {% if <cond> %}<content>{% else %}<alternate content>{% endif %}
+ if_else_regex = re.compile(
+ r'{%\s*if\s*([^\s%]+)\s*%}' + # {% if <cond> %}
+ content_without_nested_if + # content
+ r'{%\s*else\s*%}' + # {% else %}
+ content_without_nested_if + # alternate
+ r'{%\s*endif\s*%}', # {% endif %}
+ flags=re.MULTILINE | re.DOTALL)
+ while match := if_else_regex.search(template):
+ condition, content, alternate = match.groups()
+ substitution = content if template_params[condition] else alternate
+ template = (
+ template[:match.start(0)] + substitution + template[match.end(0):])
+
+ # Resolve {% if <cond> %}<content>{% endif %}
+ if_regex = re.compile(
+ r'{%\s*if\s*([^\s%]+)\s*%}' + # {% if <cond> %}
+ content_without_nested_if + # content
+ r'{%\s*endif\s*%}', # {% endif %}
+ flags=re.MULTILINE | re.DOTALL)
+ while match := if_regex.search(template):
+ condition, content = match.groups()
+ substitution = content if template_params[condition] else ''
+ template = (
+ template[:match.start(0)] + substitution + template[match.end(0):])
+
+ return template
+
+
+def _expand_templates(templates: Mapping[str, str],
+ params: Mapping[str, str]) -> Mapping[str, str]:
+ return {
+ name: _expand_template(template, params)
+ for name, template in templates.items()
+ }
+
+
+def _generate_test(test: Mapping[str, Any], templates: Mapping[str, str],
+ sub_dir: str, html_canvas_cfg: TestConfig,
+ offscreen_canvas_cfg: TestConfig) -> None:
+ name = test['name']
+
+ if test.get('expected', '') == 'green' and re.search(
+ r'@assert pixel .* 0,0,0,0;', test['code']):
+ print('Probable incorrect pixel test in %s' % name)
+
+ code_canvas = _expand_test_code(test['code']).strip()
+
+ expectation_html = ''
+ if 'expected' in test and test['expected'] is not None:
+ expected = test['expected']
+ expected_img = None
+ if expected == 'green':
+ expected_img = '/images/green-100x50.png'
+ elif expected == 'clear':
+ expected_img = '/images/clear-100x50.png'
+ else:
+ if ';' in expected:
+ print('Found semicolon in %s' % name)
+ expected = re.sub(
+ r'^size (\d+) (\d+)',
+ r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)'
+ r'\ncr = cairo.Context(surface)', expected)
+
+ expected_canvas = (
+ expected + "\nsurface.write_to_png('%s.png')\n" %
+ os.path.join(html_canvas_cfg.image_out_dir, sub_dir, name))
+ eval(compile(expected_canvas, '<test %s>' % name, 'exec'), {},
+ {'cairo': cairo})
+
+ expected_offscreencanvas = (
+ expected + "\nsurface.write_to_png('%s.png')\n" % os.path.join(
+ offscreen_canvas_cfg.image_out_dir, sub_dir, name))
+ eval(compile(expected_offscreencanvas, '<test %s>' % name, 'exec'),
+ {}, {'cairo': cairo})
+
+ expected_img = '%s.png' % name
+
+ if expected_img:
+ expectation_html = (
+ '<p class="output expectedtext">Expected output:<p>'
+ '<img src="%s" class="output expected" id="expected" '
+ 'alt="">' % expected_img)
+
+ canvas = ' ' + test['canvas'] if 'canvas' in test else ''
+ width, height = _get_canvas_size(test)
+
+ notes = '<p class="notes">%s' % test['notes'] if 'notes' in test else ''
+
+ links = f'<link rel="match" href="{name}-expected.html">\n'
+ fuzzy = ('<meta name=fuzzy content="%s">\n' %
+ test['fuzzy'] if 'fuzzy' in test else '')
+ timeout = ('<meta name="timeout" content="%s">\n' %
+ test['timeout'] if 'timeout' in test else '')
+ timeout_js = ('// META: timeout=%s\n' % test['timeout']
+ if 'timeout' in test else '')
+
+ images = ''
+ for src in test.get('images', []):
+ img_id = src.split('/')[-1]
+ if '/' not in src:
+ src = '../images/%s' % src
+ images += '<img src="%s" id="%s" class="resource">\n' % (src, img_id)
+ for src in test.get('svgimages', []):
+ img_id = src.split('/')[-1]
+ if '/' not in src:
+ src = '../images/%s' % src
+ images += ('<svg><image xlink:href="%s" id="%s" class="resource">'
+ '</svg>\n' % (src, img_id))
+ images = images.replace('../images/', '/images/')
+
+ fonts = ''
+ fonthack = ''
+ for font in test.get('fonts', []):
+ fonts += ('@font-face {\n font-family: %s;\n'
+ ' src: url("/fonts/%s.ttf");\n}\n' % (font, font))
+ # Browsers require the font to actually be used in the page.
+ if test.get('fonthack', 1):
+ fonthack += ('<span style="font-family: %s; position: '
+ 'absolute; visibility: hidden">A</span>\n' % font)
+ if fonts:
+ fonts = '<style>\n%s</style>\n' % fonts
+
+ fallback = test.get('fallback',
+ '<p class="fallback">FAIL (fallback content)</p>')
+
+ desc = test.get('desc', '')
+ escaped_desc = _simpleEscapeJS(desc)
+
+ attributes = test.get('attributes', '')
+ if attributes:
+ context_args = "'2d', %s" % attributes.strip()
+ attributes = ', ' + attributes.strip()
+ else:
+ context_args = "'2d'"
+
+ is_promise_test = False
+ if 'test_type' in test:
+ if test['test_type'] == 'promise':
+ is_promise_test = True
+ else:
+ raise InvalidTestDefinitionError(
+ f'Test {name}\' test_type is invalid, it only accepts '
+ '"promise" now for creating promise test type in the template '
+ 'file.')
+
+ template_params = {
+ 'name': name,
+ 'desc': desc,
+ 'escaped_desc': escaped_desc,
+ 'notes': notes,
+ 'images': images,
+ 'fonts': fonts,
+ 'fonthack': fonthack,
+ 'timeout': timeout,
+ 'timeout_js': timeout_js,
+ 'fuzzy': fuzzy,
+ 'links': links,
+ 'canvas': canvas,
+ 'width': width,
+ 'height': height,
+ 'expected': expectation_html,
+ 'code': code_canvas,
+ 'fallback': fallback,
+ 'attributes': attributes,
+ 'context_args': context_args,
+ 'promise_test': is_promise_test
+ }
+
+ canvas_path = os.path.join(html_canvas_cfg.out_dir, sub_dir, name)
+ offscreen_path = os.path.join(offscreen_canvas_cfg.out_dir, sub_dir, name)
+ if 'manual' in test:
+ canvas_path += '-manual'
+ offscreen_path += '-manual'
+
+ js_reference = test.get('reference')
+ html_reference = test.get('html_reference')
+ if js_reference is not None and html_reference is not None:
+ raise InvalidTestDefinitionError(
+ f'Test {name} is invalid, "reference" and "html_reference" can\'t '
+ 'both be specified at the same time.')
+
+ templates = _expand_templates(templates, template_params)
+
+ ref_code = js_reference or html_reference
+ if ref_code is not None:
+ _write_reference_test(
+ js_reference is not None, templates, template_params, ref_code,
+ canvas_path if html_canvas_cfg.enabled else None,
+ offscreen_path if offscreen_canvas_cfg.enabled else None)
+ else:
+ _write_testharness_test(
+ templates, template_params,
+ canvas_path if html_canvas_cfg.enabled else None,
+ offscreen_path if offscreen_canvas_cfg.enabled else None)
+
+
+def genTestUtils_union(TEMPLATEFILE: str, NAME2DIRFILE: str) -> None:
+ CANVASOUTPUTDIR = '../element'
+ CANVASIMAGEOUTPUTDIR = '../element'
+ OFFSCREENCANVASOUTPUTDIR = '../offscreen'
+ OFFSCREENCANVASIMAGEOUTPUTDIR = '../offscreen'
+
+ # 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()
+
+ templates = yaml.safe_load(pathlib.Path(TEMPLATEFILE).read_text())
+ name_to_sub_dir = yaml.safe_load(pathlib.Path(NAME2DIRFILE).read_text())
+
+ tests = []
+ test_yaml_directory = 'yaml-new'
+ TESTSFILES = [
+ 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()) for f in TESTSFILES], []):
+ if 'DISABLED' in t:
+ continue
+ if 'meta' in t:
+ eval(compile(t['meta'], '<meta test>', 'exec'), {},
+ {'tests': tests})
+ else:
+ tests.append(t)
+
+ # Ensure the test output directories exist.
+ testdirs = [
+ CANVASOUTPUTDIR, OFFSCREENCANVASOUTPUTDIR, CANVASIMAGEOUTPUTDIR,
+ OFFSCREENCANVASIMAGEOUTPUTDIR
+ ]
+ for sub_dir in set(name_to_sub_dir.values()):
+ testdirs.append('%s/%s' % (CANVASOUTPUTDIR, sub_dir))
+ testdirs.append('%s/%s' % (OFFSCREENCANVASOUTPUTDIR, sub_dir))
+ for d in testdirs:
+ try:
+ os.mkdir(d)
+ except FileExistsError:
+ pass # Ignore if it already exists,
+
+ used_tests = collections.defaultdict(set)
+ for original_test in tests:
+ variants = original_test.get('variants', {'': dict()})
+ for variant_name, variant_params in variants.items():
+ test = original_test.copy()
+ if variant_name or variant_params:
+ test['name'] += '.' + variant_name
+ test['code'] = test['code'] % variant_params
+ if 'reference' in test:
+ test['reference'] = test['reference'] % variant_params
+ if 'html_reference' in test:
+ test['html_reference'] = (
+ test['html_reference'] % variant_params)
+ test.update(variant_params)
+
+ name = test['name']
+ print('\r(%s)' % name, ' ' * 32, '\t')
+
+ enabled_canvas_types = _get_enabled_canvas_types(test)
+
+ already_tested = used_tests[name].intersection(
+ enabled_canvas_types)
+ if already_tested:
+ raise InvalidTestDefinitionError(
+ f'Test {name} is defined twice for types {already_tested}')
+ used_tests[name].update(enabled_canvas_types)
+
+ sub_dir = _get_test_sub_dir(name, name_to_sub_dir)
+ _generate_test(
+ test,
+ templates,
+ sub_dir,
+ html_canvas_cfg=TestConfig(
+ out_dir=CANVASOUTPUTDIR,
+ image_out_dir=CANVASIMAGEOUTPUTDIR,
+ enabled=CanvasType.HTML_CANVAS in enabled_canvas_types),
+ offscreen_canvas_cfg=TestConfig(
+ out_dir=OFFSCREENCANVASOUTPUTDIR,
+ image_out_dir=OFFSCREENCANVASIMAGEOUTPUTDIR,
+ enabled=CanvasType.OFFSCREEN_CANVAS in
+ enabled_canvas_types))
+
+ print()