# 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 __future__ import print_function import re import codecs import time import os import shutil import sys import xml.dom.minidom from xml.dom.minidom import Node try: import cairocffi as cairo except ImportError: import cairo try: import syck as yaml # compatible and lots faster except ImportError: import yaml def genTestUtils(TESTOUTPUTDIR, IMAGEOUTPUTDIR, TEMPLATEFILE, NAME2DIRFILE, ISOFFSCREENCANVAS): MISCOUTPUTDIR = './output' def simpleEscapeJS(str): return str.replace('\\', '\\\\').replace('"', '\\"') def escapeJS(str): str = simpleEscapeJS(str) str = re.sub(r'\[(\w+)\]', r'[\\""+(\1)+"\\"]', str) # kind of an ugly hack, for nicer failure-message output return str def escapeHTML(str): return str.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') def expand_nonfinite(method, argstr, tail): """ >>> 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 ", ..." (where usually # 'invalid' is Infinity/-Infinity/NaN) args = [] for arg in argstr.split(', '): a = re.match('<(.*)>', arg).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, start, depth): 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) # Run with --test argument to run unit tests if len(sys.argv) > 1 and sys.argv[1] == '--test': import doctest doctest.testmod() sys.exit() templates = yaml.safe_load(open(TEMPLATEFILE, "r").read()) name_mapping = yaml.safe_load(open(NAME2DIRFILE, "r").read()) tests = [] test_yaml_directory = "yaml/element" if ISOFFSCREENCANVAS: test_yaml_directory = "yaml/offscreen" 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(open(f, "r").read()) for f in TESTSFILES], []): if 'DISABLED' in t: continue if 'meta' in t: eval(compile(t['meta'], '', 'exec'), {}, {'tests':tests}) else: tests.append(t) category_names = [] category_contents_direct = {} category_contents_all = {} def backref_html(name): backrefs = [] c = '' for p in name.split('.')[:-1]: c += '.'+p backrefs.append('%s.' % (c, p)) backrefs.append(name.split('.')[-1]) return ''.join(backrefs) def make_flat_image(filename, w, h, r,g,b,a): if os.path.exists('%s/%s' % (IMAGEOUTPUTDIR, filename)): return filename surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) cr = cairo.Context(surface) cr.set_source_rgba(r, g, b, a) cr.rectangle(0, 0, w, h) cr.fill() surface.write_to_png('%s/%s' % (IMAGEOUTPUTDIR, filename)) return filename # Ensure the test output directories exist testdirs = [TESTOUTPUTDIR, IMAGEOUTPUTDIR, MISCOUTPUTDIR] for map_dir in set(name_mapping.values()): testdirs.append("%s/%s" % (TESTOUTPUTDIR, map_dir)) for d in testdirs: try: os.mkdir(d) except: pass # ignore if it already exists used_images = {} def map_name(name): mapped_name = None for mn in sorted(name_mapping.keys(), key=len, reverse=True): if name.startswith(mn): mapped_name = "%s/%s" % (name_mapping[mn], name) break if not mapped_name: print("LIKELY ERROR: %s has no defined target directory mapping" % name) if 'manual' in test: mapped_name += "-manual" return mapped_name def expand_test_code(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 used_tests = {} for i in range(len(tests)): test = tests[i] name = test['name'] print("\r(%s)" % name, " "*32, "\t") if name in used_tests: print("Test %s is defined twice" % name) used_tests[name] = 1 mapped_name = map_name(name) if not mapped_name: if ISOFFSCREENCANVAS: continue else: mapped_name = name cat_total = '' for cat_part in [''] + name.split('.')[:-1]: cat_total += cat_part+'.' if not cat_total in category_names: category_names.append(cat_total) category_contents_all.setdefault(cat_total, []).append(name) category_contents_direct.setdefault(cat_total, []).append(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 = expand_test_code(test['code']) 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)\ncr = cairo.Context(surface)', expected) if mapped_name.endswith("-manual"): png_name = mapped_name[:-len("-manual")] else: png_name = mapped_name expected += "\nsurface.write_to_png('%s/%s.png')\n" % (IMAGEOUTPUTDIR, png_name) eval(compile(expected, '' % test['name'], 'exec'), {}, {'cairo':cairo}) expected_img = "%s.png" % name if expected_img: expectation_html = ('

Expected output:' + '

' % (expected_img)) canvas = test.get('canvas', 'width="100" height="50"') prev = tests[i-1]['name'] if i != 0 else 'index' next = tests[i+1]['name'] if i != len(tests)-1 else 'index' name_wrapped = name.replace('.', '.​') notes = '

%s' % test['notes'] if 'notes' in test else '' timeout = '\n' % test['timeout'] if 'timeout' in test else '' scripts = '' for s in test.get('scripts', []): scripts += '\n' % (s) variants = test.get('script-variants', {}) script_variants = [(v, '\n' % (s)) for (v, s) in variants.items()] if not script_variants: script_variants = [('', '')] images = '' for i in test.get('images', []): id = i.split('/')[-1] if '/' not in i: used_images[i] = 1 i = '../images/%s' % i images += '\n' % (i,id) for i in test.get('svgimages', []): id = i.split('/')[-1] if '/' not in i: used_images[i] = 1 i = '../images/%s' % i images += '\n' % (i,id) images = images.replace("../images/", "/images/") fonts = '' fonthack = '' for i in test.get('fonts', []): fonts += '@font-face {\n font-family: %s;\n src: url("/fonts/%s.ttf");\n}\n' % (i, i) # Browsers require the font to actually be used in the page if test.get('fonthack', 1): fonthack += 'A\n' % i if fonts: fonts = '\n' % fonts fallback = test.get('fallback', '

FAIL (fallback content)

') 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'" for (variant, extra_script) in script_variants: name_variant = '' if not variant else '.' + variant template_params = { 'name':name + name_variant, 'name_wrapped':name_wrapped, 'backrefs':backref_html(name), 'mapped_name':mapped_name, 'desc':desc, 'escaped_desc':escaped_desc, 'prev':prev, 'next':next, 'notes':notes, 'images':images, 'fonts':fonts, 'fonthack':fonthack, 'timeout': timeout, 'canvas':canvas, 'expected':expectation_html, 'code':code, 'scripts':scripts + extra_script, 'fallback':fallback, 'attributes':attributes, 'context_args': context_args } if ISOFFSCREENCANVAS: f = codecs.open('%s/%s%s.html' % (TESTOUTPUTDIR, mapped_name, name_variant), 'w', 'utf-8') f.write(templates['w3coffscreencanvas'] % template_params) timeout = '// META: timeout=%s\n' % test['timeout'] if 'timeout' in test else '' template_params['timeout'] = timeout f = codecs.open('%s/%s%s.worker.js' % (TESTOUTPUTDIR, mapped_name, name_variant), 'w', 'utf-8') f.write(templates['w3cworker'] % template_params) else: f = codecs.open('%s/%s%s.html' % (TESTOUTPUTDIR, mapped_name, name_variant), 'w', 'utf-8') f.write(templates['w3ccanvas'] % template_params) print()