1# Copyright (c) 2010 Philip Taylor 2# Released under the BSD license and W3C Test Suite License: see LICENSE.txt 3 4# Current code status: 5# 6# This was originally written for use at 7# http://philip.html5.org/tests/canvas/suite/tests/ 8# 9# It has been adapted for use with the Web Platform Test Suite suite at 10# https://github.com/w3c/web-platform-tests/ 11# 12# The W3C version excludes a number of features (multiple versions of each test 13# case of varying verbosity, Mozilla mochitests, semi-automated test harness) 14# to focus on simply providing reviewable test cases. It also expects a different 15# directory structure. 16# This code attempts to support both versions, but the non-W3C version hasn't 17# been tested recently and is probably broken. 18 19# To update or add test cases: 20# 21# * Modify the tests*.yaml files. 22# 'name' is an arbitrary hierarchical name to help categorise tests. 23# 'desc' is a rough description of what behaviour the test aims to test. 24# 'testing' is a list of references to spec.yaml, to show which spec sentences 25# this test case is primarily testing. 26# 'code' is JavaScript code to execute, with some special commands starting with '@' 27# 'expected' is what the final canvas output should be: a string 'green' or 'clear' 28# (100x50 images in both cases), or a string 'size 100 50' (or any other size) 29# followed by Python code using Pycairo to generate the image. 30# 31# * Run "python gentest.py". 32# This requires a few Python modules which might not be ubiquitous. 33# It has only been tested on Linux. 34# It will usually emit some warnings, which ideally should be fixed but can 35# generally be safely ignored. 36# 37# * Test the tests, add new ones to Git, remove deleted ones from Git, etc. 38 39import re 40import codecs 41import time 42import os 43import shutil 44import sys 45import xml.dom.minidom 46from xml.dom.minidom import Node 47 48import cairo 49 50try: 51 import syck as yaml # compatible and lots faster 52except ImportError: 53 import yaml 54 55# Default mode is for the W3C test suite; the --standalone option 56# generates various extra files that aren't needed there 57W3CMODE = True 58if '--standalone' in sys.argv: 59 W3CMODE = False 60 61TESTOUTPUTDIR = '../../2dcontext' 62IMAGEOUTPUTDIR = '../../2dcontext' 63MISCOUTPUTDIR = './output' 64SPECOUTPUTDIR = '../../annotated-spec' 65 66SPECOUTPUTPATH = '../annotated-spec' # relative to TESTOUTPUTDIR 67 68def simpleEscapeJS(str): 69 return str.replace('\\', '\\\\').replace('"', '\\"') 70 71def escapeJS(str): 72 str = simpleEscapeJS(str) 73 str = re.sub(r'\[(\w+)\]', r'[\\""+(\1)+"\\"]', str) # kind of an ugly hack, for nicer failure-message output 74 return str 75 76def escapeHTML(str): 77 return str.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') 78 79def expand_nonfinite(method, argstr, tail): 80 """ 81 >>> print expand_nonfinite('f', '<0 a>, <0 b>', ';') 82 f(a, 0); 83 f(0, b); 84 f(a, b); 85 >>> print expand_nonfinite('f', '<0 a>, <0 b c>, <0 d>', ';') 86 f(a, 0, 0); 87 f(0, b, 0); 88 f(0, c, 0); 89 f(0, 0, d); 90 f(a, b, 0); 91 f(a, b, d); 92 f(a, 0, d); 93 f(0, b, d); 94 """ 95 # argstr is "<valid-1 invalid1-1 invalid2-1 ...>, ..." (where usually 96 # 'invalid' is Infinity/-Infinity/NaN) 97 args = [] 98 for arg in argstr.split(', '): 99 a = re.match('<(.*)>', arg).group(1) 100 args.append(a.split(' ')) 101 calls = [] 102 # Start with the valid argument list 103 call = [ args[j][0] for j in range(len(args)) ] 104 # For each argument alone, try setting it to all its invalid values: 105 for i in range(len(args)): 106 for a in args[i][1:]: 107 c2 = call[:] 108 c2[i] = a 109 calls.append(c2) 110 # For all combinations of >= 2 arguments, try setting them to their 111 # first invalid values. (Don't do all invalid values, because the 112 # number of combinations explodes.) 113 def f(c, start, depth): 114 for i in range(start, len(args)): 115 if len(args[i]) > 1: 116 a = args[i][1] 117 c2 = c[:] 118 c2[i] = a 119 if depth > 0: calls.append(c2) 120 f(c2, i+1, depth+1) 121 f(call, 0, 0) 122 123 return '\n'.join('%s(%s)%s' % (method, ', '.join(c), tail) for c in calls) 124 125# Run with --test argument to run unit tests 126if len(sys.argv) > 1 and sys.argv[1] == '--test': 127 import doctest 128 doctest.testmod() 129 sys.exit() 130 131templates = yaml.load(open('templates.yaml', "r").read()) 132name_mapping = yaml.load(open('name2dir.yaml', "r").read()) 133 134spec_assertions = [] 135for s in yaml.load(open('spec.yaml', "r").read())['assertions']: 136 if 'meta' in s: 137 eval(compile(s['meta'], '<meta spec assertion>', 'exec'), {}, {'assertions':spec_assertions}) 138 else: 139 spec_assertions.append(s) 140 141tests = [] 142for t in sum([ yaml.load(open(f, "r").read()) for f in ['tests.yaml', 'tests2d.yaml', 'tests2dtext.yaml']], []): 143 if 'DISABLED' in t: 144 continue 145 if 'meta' in t: 146 eval(compile(t['meta'], '<meta test>', 'exec'), {}, {'tests':tests}) 147 else: 148 tests.append(t) 149 150category_names = [] 151category_contents_direct = {} 152category_contents_all = {} 153 154spec_ids = {} 155for t in spec_assertions: spec_ids[t['id']] = True 156spec_refs = {} 157 158def backref_html(name): 159 backrefs = [] 160 c = '' 161 for p in name.split('.')[:-1]: 162 c += '.'+p 163 backrefs.append('<a href="index%s.html">%s</a>.' % (c, p)) 164 backrefs.append(name.split('.')[-1]) 165 return ''.join(backrefs) 166 167def make_flat_image(filename, w, h, r,g,b,a): 168 if os.path.exists('%s/%s' % (IMAGEOUTPUTDIR, filename)): 169 return filename 170 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) 171 cr = cairo.Context(surface) 172 cr.set_source_rgba(r, g, b, a) 173 cr.rectangle(0, 0, w, h) 174 cr.fill() 175 surface.write_to_png('%s/%s' % (IMAGEOUTPUTDIR, filename)) 176 return filename 177 178# Ensure the test output directories exist 179testdirs = [TESTOUTPUTDIR, IMAGEOUTPUTDIR, MISCOUTPUTDIR] 180if not W3CMODE: testdirs.append('%s/mochitests' % MISCOUTPUTDIR) 181else: 182 for map_dir in set(name_mapping.values()): 183 testdirs.append("%s/%s" % (TESTOUTPUTDIR, map_dir)) 184for d in testdirs: 185 try: os.mkdir(d) 186 except: pass # ignore if it already exists 187 188mochitests = [] 189used_images = {} 190 191def expand_test_code(code): 192 code = re.sub(r'@nonfinite ([^(]+)\(([^)]+)\)(.*)', lambda m: expand_nonfinite(m.group(1), m.group(2), m.group(3)), code) # must come before '@assert throws' 193 194 code = re.sub(r'@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);', 195 r'_assertPixel(canvas, \1, \2, "\1", "\2");', 196 code) 197 198 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);', 199 r'_assertPixelApprox(canvas, \1, \2, "\1", "\2", 2);', 200 code) 201 202 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+/- (\d+);', 203 r'_assertPixelApprox(canvas, \1, \2, "\1", "\2", \3);', 204 code) 205 206 code = re.sub(r'@assert throws (\S+_ERR) (.*);', 207 r'assert_throws("\1", function() { \2; });', 208 code) 209 210 code = re.sub(r'@assert throws (\S+Error) (.*);', 211 r'assert_throws(new \1(), function() { \2; });', 212 code) 213 214 code = re.sub(r'@assert throws (.*);', 215 r'assert_throws(null, function() { \1; });', 216 code) 217 218 code = re.sub(r'@assert (.*) === (.*);', 219 lambda m: '_assertSame(%s, %s, "%s", "%s");' 220 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2))) 221 , code) 222 223 code = re.sub(r'@assert (.*) !== (.*);', 224 lambda m: '_assertDifferent(%s, %s, "%s", "%s");' 225 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2))) 226 , code) 227 228 code = re.sub(r'@assert (.*) =~ (.*);', 229 lambda m: 'assert_regexp_match(%s, %s);' 230 % (m.group(1), m.group(2)) 231 , code) 232 233 code = re.sub(r'@assert (.*);', 234 lambda m: '_assert(%s, "%s");' 235 % (m.group(1), escapeJS(m.group(1))) 236 , code) 237 238 code = re.sub(r' @moz-todo', '', code) 239 240 code = re.sub(r'@moz-UniversalBrowserRead;', 241 "" 242 , code) 243 244 assert('@' not in code) 245 246 return code 247 248def expand_mochitest_code(code): 249 code = re.sub(r'@nonfinite ([^(]+)\(([^)]+)\)(.*)', lambda m: expand_nonfinite(m.group(1), m.group(2), m.group(3)), code) 250 251 code = re.sub(r'@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);', 252 r'isPixel(ctx, \1, \2, "\1", "\2", 0);', 253 code) 254 255 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);', 256 r'isPixel(ctx, \1, \2, "\1", "\2", 2);', 257 code) 258 259 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+/- (\d+);', 260 r'isPixel(ctx, \1, \2, "\1", "\2", \3);', 261 code) 262 263 code = re.sub(r'@assert throws (\S+_ERR) (.*);', 264 lambda m: 'var _thrown = undefined; try {\n %s;\n} catch (e) { _thrown = e }; ok(_thrown && _thrown.code == DOMException.%s, "should throw %s");' 265 % (m.group(2), m.group(1), m.group(1)) 266 , code) 267 268 code = re.sub(r'@assert throws (\S+Error) (.*);', 269 lambda m: 'var _thrown = undefined; try {\n %s;\n} catch (e) { _thrown = e }; ok(_thrown && (_thrown instanceof %s), "should throw %s");' 270 % (m.group(2), m.group(1), m.group(1)) 271 , code) 272 273 code = re.sub(r'@assert throws (.*);', 274 lambda m: 'try { var _thrown = false;\n %s;\n} catch (e) { _thrown = true; } finally { ok(_thrown, "should throw exception"); }' 275 % (m.group(1)) 276 , code) 277 278 code = re.sub(r'@assert (.*) =~ (.*);', 279 lambda m: 'ok(%s.match(%s), "%s.match(%s)");' 280 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2))) 281 , code) 282 283 code = re.sub(r'@assert (.*);', 284 lambda m: 'ok(%s, "%s");' 285 % (m.group(1), escapeJS(m.group(1))) 286 , code) 287 288 code = re.sub(r'((?:^|\n|;)\s*)ok(.*;) @moz-todo', 289 lambda m: '%stodo%s' 290 % (m.group(1), m.group(2)) 291 , code) 292 293 code = re.sub(r'((?:^|\n|;)\s*)(is.*;) @moz-todo', 294 lambda m: '%stodo_%s' 295 % (m.group(1), m.group(2)) 296 , code) 297 298 code = re.sub(r'@moz-UniversalBrowserRead;', 299 "netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserRead');" 300 , code) 301 302 code = code.replace('../images/', 'image_') 303 304 assert '@' not in code, '@ not in code:\n%s' % code 305 306 return code 307 308used_tests = {} 309for i in range(len(tests)): 310 test = tests[i] 311 312 name = test['name'] 313 print "\r(%s)" % name, " "*32, "\t", 314 315 if name in used_tests: 316 print "Test %s is defined twice" % name 317 used_tests[name] = 1 318 319 mapped_name = None 320 for mn in sorted(name_mapping.keys(), key=len, reverse=True): 321 if name.startswith(mn): 322 mapped_name = "%s/%s" % (name_mapping[mn], name) 323 break 324 if not mapped_name: 325 print "LIKELY ERROR: %s has no defined target directory mapping" % name 326 mapped_name = name 327 if 'manual' in test: 328 mapped_name += "-manual" 329 330 cat_total = '' 331 for cat_part in [''] + name.split('.')[:-1]: 332 cat_total += cat_part+'.' 333 if not cat_total in category_names: category_names.append(cat_total) 334 category_contents_all.setdefault(cat_total, []).append(name) 335 category_contents_direct.setdefault(cat_total, []).append(name) 336 337 for ref in test.get('testing', []): 338 if ref not in spec_ids: 339 print "Test %s uses nonexistent spec point %s" % (name, ref) 340 spec_refs.setdefault(ref, []).append(name) 341 #if not (len(test.get('testing', [])) or 'mozilla' in test): 342 if not test.get('testing', []): 343 print "Test %s doesn't refer to any spec points" % name 344 345 if test.get('expected', '') == 'green' and re.search(r'@assert pixel .* 0,0,0,0;', test['code']): 346 print "Probable incorrect pixel test in %s" % name 347 348 code = expand_test_code(test['code']) 349 350 mochitest = not (W3CMODE or 'manual' in test or 'disabled' in test.get('mozilla', {})) 351 if mochitest: 352 mochi_code = expand_mochitest_code(test['code']) 353 354 mochi_name = name 355 if 'mozilla' in test: 356 if 'throws' in test['mozilla']: 357 mochi_code = templates['mochitest.exception'] % mochi_code 358 if 'bug' in test['mozilla']: 359 mochi_name = "%s - bug %s" % (name, test['mozilla']['bug']) 360 361 if 'desc' in test: 362 mochi_desc = '<!-- Testing: %s -->\n' % test['desc'] 363 else: 364 mochi_desc = '' 365 366 if 'deferTest' in mochi_code: 367 mochi_setup = '' 368 mochi_footer = '' 369 else: 370 mochi_setup = '' 371 mochi_footer = 'SimpleTest.finish();\n' 372 373 for f in ['isPixel', 'todo_isPixel', 'deferTest', 'wrapFunction']: 374 if f in mochi_code: 375 mochi_setup += templates['mochitest.%s' % f] 376 else: 377 if not W3CMODE: 378 print "Skipping mochitest for %s" % name 379 mochi_name = '' 380 mochi_desc = '' 381 mochi_code = '' 382 mochi_setup = '' 383 mochi_footer = '' 384 385 expectation_html = '' 386 if 'expected' in test and test['expected'] is not None: 387 expected = test['expected'] 388 expected_img = None 389 if expected == 'green': 390 expected_img = make_flat_image('green-100x50.png', 100, 50, 0,1,0,1) 391 if W3CMODE: expected_img = "/images/" + expected_img 392 elif expected == 'clear': 393 expected_img = make_flat_image('clear-100x50.png', 100, 50, 0,0,0,0) 394 if W3CMODE: expected_img = "/images/" + expected_img 395 else: 396 if ';' in expected: print "Found semicolon in %s" % name 397 expected = re.sub(r'^size (\d+) (\d+)', 398 r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)\ncr = cairo.Context(surface)', 399 expected) 400 401 if mapped_name.endswith("-manual"): 402 png_name = mapped_name[:-len("-manual")] 403 else: 404 png_name = mapped_name 405 expected += "\nsurface.write_to_png('%s/%s.png')\n" % (IMAGEOUTPUTDIR, png_name) 406 eval(compile(expected, '<test %s>' % test['name'], 'exec'), {}, {'cairo':cairo}) 407 expected_img = "%s.png" % name 408 409 if expected_img: 410 expectation_html = ('<p class="output expectedtext">Expected output:' + 411 '<p><img src="%s" class="output expected" id="expected" alt="">' % (expected_img)) 412 413 canvas = test.get('canvas', 'width="100" height="50"') 414 415 prev = tests[i-1]['name'] if i != 0 else 'index' 416 next = tests[i+1]['name'] if i != len(tests)-1 else 'index' 417 418 name_wrapped = name.replace('.', '.​') # (see https://bugzilla.mozilla.org/show_bug.cgi?id=376188) 419 420 refs = ''.join('<li><a href="%s/canvas.html#testrefs.%s">%s</a>\n' % (SPECOUTPUTPATH, n,n) for n in test.get('testing', [])) 421 if not W3CMODE and 'mozilla' in test and 'bug' in test['mozilla']: 422 refs += '<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=%d">Bugzilla</a>' % test['mozilla']['bug'] 423 424 notes = '<p class="notes">%s' % test['notes'] if 'notes' in test else '' 425 426 scripts = '' 427 for s in test.get('scripts', []): 428 scripts += '<script src="%s"></script>\n' % (s) 429 430 images = '' 431 for i in test.get('images', []): 432 id = i.split('/')[-1] 433 if '/' not in i: 434 used_images[i] = 1 435 i = '../images/%s' % i 436 images += '<img src="%s" id="%s" class="resource">\n' % (i,id) 437 mochi_images = images.replace('../images/', 'image_') 438 if W3CMODE: images = images.replace("../images/", "/images/") 439 440 fonts = '' 441 fonthack = '' 442 for i in test.get('fonts', []): 443 fonts += '@font-face {\n font-family: %s;\n src: url("/fonts/%s.ttf");\n}\n' % (i, i) 444 # Browsers require the font to actually be used in the page 445 if test.get('fonthack', 1): 446 fonthack += '<span style="font-family: %s; position: absolute; visibility: hidden">A</span>\n' % i 447 if fonts: 448 fonts = '<style>\n%s</style>\n' % fonts 449 450 fallback = test.get('fallback', '<p class="fallback">FAIL (fallback content)</p>') 451 452 desc = test.get('desc', '') 453 escaped_desc = simpleEscapeJS(desc) 454 template_params = { 455 'name':name, 'name_wrapped':name_wrapped, 'backrefs':backref_html(name), 456 'mapped_name':mapped_name, 457 'desc':desc, 'escaped_desc':escaped_desc, 458 'prev':prev, 'next':next, 'refs':refs, 'notes':notes, 'images':images, 459 'fonts':fonts, 'fonthack':fonthack, 460 'canvas':canvas, 'expected':expectation_html, 'code':code, 'scripts':scripts, 461 'mochi_name':mochi_name, 'mochi_desc':mochi_desc, 'mochi_code':mochi_code, 462 'mochi_setup':mochi_setup, 'mochi_footer':mochi_footer, 'mochi_images':mochi_images, 463 'fallback':fallback 464 } 465 466 if W3CMODE: 467 f = codecs.open('%s/%s.html' % (TESTOUTPUTDIR, mapped_name), 'w', 'utf-8') 468 f.write(templates['w3c'] % template_params) 469 else: 470 f = codecs.open('%s/%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8') 471 f.write(templates['standalone'] % template_params) 472 473 f = codecs.open('%s/framed.%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8') 474 f.write(templates['framed'] % template_params) 475 476 f = codecs.open('%s/minimal.%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8') 477 f.write(templates['minimal'] % template_params) 478 479 if mochitest: 480 mochitests.append(name) 481 f = codecs.open('%s/mochitests/test_%s.html' % (MISCOUTPUTDIR, name), 'w', 'utf-8') 482 f.write(templates['mochitest'] % template_params) 483 484def write_mochitest_makefile(): 485 f = open('%s/mochitests/Makefile.in' % MISCOUTPUTDIR, 'w') 486 f.write(templates['mochitest.Makefile']) 487 files = ['test_%s.html' % n for n in mochitests] + ['image_%s' % n for n in used_images] 488 chunksize = 100 489 chunks = [] 490 for i in range(0, len(files), chunksize): 491 chunk = files[i:i+chunksize] 492 name = '_TEST_FILES_%d' % (i / chunksize) 493 chunks.append(name) 494 f.write('%s = \\\n' % name) 495 for file in chunk: f.write('\t%s \\\n' % file) 496 f.write('\t$(NULL)\n\n') 497 f.write('# split up into groups to work around command-line length limits\n') 498 for name in chunks: 499 f.write('libs:: $(%s)\n\t$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)\n\n' % name) 500 501if not W3CMODE: 502 for i in used_images: 503 shutil.copyfile("../../images/%s" % i, "%s/mochitests/image_%s" % (MISCOUTPUTDIR, i)) 504 write_mochitest_makefile() 505 506print 507 508def write_index(): 509 f = open('%s/index.html' % TESTOUTPUTDIR, 'w') 510 f.write(templates['index.w3c' if W3CMODE else 'index'] % { 'updated':time.strftime('%Y-%m-%d', time.gmtime()) }) 511 f.write('\n<ul class="testlist">\n') 512 depth = 1 513 for category in category_names: 514 name = category[1:-1] or '' 515 count = len(category_contents_all[category]) 516 new_depth = category.count('.') 517 while new_depth < depth: f.write(' '*(depth-1) + '</ul>\n'); depth -= 1 518 f.write(' '*depth + templates['index.w3c.category.item' if W3CMODE else 'index.category.item'] % (name or 'all', name, count, '' if count==1 else 's')) 519 while new_depth+1 > depth: f.write(' '*depth + '<ul>\n'); depth += 1 520 for item in category_contents_direct.get(category, []): 521 f.write(' '*depth + '<li><a href="%s.html">%s</a>\n' % (item, item) ) 522 while 0 < depth: f.write(' '*(depth-1) + '</ul>\n'); depth -= 1 523 524def write_category_indexes(): 525 for category in category_names: 526 name = (category[1:-1] or 'all') 527 528 f = open('%s/index.%s.html' % (TESTOUTPUTDIR, name), 'w') 529 f.write(templates['index.w3c.frame' if W3CMODE else 'index.frame'] % { 'backrefs':backref_html(name), 'category':name }) 530 for item in category_contents_all[category]: 531 f.write(templates['index.w3c.frame.item' if W3CMODE else 'index.frame.item'] % item) 532 533def write_reportgen(): 534 f = open('%s/reportgen.html' % MISCOUTPUTDIR, 'w') 535 items_text = ',\n'.join(('"%s"' % item) for item in category_contents_all['.']) 536 f.write(templates['reportgen'] % {'items':items_text }) 537 538def write_results(): 539 results = {} 540 uas = [] 541 uastrings = {} 542 for item in category_contents_all['.']: results[item] = {} 543 544 f = open('%s/results.html' % MISCOUTPUTDIR, 'w') 545 f.write(templates['results']) 546 547 if not os.path.exists('results.yaml'): 548 print "Can't find results.yaml" 549 else: 550 for resultset in yaml.load(open('results.yaml', "r").read()): 551 #title = "%s (%s)" % (resultset['ua'], resultset['time']) 552 title = resultset['name'] 553 #assert title not in uas # don't allow repetitions 554 if title not in uas: 555 uas.append(title) 556 uastrings[title] = resultset['ua'] 557 else: 558 assert uastrings[title] == resultset['ua'] 559 for r in resultset['results']: 560 if r['id'] not in results: 561 print 'Skipping results for removed test %s' % r['id'] 562 continue 563 results[r['id']][title] = ( 564 r['status'].lower(), 565 re.sub(r'%(..)', lambda m: chr(int(m.group(1), 16)), 566 re.sub(r'%u(....)', lambda m: unichr(int(m.group(1), 16)), 567 r['notes'])).encode('utf8') 568 ) 569 570 passes = {} 571 for ua in uas: 572 f.write('<th title="%s">%s\n' % (uastrings[ua], ua)) 573 passes[ua] = 0 574 for id in category_contents_all['.']: 575 f.write('<tr><td><a href="#%s" id="%s">#</a> <a href="%s.html">%s</a>\n' % (id, id, id, id)) 576 for ua in uas: 577 status, details = results[id].get(ua, ('', '')) 578 f.write('<td class="r %s"><ul class="d">%s</ul>\n' % (status, details)) 579 if status == 'pass': passes[ua] += 1 580 f.write('<tr><th>Passes\n') 581 for ua in uas: 582 f.write('<td>%.1f%%\n' % ((100.0 * passes[ua]) / len(category_contents_all['.']))) 583 f.write('<tr><td>\n') 584 for ua in uas: 585 f.write('<td>%s\n' % ua) 586 f.write('</table>\n') 587 588def getNodeText(node): 589 t, offsets = '', [] 590 591 # Skip over any previous annotations we added 592 if node.nodeType == node.ELEMENT_NODE and 'testrefs' in node.getAttribute('class').split(' '): 593 return t, offsets 594 595 if node.nodeType == node.TEXT_NODE: 596 val = node.nodeValue 597 val = val.replace(unichr(0xa0), ' ') # replace s 598 t += val 599 offsets += [ (node, len(node.nodeValue)) ] 600 for n in node.childNodes: 601 child_t, child_offsets = getNodeText(n) 602 t += child_t 603 offsets += child_offsets 604 return t, offsets 605 606def htmlSerializer(element): 607 element.normalize() 608 rv = [] 609 specialtext = ['style', 'script', 'xmp', 'iframe', 'noembed', 'noframes', 'noscript'] 610 empty = ['area', 'base', 'basefont', 'bgsound', 'br', 'col', 'embed', 'frame', 611 'hr', 'img', 'input', 'link', 'meta', 'param', 'spacer', 'wbr'] 612 613 def serializeElement(element): 614 if element.nodeType == Node.DOCUMENT_TYPE_NODE: 615 rv.append("<!DOCTYPE %s>" % element.name) 616 elif element.nodeType == Node.DOCUMENT_NODE: 617 for child in element.childNodes: 618 serializeElement(child) 619 elif element.nodeType == Node.COMMENT_NODE: 620 rv.append("<!--%s-->" % element.nodeValue) 621 elif element.nodeType == Node.TEXT_NODE: 622 unescaped = False 623 n = element.parentNode 624 while n is not None: 625 if n.nodeName in specialtext: 626 unescaped = True 627 break 628 n = n.parentNode 629 if unescaped: 630 rv.append(element.nodeValue) 631 else: 632 rv.append(escapeHTML(element.nodeValue)) 633 else: 634 rv.append("<%s" % element.nodeName) 635 if element.hasAttributes(): 636 for name, value in element.attributes.items(): 637 rv.append(' %s="%s"' % (name, escapeHTML(value))) 638 rv.append(">") 639 if element.nodeName not in empty: 640 for child in element.childNodes: 641 serializeElement(child) 642 rv.append("</%s>" % element.nodeName) 643 serializeElement(element) 644 return '<!DOCTYPE html>\n' + ''.join(rv) 645 646def write_annotated_spec(): 647 # Load the stripped-down XHTMLised copy of the spec 648 doc = xml.dom.minidom.parse(open('current-work-canvas.xhtml', 'r')) 649 650 # Insert our new stylesheet 651 n = doc.getElementsByTagName('head')[0].appendChild(doc.createElement('link')) 652 n.setAttribute('rel', 'stylesheet') 653 n.setAttribute('href', '../common/canvas-spec.css' if W3CMODE else '../spectest.css') 654 n.setAttribute('type', 'text/css') 655 656 spec_assertion_patterns = [] 657 for a in spec_assertions: 658 # Warn about problems 659 if a['id'] not in spec_refs: 660 print "Unused spec statement %s" % a['id'] 661 662 pattern_text = a['text'] 663 664 if 'keyword' in a: 665 # Explicit keyword override 666 keyword = a['keyword'] 667 else: 668 # Extract the marked keywords, and remove the markers 669 keyword = 'none' 670 for kw in ['must', 'should', 'required']: 671 if ('*%s*' % kw) in pattern_text: 672 keyword = kw 673 pattern_text = pattern_text.replace('*%s*' % kw, kw) 674 break 675 # Make sure there wasn't >1 keyword 676 for kw in ['must', 'should', 'required']: 677 assert('*%s*' % kw not in pattern_text) 678 679 # Convert the special pattern format into regexp syntax 680 pattern_text = (pattern_text. 681 # Escape relevant characters 682 replace('*', r'\*'). 683 replace('+', r'\+'). 684 replace('.', r'\.'). 685 replace('(', r'\('). 686 replace(')', r'\)'). 687 replace('[', r'\['). 688 replace(']', r'\]'). 689 # Convert special sequences back into unescaped regexp code 690 replace(' ', r'\s+'). 691 replace(r'<\.\.\.>', r'.+'). 692 replace('<^>', r'()'). 693 replace('<eol>', r'\s*?\n') 694 ) 695 pattern = re.compile(pattern_text, re.S) 696 spec_assertion_patterns.append( (a['id'], pattern, keyword, a.get('previously', None)) ) 697 matched_assertions = {} 698 699 def process_element(e): 700 if e.nodeType == e.ELEMENT_NODE and (e.getAttribute('class') == 'impl' or e.hasAttribute('data-component')): 701 for c in e.childNodes: 702 process_element(c) 703 return 704 705 t, offsets = getNodeText(e) 706 for id, pattern, keyword, previously in spec_assertion_patterns: 707 m = pattern.search(t) 708 if m: 709 # When the pattern-match isn't enough to uniquely identify a sentence, 710 # allow explicit back-references to earlier paragraphs 711 if previously: 712 if len(previously) >= 3: 713 n, text, exp = previously 714 else: 715 n, text = previously 716 exp = True 717 node = e 718 while n and node.previousSibling: 719 node = node.previousSibling 720 n -= 1 721 if (text not in getNodeText(node)[0]) == exp: 722 continue # discard this match 723 724 if id in matched_assertions: 725 print "Spec statement %s matches multiple places" % id 726 matched_assertions[id] = True 727 728 if m.lastindex != 1: 729 print "Spec statement %s has incorrect number of match groups" % id 730 731 end = m.end(1) 732 end_node = None 733 for end_node, o in offsets: 734 if end < o: 735 break 736 end -= o 737 assert(end_node) 738 739 n1 = doc.createElement('span') 740 n1.setAttribute('class', 'testrefs kw-%s' % keyword) 741 n1.setAttribute('id', 'testrefs.%s' % id) 742 n1.appendChild(doc.createTextNode(' ')) 743 744 n = n1.appendChild(doc.createElement('a')) 745 n.setAttribute('href', '#testrefs.%s' % id) 746 n.setAttribute('title', id) 747 n.appendChild(doc.createTextNode('#')) 748 749 n1.appendChild(doc.createTextNode(' ')) 750 for test_id in spec_refs.get(id, []): 751 n = n1.appendChild(doc.createElement('a')) 752 n.setAttribute('href', '../canvas/%s.html' % test_id) 753 n.appendChild(doc.createTextNode(test_id)) 754 n1.appendChild(doc.createTextNode(' ')) 755 n0 = doc.createTextNode(end_node.nodeValue[:end]) 756 n2 = doc.createTextNode(end_node.nodeValue[end:]) 757 758 p = end_node.parentNode 759 p.replaceChild(n2, end_node) 760 p.insertBefore(n1, n2) 761 p.insertBefore(n0, n1) 762 763 t, offsets = getNodeText(e) 764 765 for e in doc.getElementsByTagName('body')[0].childNodes: 766 process_element(e) 767 768 for s in spec_assertions: 769 if s['id'] not in matched_assertions: 770 print "Annotation incomplete: Unmatched spec statement %s" % s['id'] 771 772 # Convert from XHTML back to HTML 773 doc.documentElement.removeAttribute('xmlns') 774 doc.documentElement.setAttribute('lang', doc.documentElement.getAttribute('xml:lang')) 775 776 head = doc.documentElement.getElementsByTagName('head')[0] 777 head.insertBefore(doc.createElement('meta'), head.firstChild).setAttribute('charset', 'UTF-8') 778 779 f = codecs.open('%s/canvas.html' % SPECOUTPUTDIR, 'w', 'utf-8') 780 f.write(htmlSerializer(doc)) 781 782if not W3CMODE: 783 write_index() 784 write_category_indexes() 785 write_reportgen() 786 write_results() 787 write_annotated_spec() 788