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