html

view tests/submission/PhilipTaylor/tools/canvas/gentest.py @ 429:a85fcfbbf6b9

Add a few async tests now that the delay.php has been added and works.
author kkrueger@ip-208-109-98-22.ip.secureserver.net
date Fri, 19 Aug 2011 18:27:56 -0700
parents a0c3e6ae3b29
children 7145549fcecf
line source
1 # Copyright (c) 2010 Philip Taylor
2 # Released under the BSD license and W3C Test Suite License: see LICENSE.txt
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 W3C HTML5 test suite at
10 # http://dvcs.w3.org/hg/html/file/tip/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.
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 Hg, remove deleted ones from Hg, etc.
39 import re
40 import codecs
41 import time
42 import os
43 import shutil
44 import sys
45 import xml.dom.minidom
46 from xml.dom.minidom import Node
48 import cairo
50 try:
51 import syck as yaml # compatible and lots faster
52 except ImportError:
53 import yaml
55 # Default mode is for the W3C test suite; the --standalone option
56 # generates various extra files that aren't needed there
57 W3CMODE = True
58 if '--standalone' in sys.argv:
59 W3CMODE = False
61 TESTOUTPUTDIR = '../../canvas'
62 IMAGEOUTPUTDIR = '../../canvas'
63 MISCOUTPUTDIR = './output'
64 SPECOUTPUTDIR = '../../annotated-spec'
66 SPECOUTPUTPATH = '../annotated-spec' # relative to TESTOUTPUTDIR
68 def escapeJS(str):
69 str = str.replace('\\', '\\\\').replace('"', '\\"')
70 str = re.sub(r'\[(\w+)\]', r'[\\""+(\1)+"\\"]', str) # kind of an ugly hack, for nicer failure-message output
71 return str
73 def escapeHTML(str):
74 return str.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
76 def expand_nonfinite(method, argstr, tail):
77 """
78 >>> print expand_nonfinite('f', '<0 a>, <0 b>', ';')
79 f(a, 0);
80 f(0, b);
81 f(a, b);
82 >>> print expand_nonfinite('f', '<0 a>, <0 b c>, <0 d>', ';')
83 f(a, 0, 0);
84 f(0, b, 0);
85 f(0, c, 0);
86 f(0, 0, d);
87 f(a, b, 0);
88 f(a, b, d);
89 f(a, 0, d);
90 f(0, b, d);
91 """
92 # argstr is "<valid-1 invalid1-1 invalid2-1 ...>, ..." (where usually
93 # 'invalid' is Infinity/-Infinity/NaN)
94 args = []
95 for arg in argstr.split(', '):
96 a = re.match('<(.*)>', arg).group(1)
97 args.append(a.split(' '))
98 calls = []
99 # Start with the valid argument list
100 call = [ args[j][0] for j in range(len(args)) ]
101 # For each argument alone, try setting it to all its invalid values:
102 for i in range(len(args)):
103 for a in args[i][1:]:
104 c2 = call[:]
105 c2[i] = a
106 calls.append(c2)
107 # For all combinations of >= 2 arguments, try setting them to their
108 # first invalid values. (Don't do all invalid values, because the
109 # number of combinations explodes.)
110 def f(c, start, depth):
111 for i in range(start, len(args)):
112 if len(args[i]) > 1:
113 a = args[i][1]
114 c2 = c[:]
115 c2[i] = a
116 if depth > 0: calls.append(c2)
117 f(c2, i+1, depth+1)
118 f(call, 0, 0)
120 return '\n'.join('%s(%s)%s' % (method, ', '.join(c), tail) for c in calls)
122 # Run with --test argument to run unit tests
123 if len(sys.argv) > 1 and sys.argv[1] == '--test':
124 import doctest
125 doctest.testmod()
126 sys.exit()
129 templates = yaml.load(open('templates.yaml').read())
131 spec_assertions = []
132 for s in yaml.load(open('spec.yaml').read())['assertions']:
133 if 'meta' in s:
134 eval(compile(s['meta'], '<meta spec assertion>', 'exec'), {}, {'assertions':spec_assertions})
135 else:
136 spec_assertions.append(s)
138 tests = []
139 for t in sum([ yaml.load(open(f).read()) for f in ['tests.yaml', 'tests2d.yaml', 'tests2dtext.yaml']], []):
140 if 'DISABLED' in t:
141 continue
142 if 'meta' in t:
143 eval(compile(t['meta'], '<meta test>', 'exec'), {}, {'tests':tests})
144 else:
145 tests.append(t)
147 category_names = []
148 category_contents_direct = {}
149 category_contents_all = {}
151 spec_ids = {}
152 for t in spec_assertions: spec_ids[t['id']] = True
153 spec_refs = {}
155 def backref_html(name):
156 backrefs = []
157 c = ''
158 for p in name.split('.')[:-1]:
159 c += '.'+p
160 backrefs.append('<a href="index%s.html">%s</a>.' % (c, p))
161 backrefs.append(name.split('.')[-1])
162 return ''.join(backrefs)
164 def make_flat_image(filename, w, h, r,g,b,a):
165 if os.path.exists('%s/%s' % (IMAGEOUTPUTDIR, filename)):
166 return filename
167 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
168 cr = cairo.Context(surface)
169 cr.set_source_rgba(r, g, b, a)
170 cr.rectangle(0, 0, w, h)
171 cr.fill()
172 surface.write_to_png('%s/%s' % (IMAGEOUTPUTDIR, filename))
173 return filename
175 # Ensure the test output directories exist
176 testdirs = [TESTOUTPUTDIR, IMAGEOUTPUTDIR, MISCOUTPUTDIR]
177 if not W3CMODE: testdirs.append('%s/mochitests' % MISCOUTPUTDIR)
178 for d in testdirs:
179 try: os.mkdir(d)
180 except: pass # ignore if it already exists
182 mochitests = []
183 used_images = {}
185 def expand_test_code(code):
186 code = re.sub(r'@nonfinite ([^(]+)\(([^)]+)\)(.*)', lambda m: expand_nonfinite(m.group(1), m.group(2), m.group(3)), code) # must come before '@assert throws'
188 code = re.sub(r'@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);',
189 r'_assertPixel(canvas, \1, \2, "\1", "\2");',
190 code)
192 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);',
193 r'_assertPixelApprox(canvas, \1, \2, "\1", "\2", 2);',
194 code)
196 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+/- (\d+);',
197 r'_assertPixelApprox(canvas, \1, \2, "\1", "\2", \3);',
198 code)
200 code = re.sub(r'@assert throws (\S+_ERR) (.*);',
201 lambda m: 'try { var _thrown = false;\n %s;\n} catch (e) { if (e.code != DOMException.%s) _fail("Failed assertion: expected exception of type %s, got: "+e.message); _thrown = true; } finally { _assert(_thrown, "should throw exception of type %s: %s"); }'
202 % (m.group(2), m.group(1), m.group(1), m.group(1), escapeJS(m.group(2)))
203 , code)
205 code = re.sub(r'@assert throws (\S+Error) (.*);',
206 lambda m: 'try { var _thrown = false;\n %s;\n} catch (e) { if (!(e instanceof %s)) _fail("Failed assertion: expected exception of type %s, got: "+e); _thrown = true; } finally { _assert(_thrown, "should throw exception of type %s: %s"); }'
207 % (m.group(2), m.group(1), m.group(1), m.group(1), escapeJS(m.group(2)))
208 , code)
210 code = re.sub(r'@assert throws (.*);',
211 lambda m: 'try { var _thrown = false; %s; } catch (e) { _thrown = true; } finally { _assert(_thrown, "should throw exception: %s"); }'
212 % (m.group(1), escapeJS(m.group(1)))
213 , code)
215 code = re.sub(r'@assert (.*) === (.*);',
216 lambda m: '_assertSame(%s, %s, "%s", "%s");'
217 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2)))
218 , code)
220 code = re.sub(r'@assert (.*) !== (.*);',
221 lambda m: '_assertDifferent(%s, %s, "%s", "%s");'
222 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2)))
223 , code)
225 code = re.sub(r'@assert (.*) == (.*);',
226 lambda m: '_assertEqual(%s, %s, "%s", "%s");'
227 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2)))
228 , code)
230 code = re.sub(r'@assert (.*) =~ (.*);',
231 lambda m: '_assertMatch(%s, %s, "%s", "%s");'
232 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2)))
233 , code)
235 code = re.sub(r'@assert (.*);',
236 lambda m: '_assert(%s, "%s");'
237 % (m.group(1), escapeJS(m.group(1)))
238 , code)
240 code = re.sub(r'@manual;', '_requireManualCheck();', code)
242 code = re.sub(r'@crash;', 'return _crash();', code)
244 code = re.sub(r' @moz-todo', '', code)
246 code = re.sub(r'@moz-UniversalBrowserRead;',
247 ""
248 , code)
250 assert('@' not in code)
252 return code
254 def expand_mochitest_code(code):
255 code = re.sub(r'@nonfinite ([^(]+)\(([^)]+)\)(.*)', lambda m: expand_nonfinite(m.group(1), m.group(2), m.group(3)), code)
257 code = re.sub(r'@assert pixel (\d+,\d+) == (\d+,\d+,\d+,\d+);',
258 r'isPixel(ctx, \1, \2, "\1", "\2", 0);',
259 code)
261 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+);',
262 r'isPixel(ctx, \1, \2, "\1", "\2", 2);',
263 code)
265 code = re.sub(r'@assert pixel (\d+,\d+) ==~ (\d+,\d+,\d+,\d+) \+/- (\d+);',
266 r'isPixel(ctx, \1, \2, "\1", "\2", \3);',
267 code)
269 code = re.sub(r'@assert throws (\S+_ERR) (.*);',
270 lambda m: 'var _thrown = undefined; try {\n %s;\n} catch (e) { _thrown = e }; ok(_thrown && _thrown.code == DOMException.%s, "should throw %s");'
271 % (m.group(2), m.group(1), m.group(1))
272 , code)
274 code = re.sub(r'@assert throws (\S+Error) (.*);',
275 lambda m: 'var _thrown = undefined; try {\n %s;\n} catch (e) { _thrown = e }; ok(_thrown && (_thrown instanceof %s), "should throw %s");'
276 % (m.group(2), m.group(1), m.group(1))
277 , code)
279 code = re.sub(r'@assert throws (.*);',
280 lambda m: 'try { var _thrown = false;\n %s;\n} catch (e) { _thrown = true; } finally { ok(_thrown, "should throw exception"); }'
281 % (m.group(1))
282 , code)
284 code = re.sub(r'@assert (.*) =~ (.*);',
285 lambda m: 'ok(%s.match(%s), "%s.match(%s)");'
286 % (m.group(1), m.group(2), escapeJS(m.group(1)), escapeJS(m.group(2)))
287 , code)
289 code = re.sub(r'@assert (.*);',
290 lambda m: 'ok(%s, "%s");'
291 % (m.group(1), escapeJS(m.group(1)))
292 , code)
294 code = re.sub(r'((?:^|\n|;)\s*)ok(.*;) @moz-todo',
295 lambda m: '%stodo%s'
296 % (m.group(1), m.group(2))
297 , code)
299 code = re.sub(r'((?:^|\n|;)\s*)(is.*;) @moz-todo',
300 lambda m: '%stodo_%s'
301 % (m.group(1), m.group(2))
302 , code)
304 code = re.sub(r'@moz-UniversalBrowserRead;',
305 "netscape.security.PrivilegeManager.enablePrivilege('UniversalBrowserRead');"
306 , code)
308 code = code.replace('../images/', 'image_')
310 assert '@' not in code, '@ not in code:\n%s' % code
312 return code
314 used_tests = {}
315 for i in range(len(tests)):
316 test = tests[i]
318 name = test['name']
319 print "\r(%s)" % name, " "*32, "\t",
321 if name in used_tests:
322 print "Test %s is defined twice" % name
323 used_tests[name] = 1
325 cat_total = ''
326 for cat_part in [''] + name.split('.')[:-1]:
327 cat_total += cat_part+'.'
328 if not cat_total in category_names: category_names.append(cat_total)
329 category_contents_all.setdefault(cat_total, []).append(name)
330 category_contents_direct.setdefault(cat_total, []).append(name)
332 for ref in test.get('testing', []):
333 if ref not in spec_ids:
334 print "Test %s uses nonexistent spec point %s" % (name, ref)
335 spec_refs.setdefault(ref, []).append(name)
336 #if not (len(test.get('testing', [])) or 'mozilla' in test):
337 if not test.get('testing', []):
338 print "Test %s doesn't refer to any spec points" % name
340 if test.get('expected', '') == 'green' and re.search(r'@assert pixel .* 0,0,0,0;', test['code']):
341 print "Probable incorrect pixel test in %s" % name
343 code = expand_test_code(test['code'])
345 mochitest = not (W3CMODE or '@manual' in test['code'] or 'disabled' in test.get('mozilla', {}))
346 if mochitest:
347 mochi_code = expand_mochitest_code(test['code'])
349 mochi_name = name
350 if 'mozilla' in test:
351 if 'throws' in test['mozilla']:
352 mochi_code = templates['mochitest.exception'] % mochi_code
353 if 'bug' in test['mozilla']:
354 mochi_name = "%s - bug %s" % (name, test['mozilla']['bug'])
356 if 'desc' in test:
357 mochi_desc = '<!-- Testing: %s -->\n' % test['desc']
358 else:
359 mochi_desc = ''
361 if 'deferTest' in mochi_code:
362 mochi_setup = ''
363 mochi_footer = ''
364 else:
365 mochi_setup = ''
366 mochi_footer = 'SimpleTest.finish();\n'
368 for f in ['isPixel', 'todo_isPixel', 'deferTest', 'wrapFunction']:
369 if f in mochi_code:
370 mochi_setup += templates['mochitest.%s' % f]
371 else:
372 if not W3CMODE:
373 print "Skipping mochitest for %s" % name
374 mochi_name = ''
375 mochi_desc = ''
376 mochi_code = ''
377 mochi_setup = ''
378 mochi_footer = ''
380 expectation_html = ''
381 if 'expected' in test and test['expected'] is not None:
382 expected = test['expected']
383 expected_img = None
384 if expected == 'green':
385 expected_img = make_flat_image('green-100x50.png', 100, 50, 0,1,0,1)
386 elif expected == 'clear':
387 expected_img = make_flat_image('clear-100x50.png', 100, 50, 0,0,0,0)
388 else:
389 if ';' in expected: print "Found semicolon in %s" % name
390 expected = re.sub(r'^size (\d+) (\d+)',
391 r'surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \1, \2)\ncr = cairo.Context(surface)',
392 expected)
393 expected += "\nsurface.write_to_png('%s/%s.png')\n" % (IMAGEOUTPUTDIR, name)
394 eval(compile(expected, '<test %s>' % test['name'], 'exec'), {}, {'cairo':cairo})
395 expected_img = "%s.png" % name
397 if expected_img:
398 expectation_html = ('<p class="output expectedtext">Expected output:' +
399 '<p><img src="%s" class="output expected" id="expected" alt="">' % (expected_img))
401 canvas = test.get('canvas', 'width="100" height="50"')
403 prev = tests[i-1]['name'] if i != 0 else 'index'
404 next = tests[i+1]['name'] if i != len(tests)-1 else 'index'
406 name_wrapped = name.replace('.', '.&#8203;') # (see https://bugzilla.mozilla.org/show_bug.cgi?id=376188)
408 refs = ''.join('<li><a href="%s/canvas.html#testrefs.%s">%s</a>\n' % (SPECOUTPUTPATH, n,n) for n in test.get('testing', []))
409 if not W3CMODE and 'mozilla' in test and 'bug' in test['mozilla']:
410 refs += '<li><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=%d">Bugzilla</a>' % test['mozilla']['bug']
412 notes = '<p class="notes">%s' % test['notes'] if 'notes' in test else ''
414 images = ''
415 for i in test.get('images', []):
416 id = i.split('/')[-1]
417 if '/' not in i:
418 used_images[i] = 1
419 i = '../images/%s' % i
420 images += '<img src="%s" id="%s" class="resource">\n' % (i,id)
421 mochi_images = images.replace('../images/', 'image_')
423 fonts = ''
424 fonthack = ''
425 for i in test.get('fonts', []):
426 fonts += '@font-face {\n font-family: %s;\n src: url("../fonts/%s.ttf");\n}\n' % (i, i)
427 # Browsers require the font to actually be used in the page
428 if test.get('fonthack', 1):
429 fonthack += '<span style="font-family: %s; position: absolute; visibility: hidden">A</span>\n' % i
430 if fonts:
431 fonts = '<style>\n%s</style>\n' % fonts
433 fallback = test.get('fallback', '<p class="fallback">FAIL (fallback content)</p>')
435 desc = test.get('desc', '')
437 template_params = {
438 'name':name, 'name_wrapped':name_wrapped, 'backrefs':backref_html(name), 'desc':desc,
439 'prev':prev, 'next':next, 'refs':refs, 'notes':notes, 'images':images,
440 'fonts':fonts, 'fonthack':fonthack,
441 'canvas':canvas, 'expected':expectation_html, 'code':code,
442 'mochi_name':mochi_name, 'mochi_desc':mochi_desc, 'mochi_code':mochi_code,
443 'mochi_setup':mochi_setup, 'mochi_footer':mochi_footer, 'mochi_images':mochi_images,
444 'fallback':fallback
445 }
447 if W3CMODE:
448 f = codecs.open('%s/%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8')
449 f.write(templates['w3c'] % template_params)
450 else:
451 f = codecs.open('%s/%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8')
452 f.write(templates['standalone'] % template_params)
454 f = codecs.open('%s/framed.%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8')
455 f.write(templates['framed'] % template_params)
457 f = codecs.open('%s/minimal.%s.html' % (TESTOUTPUTDIR, name), 'w', 'utf-8')
458 f.write(templates['minimal'] % template_params)
460 if mochitest:
461 mochitests.append(name)
462 f = codecs.open('%s/mochitests/test_%s.html' % (MISCOUTPUTDIR, name), 'w', 'utf-8')
463 f.write(templates['mochitest'] % template_params)
465 def write_mochitest_makefile():
466 f = open('%s/mochitests/Makefile.in' % MISCOUTPUTDIR, 'w')
467 f.write(templates['mochitest.Makefile'])
468 files = ['test_%s.html' % n for n in mochitests] + ['image_%s' % n for n in used_images]
469 chunksize = 100
470 chunks = []
471 for i in range(0, len(files), chunksize):
472 chunk = files[i:i+chunksize]
473 name = '_TEST_FILES_%d' % (i / chunksize)
474 chunks.append(name)
475 f.write('%s = \\\n' % name)
476 for file in chunk: f.write('\t%s \\\n' % file)
477 f.write('\t$(NULL)\n\n')
478 f.write('# split up into groups to work around command-line length limits\n')
479 for name in chunks:
480 f.write('libs:: $(%s)\n\t$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)\n\n' % name)
482 if not W3CMODE:
483 for i in used_images:
484 shutil.copyfile("../../images/%s" % i, "%s/mochitests/image_%s" % (MISCOUTPUTDIR, i))
485 write_mochitest_makefile()
487 print
489 def write_index():
490 f = open('%s/index.html' % TESTOUTPUTDIR, 'w')
491 f.write(templates['index.w3c' if W3CMODE else 'index'] % { 'updated':time.strftime('%Y-%m-%d', time.gmtime()) })
492 f.write('\n<ul class="testlist">\n')
493 depth = 1
494 for category in category_names:
495 name = category[1:-1] or ''
496 count = len(category_contents_all[category])
497 new_depth = category.count('.')
498 while new_depth < depth: f.write(' '*(depth-1) + '</ul>\n'); depth -= 1
499 f.write(' '*depth + templates['index.w3c.category.item' if W3CMODE else 'index.category.item'] % (name or 'all', name, count, '' if count==1 else 's'))
500 while new_depth+1 > depth: f.write(' '*depth + '<ul>\n'); depth += 1
501 for item in category_contents_direct.get(category, []):
502 f.write(' '*depth + '<li><a href="%s.html">%s</a>\n' % (item, item) )
503 while 0 < depth: f.write(' '*(depth-1) + '</ul>\n'); depth -= 1
505 def write_category_indexes():
506 for category in category_names:
507 name = (category[1:-1] or 'all')
509 f = open('%s/index.%s.html' % (TESTOUTPUTDIR, name), 'w')
510 f.write(templates['index.w3c.frame' if W3CMODE else 'index.frame'] % { 'backrefs':backref_html(name), 'category':name })
511 for item in category_contents_all[category]:
512 f.write(templates['index.w3c.frame.item' if W3CMODE else 'index.frame.item'] % item)
514 def write_reportgen():
515 f = open('%s/reportgen.html' % MISCOUTPUTDIR, 'w')
516 items_text = ',\n'.join(('"%s"' % item) for item in category_contents_all['.'])
517 f.write(templates['reportgen'] % {'items':items_text })
519 def write_results():
520 results = {}
521 uas = []
522 uastrings = {}
523 for item in category_contents_all['.']: results[item] = {}
525 f = open('%s/results.html' % MISCOUTPUTDIR, 'w')
526 f.write(templates['results'])
528 if not os.path.exists('results.yaml'):
529 print "Can't find results.yaml"
530 else:
531 for resultset in yaml.load(open('results.yaml').read()):
532 #title = "%s (%s)" % (resultset['ua'], resultset['time'])
533 title = resultset['name']
534 #assert title not in uas # don't allow repetitions
535 if title not in uas:
536 uas.append(title)
537 uastrings[title] = resultset['ua']
538 else:
539 assert uastrings[title] == resultset['ua']
540 for r in resultset['results']:
541 if r['id'] not in results:
542 print 'Skipping results for removed test %s' % r['id']
543 continue
544 results[r['id']][title] = (
545 r['status'].lower(),
546 re.sub(r'%(..)', lambda m: chr(int(m.group(1), 16)),
547 re.sub(r'%u(....)', lambda m: unichr(int(m.group(1), 16)),
548 r['notes'])).encode('utf8')
549 )
551 passes = {}
552 for ua in uas:
553 f.write('<th title="%s">%s\n' % (uastrings[ua], ua))
554 passes[ua] = 0
555 for id in category_contents_all['.']:
556 f.write('<tr><td><a href="#%s" id="%s">#</a> <a href="%s.html">%s</a>\n' % (id, id, id, id))
557 for ua in uas:
558 status, details = results[id].get(ua, ('', ''))
559 f.write('<td class="r %s"><ul class="d">%s</ul>\n' % (status, details))
560 if status == 'pass': passes[ua] += 1
561 f.write('<tr><th>Passes\n')
562 for ua in uas:
563 f.write('<td>%.1f%%\n' % ((100.0 * passes[ua]) / len(category_contents_all['.'])))
564 f.write('<tr><td>\n')
565 for ua in uas:
566 f.write('<td>%s\n' % ua)
567 f.write('</table>\n')
569 def getNodeText(node):
570 t, offsets = '', []
572 # Skip over any previous annotations we added
573 if node.nodeType == node.ELEMENT_NODE and 'testrefs' in node.getAttribute('class').split(' '):
574 return t, offsets
576 if node.nodeType == node.TEXT_NODE:
577 val = node.nodeValue
578 val = val.replace(unichr(0xa0), ' ') # replace &nbsp;s
579 t += val
580 offsets += [ (node, len(node.nodeValue)) ]
581 for n in node.childNodes:
582 child_t, child_offsets = getNodeText(n)
583 t += child_t
584 offsets += child_offsets
585 return t, offsets
587 def html5Serializer(element):
588 element.normalize()
589 rv = []
590 specialtext = ['style', 'script', 'xmp', 'iframe', 'noembed', 'noframes', 'noscript']
591 empty = ['area', 'base', 'basefont', 'bgsound', 'br', 'col', 'embed', 'frame',
592 'hr', 'img', 'input', 'link', 'meta', 'param', 'spacer', 'wbr']
594 def serializeElement(element):
595 if element.nodeType == Node.DOCUMENT_TYPE_NODE:
596 rv.append("<!DOCTYPE %s>" % element.name)
597 elif element.nodeType == Node.DOCUMENT_NODE:
598 for child in element.childNodes:
599 serializeElement(child)
600 elif element.nodeType == Node.COMMENT_NODE:
601 rv.append("<!--%s-->" % element.nodeValue)
602 elif element.nodeType == Node.TEXT_NODE:
603 unescaped = False
604 n = element.parentNode
605 while n is not None:
606 if n.nodeName in specialtext:
607 unescaped = True
608 break
609 n = n.parentNode
610 if unescaped:
611 rv.append(element.nodeValue)
612 else:
613 rv.append(escapeHTML(element.nodeValue))
614 else:
615 rv.append("<%s" % element.nodeName)
616 if element.hasAttributes():
617 for name, value in element.attributes.items():
618 rv.append(' %s="%s"' % (name, escapeHTML(value)))
619 rv.append(">")
620 if element.nodeName not in empty:
621 for child in element.childNodes:
622 serializeElement(child)
623 rv.append("</%s>" % element.nodeName)
624 serializeElement(element)
625 return '<!DOCTYPE html>\n' + ''.join(rv)
627 def write_annotated_spec():
628 # Load the stripped-down XHTMLised copy of the spec
629 doc = xml.dom.minidom.parse(open('current-work-canvas.xhtml', 'r'))
631 # Insert our new stylesheet
632 n = doc.getElementsByTagName('head')[0].appendChild(doc.createElement('link'))
633 n.setAttribute('rel', 'stylesheet')
634 n.setAttribute('href', '../common/canvas-spec.css' if W3CMODE else '../spectest.css')
635 n.setAttribute('type', 'text/css')
637 spec_assertion_patterns = []
638 for a in spec_assertions:
639 # Warn about problems
640 if a['id'] not in spec_refs:
641 print "Unused spec statement %s" % a['id']
643 pattern_text = a['text']
645 if 'keyword' in a:
646 # Explicit keyword override
647 keyword = a['keyword']
648 else:
649 # Extract the marked keywords, and remove the markers
650 keyword = 'none'
651 for kw in ['must', 'should', 'required']:
652 if ('*%s*' % kw) in pattern_text:
653 keyword = kw
654 pattern_text = pattern_text.replace('*%s*' % kw, kw)
655 break
656 # Make sure there wasn't >1 keyword
657 for kw in ['must', 'should', 'required']:
658 assert('*%s*' % kw not in pattern_text)
660 # Convert the special pattern format into regexp syntax
661 pattern_text = (pattern_text.
662 # Escape relevant characters
663 replace('*', r'\*').
664 replace('+', r'\+').
665 replace('.', r'\.').
666 replace('(', r'\(').
667 replace(')', r'\)').
668 replace('[', r'\[').
669 replace(']', r'\]').
670 # Convert special sequences back into unescaped regexp code
671 replace(' ', r'\s+').
672 replace(r'<\.\.\.>', r'.+').
673 replace('<^>', r'()').
674 replace('<eol>', r'\s*?\n')
675 )
676 pattern = re.compile(pattern_text, re.S)
677 spec_assertion_patterns.append( (a['id'], pattern, keyword, a.get('previously', None)) )
678 matched_assertions = {}
680 def process_element(e):
681 if e.nodeType == e.ELEMENT_NODE and (e.getAttribute('class') == 'impl' or e.hasAttribute('data-component')):
682 for c in e.childNodes:
683 process_element(c)
684 return
686 t, offsets = getNodeText(e)
687 for id, pattern, keyword, previously in spec_assertion_patterns:
688 m = pattern.search(t)
689 if m:
690 # When the pattern-match isn't enough to uniquely identify a sentence,
691 # allow explicit back-references to earlier paragraphs
692 if previously:
693 if len(previously) >= 3:
694 n, text, exp = previously
695 else:
696 n, text = previously
697 exp = True
698 node = e
699 while n and node.previousSibling:
700 node = node.previousSibling
701 n -= 1
702 if (text not in getNodeText(node)[0]) == exp:
703 continue # discard this match
705 if id in matched_assertions:
706 print "Spec statement %s matches multiple places" % id
707 matched_assertions[id] = True
709 if m.lastindex != 1:
710 print "Spec statement %s has incorrect number of match groups" % id
712 end = m.end(1)
713 end_node = None
714 for end_node, o in offsets:
715 if end < o:
716 break
717 end -= o
718 assert(end_node)
720 n1 = doc.createElement('span')
721 n1.setAttribute('class', 'testrefs kw-%s' % keyword)
722 n1.setAttribute('id', 'testrefs.%s' % id)
723 n1.appendChild(doc.createTextNode(' '))
725 n = n1.appendChild(doc.createElement('a'))
726 n.setAttribute('href', '#testrefs.%s' % id)
727 n.setAttribute('title', id)
728 n.appendChild(doc.createTextNode('#'))
730 n1.appendChild(doc.createTextNode(' '))
731 for test_id in spec_refs.get(id, []):
732 n = n1.appendChild(doc.createElement('a'))
733 n.setAttribute('href', '../canvas/%s.html' % test_id)
734 n.appendChild(doc.createTextNode(test_id))
735 n1.appendChild(doc.createTextNode(' '))
736 n0 = doc.createTextNode(end_node.nodeValue[:end])
737 n2 = doc.createTextNode(end_node.nodeValue[end:])
739 p = end_node.parentNode
740 p.replaceChild(n2, end_node)
741 p.insertBefore(n1, n2)
742 p.insertBefore(n0, n1)
744 t, offsets = getNodeText(e)
746 for e in doc.getElementsByTagName('body')[0].childNodes:
747 process_element(e)
749 for s in spec_assertions:
750 if s['id'] not in matched_assertions:
751 print "Annotation incomplete: Unmatched spec statement %s" % s['id']
753 # Convert from XHTML5 back to HTML5
754 doc.documentElement.removeAttribute('xmlns')
755 doc.documentElement.setAttribute('lang', doc.documentElement.getAttribute('xml:lang'))
757 head = doc.documentElement.getElementsByTagName('head')[0]
758 head.insertBefore(doc.createElement('meta'), head.firstChild).setAttribute('charset', 'UTF-8')
760 f = codecs.open('%s/canvas.html' % SPECOUTPUTDIR, 'w', 'utf-8')
761 f.write(html5Serializer(doc))
763 write_index()
764 write_category_indexes()
765 if not W3CMODE:
766 write_reportgen()
767 write_results()
768 write_annotated_spec()
Set up and maintained by W3C Systems Team, please report bugs to sysreq@w3.org.

W3C would like to thank Microsoft who donated the server that allows us to run this service.