process-ogles2-tests.py (16509B)
1 #! /usr/bin/env python2 2 3 """generates tests from OpenGL ES 2.0 .run/.test files.""" 4 5 import os 6 import os.path 7 import sys 8 import re 9 import json 10 import shutil 11 from optparse import OptionParser 12 from xml.dom.minidom import parse 13 14 if sys.version < '2.6': 15 print 'Wrong Python Version !!!: Need >= 2.6' 16 sys.exit(1) 17 18 # each shader test generates up to 3 512x512 images. 19 # a 512x512 image takes 1meg of memory so set this 20 # number apporpriate for the platform with 21 # the smallest memory issue. At 8 that means 22 # at least 24 meg is needed to run the test. 23 MAX_TESTS_PER_SET = 8 24 25 VERBOSE = False 26 27 FILTERS = [ 28 re.compile("GL/"), 29 ] 30 31 LICENSE = """ 32 Copyright (c) 2019 The Khronos Group Inc. 33 Use of this source code is governed by an MIT-style license that can be 34 found in the LICENSE.txt file. 35 """.strip() 36 37 COMMENT_RE = re.compile("/\*\n\*\*\s+Copyright.*?\*/", 38 re.IGNORECASE | re.DOTALL) 39 REMOVE_COPYRIGHT_RE = re.compile("\/\/\s+Copyright.*?\n", 40 re.IGNORECASE | re.DOTALL) 41 MATRIX_RE = re.compile("Matrix(\\d)") 42 43 VALID_UNIFORM_TYPES = [ 44 "uniform1f", 45 "uniform1fv", 46 "uniform1fv", 47 "uniform1i", 48 "uniform1iv", 49 "uniform1iv", 50 "uniform2f", 51 "uniform2fv", 52 "uniform2fv", 53 "uniform2i", 54 "uniform2iv", 55 "uniform2iv", 56 "uniform3f", 57 "uniform3fv", 58 "uniform3fv", 59 "uniform3i", 60 "uniform3iv", 61 "uniform3iv", 62 "uniform4f", 63 "uniform4fv", 64 "uniform4fv", 65 "uniform4i", 66 "uniform4iv", 67 "uniform4ivy", 68 "uniformMatrix2fv", 69 "uniformMatrix2fv", 70 "uniformMatrix3fv", 71 "uniformMatrix3fv", 72 "uniformMatrix4fv", 73 "uniformMatrix4fv", 74 ] 75 76 SUBSTITUTIONS = [ 77 ("uniformmat3fv", "uniformMatrix3fv"), 78 ("uniformmat4fv", "uniformMatrix4fv"), 79 ] 80 81 82 def Log(msg): 83 global VERBOSE 84 if VERBOSE: 85 print msg 86 87 88 def TransposeMatrix(values, dim): 89 size = dim * dim 90 count = len(values) / size 91 for m in range(0, count): 92 offset = m * size 93 for i in range(0, dim): 94 for j in range(i + 1, dim): 95 t = values[offset + i * dim + j] 96 values[offset + i * dim + j] = values[offset + j * dim + i] 97 values[offset + j * dim + i] = t 98 99 100 def GetValidTypeName(type_name): 101 global VALID_UNIFORM_TYPES 102 global SUBSTITUTIONS 103 for subst in SUBSTITUTIONS: 104 type_name = type_name.replace(subst[0], subst[1]) 105 if not type_name in VALID_UNIFORM_TYPES: 106 print "unknown type name: ", type_name 107 raise SyntaxError 108 return type_name 109 110 111 def WriteOpen(filename): 112 dirname = os.path.dirname(filename) 113 if len(dirname) > 0 and not os.path.exists(dirname): 114 os.makedirs(dirname) 115 return open(filename, "wb") 116 117 118 class TxtWriter(): 119 def __init__(self, filename): 120 self.filename = filename 121 self.lines = [] 122 123 def Write(self, line): 124 self.lines.append(line) 125 126 def Close(self): 127 if len(self.lines) > 0: 128 Log("Writing: %s" % self.filename) 129 f = WriteOpen(self.filename) 130 f.write("# this file is auto-generated. DO NOT EDIT.\n") 131 f.write("".join(self.lines)) 132 f.close() 133 134 135 def ReadFileAsLines(filename): 136 f = open(filename, "r") 137 lines = f.readlines() 138 f.close() 139 return [line.strip() for line in lines] 140 141 142 def ReadFile(filename): 143 f = open(filename, "r") 144 content = f.read() 145 f.close() 146 return content.replace("\r\n", "\n") 147 148 149 def Chunkify(list, chunk_size): 150 """divides an array into chunks of chunk_size""" 151 return [list[i:i + chunk_size] for i in range(0, len(list), chunk_size)] 152 153 154 def GetText(nodelist): 155 """Gets the text of from a list of nodes""" 156 rc = [] 157 for node in nodelist: 158 if node.nodeType == node.TEXT_NODE: 159 rc.append(node.data) 160 return ''.join(rc) 161 162 163 def GetElementText(node, name): 164 """Gets the text of an element""" 165 elements = node.getElementsByTagName(name) 166 if len(elements) > 0: 167 return GetText(elements[0].childNodes) 168 else: 169 return None 170 171 172 def GetBoolElement(node, name): 173 text = GetElementText(node, name) 174 return text.lower() == "true" 175 176 177 def GetModel(node): 178 """Gets the model""" 179 model = GetElementText(node, "model") 180 if model and len(model.strip()) == 0: 181 elements = node.getElementsByTagName("model") 182 if len(elements) > 0: 183 model = GetElementText(elements[0], "filename") 184 return model 185 186 187 def RelativizePaths(base, paths, template): 188 """converts paths to relative paths""" 189 rels = [] 190 for p in paths: 191 #print "---" 192 #print "base: ", os.path.abspath(base) 193 #print "path: ", os.path.abspath(p) 194 relpath = os.path.relpath(os.path.abspath(p), os.path.dirname(os.path.abspath(base))).replace("\\", "/") 195 #print "rel : ", relpath 196 rels.append(template % relpath) 197 return "\n".join(rels) 198 199 200 def CopyFile(filename, src, dst): 201 s = os.path.abspath(os.path.join(os.path.dirname(src), filename)) 202 d = os.path.abspath(os.path.join(os.path.dirname(dst), filename)) 203 dst_dir = os.path.dirname(d) 204 if not os.path.exists(dst_dir): 205 os.makedirs(dst_dir) 206 shutil.copyfile(s, d) 207 208 209 def CopyShader(filename, src, dst): 210 s = os.path.abspath(os.path.join(os.path.dirname(src), filename)) 211 d = os.path.abspath(os.path.join(os.path.dirname(dst), filename)) 212 text = ReadFile(s) 213 # By agreement with the Khronos OpenGL working group we are allowed 214 # to open source only the .vert and .frag files from the OpenGL ES 2.0 215 # conformance tests. All other files from the OpenGL ES 2.0 conformance 216 # tests are not included. 217 marker = "insert-copyright-here" 218 new_text = COMMENT_RE.sub(marker, text) 219 if new_text == text: 220 print "no matching license found:", s 221 raise RuntimeError 222 new_text = REMOVE_COPYRIGHT_RE.sub("", new_text) 223 glsl_license = '/*\n' + LICENSE + '\n*/' 224 new_text = new_text.replace(marker, glsl_license) 225 f = WriteOpen(d) 226 f.write(new_text) 227 f.close() 228 229 230 def IsOneOf(string, regexs): 231 for regex in regexs: 232 if re.match(regex, string): 233 return True 234 return False 235 236 237 def CheckForUnknownTags(valid_tags, node, depth=1): 238 """do a hacky check to make sure we're not missing something.""" 239 for child in node.childNodes: 240 if child.localName and not IsOneOf(child.localName, valid_tags[0]): 241 print "unsupported tag:", child.localName 242 print "depth:", depth 243 raise SyntaxError 244 else: 245 if len(valid_tags) > 1: 246 CheckForUnknownTags(valid_tags[1:], child, depth + 1) 247 248 249 def IsFileWeWant(filename): 250 for f in FILTERS: 251 if f.search(filename): 252 return True 253 return False 254 255 256 class TestReader(): 257 """class to read and parse tests""" 258 259 def __init__(self, basepath): 260 self.tests = [] 261 self.modes = {} 262 self.patterns = {} 263 self.basepath = basepath 264 265 def Print(self, msg): 266 if self.verbose: 267 print msg 268 269 def MakeOutPath(self, filename): 270 relpath = os.path.relpath(os.path.abspath(filename), os.path.dirname(os.path.abspath(self.basepath))) 271 return relpath 272 273 def ReadTests(self, filename): 274 """reads a .run file and parses.""" 275 Log("reading %s" % filename) 276 outname = self.MakeOutPath(filename + ".txt") 277 f = TxtWriter(outname) 278 dirname = os.path.dirname(filename) 279 lines = ReadFileAsLines(filename) 280 count = 0 281 tests_data = [] 282 for line in lines: 283 if len(line) > 0 and not line.startswith("#"): 284 fname = os.path.join(dirname, line) 285 if line.endswith(".run"): 286 if self.ReadTests(fname): 287 f.Write(line + ".txt\n") 288 count += 1 289 elif line.endswith(".test"): 290 tests_data.extend(self.ReadTest(fname)) 291 else: 292 print "Error in %s:%d:%s" % (filename, count, line) 293 raise SyntaxError() 294 if len(tests_data): 295 global MAX_TESTS_PER_SET 296 sets = Chunkify(tests_data, MAX_TESTS_PER_SET) 297 id = 1 298 for set in sets: 299 suffix = "_%03d_to_%03d" % (id, id + len(set) - 1) 300 test_outname = self.MakeOutPath(filename + suffix + ".html") 301 if os.path.basename(test_outname).startswith("input.run"): 302 dname = os.path.dirname(test_outname) 303 folder_name = os.path.basename(dname) 304 test_outname = os.path.join(dname, folder_name + suffix + ".html") 305 self.WriteTests(filename, test_outname, {"tests":set}) 306 f.Write(os.path.basename(test_outname) + "\n") 307 id += len(set) 308 count += 1 309 f.Close() 310 return count 311 312 def ReadTest(self, filename): 313 """reads a .test file and parses.""" 314 Log("reading %s" % filename) 315 dom = parse(filename) 316 tests = dom.getElementsByTagName("test") 317 tests_data = [] 318 outname = self.MakeOutPath(filename + ".html") 319 for test in tests: 320 if not IsFileWeWant(filename): 321 self.CopyShaders(test, filename, outname) 322 else: 323 test_data = self.ProcessTest(test, filename, outname, len(tests_data)) 324 if test_data: 325 tests_data.append(test_data) 326 return tests_data 327 328 def ProcessTest(self, test, filename, outname, id): 329 """Process a test""" 330 mode = test.getAttribute("mode") 331 pattern = test.getAttribute("pattern") 332 self.modes[mode] = 1 333 self.patterns[pattern] = 1 334 Log ("%d: mode: %s pattern: %s" % (id, mode, pattern)) 335 method = getattr(self, 'Process_' + pattern) 336 test_data = method(test, filename, outname) 337 if test_data: 338 test_data["pattern"] = pattern 339 return test_data 340 341 def WriteTests(self, filename, outname, tests_data): 342 Log("Writing %s" % outname) 343 template = """<!DOCTYPE html> 344 <!-- this file is auto-generated. DO NOT EDIT. --> 345 %(license)s 346 <html> 347 <head> 348 <meta charset="utf-8"> 349 <title>WebGL GLSL conformance test: %(title)s</title> 350 %(css)s 351 %(scripts)s 352 </head> 353 <body> 354 <canvas id="example" width="500" height="500" style="width: 16px; height: 16px;"></canvas> 355 <div id="description"></div> 356 <div id="console"></div> 357 </body> 358 <script> 359 "use strict"; 360 OpenGLESTestRunner.run(%(tests_data)s); 361 var successfullyParsed = true; 362 </script> 363 </html> 364 """ 365 css = [ 366 "../../resources/js-test-style.css", 367 "../../resources/ogles-tests.css", 368 ] 369 scripts = [ 370 "../../resources/js-test-pre.js", 371 "../../resources/webgl-test-utils.js", 372 "ogles-utils.js", 373 ] 374 css_html = RelativizePaths(outname, css, '<link rel="stylesheet" href="%s" />') 375 scripts_html = RelativizePaths(outname, scripts, '<script src="%s"></script>') 376 377 html_license = '<!--\n' + LICENSE + '\n-->' 378 f = WriteOpen(outname) 379 f.write(template % { 380 "license": html_license, 381 "css": css_html, 382 "scripts": scripts_html, 383 "title": os.path.basename(outname), 384 "tests_data": json.dumps(tests_data, indent=2) 385 }) 386 f.close() 387 388 389 def CopyShaders(self, test, filename, outname): 390 """For tests we don't actually support yet, at least copy the shaders""" 391 shaders = test.getElementsByTagName("shader") 392 for shader in shaders: 393 for name in ["vertshader", "fragshader"]: 394 s = GetElementText(shader, name) 395 if s and s != "empty": 396 CopyShader(s, filename, outname) 397 398 # 399 # pattern handlers. 400 # 401 402 def Process_compare(self, test, filename, outname): 403 global MATRIX_RE 404 405 valid_tags = [ 406 ["shader", "model", "glstate"], 407 ["uniform", "vertshader", "fragshader", "filename", "depthrange"], 408 ["name", "count", "transpose", "uniform*", "near", "far"], 409 ] 410 CheckForUnknownTags(valid_tags, test) 411 412 # parse the test 413 shaders = test.getElementsByTagName("shader") 414 shaderInfos = [] 415 for shader in shaders: 416 v = GetElementText(shader, "vertshader") 417 f = GetElementText(shader, "fragshader") 418 CopyShader(v, filename, outname) 419 CopyShader(f, filename, outname) 420 info = { 421 "vertexShader": v, 422 "fragmentShader": f, 423 } 424 shaderInfos.append(info) 425 uniformElems = shader.getElementsByTagName("uniform") 426 if len(uniformElems) > 0: 427 uniforms = {} 428 info["uniforms"] = uniforms 429 for uniformElem in uniformElems: 430 uniform = {"count": 1} 431 for child in uniformElem.childNodes: 432 if child.localName == None: 433 pass 434 elif child.localName == "name": 435 uniforms[GetText(child.childNodes)] = uniform 436 elif child.localName == "count": 437 uniform["count"] = int(GetText(child.childNodes)) 438 elif child.localName == "transpose": 439 uniform["transpose"] = (GetText(child.childNodes) == "true") 440 else: 441 if "type" in uniform: 442 print "utype was:", uniform["type"], " found ", child.localName 443 raise SyntaxError 444 type_name = GetValidTypeName(child.localName) 445 uniform["type"] = type_name 446 valueText = GetText(child.childNodes).replace(",", " ") 447 uniform["value"] = [float(t) for t in valueText.split()] 448 m = MATRIX_RE.search(type_name) 449 if m: 450 # Why are these backward from the API?!?!? 451 TransposeMatrix(uniform["value"], int(m.group(1))) 452 data = { 453 "name": os.path.basename(outname), 454 "model": GetModel(test), 455 "referenceProgram": shaderInfos[1], 456 "testProgram": shaderInfos[0], 457 } 458 gl_states = test.getElementsByTagName("glstate") 459 if len(gl_states) > 0: 460 state = {} 461 data["state"] = state 462 for gl_state in gl_states: 463 for state_name in gl_state.childNodes: 464 if state_name.localName: 465 values = {} 466 for field in state_name.childNodes: 467 if field.localName: 468 values[field.localName] = GetText(field.childNodes) 469 state[state_name.localName] = values 470 return data 471 472 def Process_shaderload(self, test, filename, outname): 473 """no need for shaderload tests""" 474 self.CopyShaders(test, filename, outname) 475 476 def Process_extension(self, test, filename, outname): 477 """no need for extension tests""" 478 self.CopyShaders(test, filename, outname) 479 480 def Process_createtests(self, test, filename, outname): 481 Log("createtests Not implemented: %s" % filename) 482 self.CopyShaders(test, filename, outname) 483 484 def Process_GL2Test(self, test, filename, outname): 485 Log("GL2Test Not implemented: %s" % filename) 486 self.CopyShaders(test, filename, outname) 487 488 def Process_uniformquery(self, test, filename, outname): 489 Log("uniformquery Not implemented: %s" % filename) 490 self.CopyShaders(test, filename, outname) 491 492 def Process_egl_image_external(self, test, filename, outname): 493 """no need for egl_image_external tests""" 494 self.CopyShaders(test, filename, outname) 495 496 def Process_dismount(self, test, filename, outname): 497 Log("dismount Not implemented: %s" % filename) 498 self.CopyShaders(test, filename, outname) 499 500 def Process_build(self, test, filename, outname): 501 """don't need build tests""" 502 valid_tags = [ 503 ["shader", "compstat", "linkstat"], 504 ["vertshader", "fragshader"], 505 ] 506 CheckForUnknownTags(valid_tags, test) 507 508 shader = test.getElementsByTagName("shader") 509 if not shader: 510 return None 511 vs = GetElementText(shader[0], "vertshader") 512 fs = GetElementText(shader[0], "fragshader") 513 if vs and vs != "empty": 514 CopyShader(vs, filename, outname) 515 if fs and fs != "empty": 516 CopyShader(fs, filename, outname) 517 data = { 518 "name": os.path.basename(outname), 519 "compstat": bool(GetBoolElement(test, "compstat")), 520 "linkstat": bool(GetBoolElement(test, "linkstat")), 521 "testProgram": { 522 "vertexShader": vs, 523 "fragmentShader": fs, 524 }, 525 } 526 attach = test.getElementsByTagName("attach") 527 if len(attach) > 0: 528 data["attachError"] = GetElementText(attach[0], "attacherror") 529 return data 530 531 def Process_coverage(self, test, filename, outname): 532 Log("coverage Not implemented: %s" % filename) 533 self.CopyShaders(test, filename, outname) 534 535 def Process_attributes(self, test, filename, outname): 536 Log("attributes Not implemented: %s" % filename) 537 self.CopyShaders(test, filename, outname) 538 539 def Process_fixed(self, test, filename, outname): 540 """no need for fixed function tests""" 541 self.CopyShaders(test, filename, outname) 542 543 544 def main(argv): 545 """This is the main function.""" 546 global VERBOSE 547 548 parser = OptionParser() 549 parser.add_option( 550 "-v", "--verbose", action="store_true", 551 help="prints more output.") 552 553 (options, args) = parser.parse_args(args=argv) 554 555 if len(args) < 1: 556 pass # fix me 557 558 os.chdir(os.path.dirname(__file__) or '.') 559 560 VERBOSE = options.verbose 561 562 filename = args[0] 563 test_reader = TestReader(filename) 564 test_reader.ReadTests(filename) 565 566 567 if __name__ == '__main__': 568 sys.exit(main(sys.argv[1:]))