1#!/usr/bin/env python3 2# 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6# 7# Write a Mochitest manifest for WebGL conformance test files. 8 9import os 10from pathlib import Path 11import re 12import shutil 13 14# All paths in this file are based where this file is run. 15WRAPPER_TEMPLATE_FILE = "mochi-wrapper.html.template" 16MANIFEST_TEMPLATE_FILE = "mochitest.ini.template" 17ERRATA_FILE = "mochitest-errata.ini" 18DEST_MANIFEST_PATHSTR = "generated-mochitest.ini" 19 20BASE_TEST_LIST_PATHSTR = "checkout/00_test_list.txt" 21GENERATED_PATHSTR = "generated" 22WEBGL2_TEST_MANGLE = "2_" 23PATH_SEP_MANGLING = "__" 24 25SUPPORT_DIRS = [ 26 "checkout", 27] 28 29EXTRA_SUPPORT_FILES = [ 30 "always-fail.html", 31 "iframe-passthrough.css", 32 "mochi-single.html", 33] 34 35ACCEPTABLE_ERRATA_KEYS = set( 36 [ 37 "fail-if", 38 "skip-if", 39 ] 40) 41 42 43def ChooseSubsuite(name): 44 # name: generated/test_2_conformance2__vertex_arrays__vertex-array-object.html 45 46 split = name.split("__") 47 48 version = "1" 49 if "/test_2_" in split[0]: 50 version = "2" 51 52 category = "core" 53 54 split[0] = split[0].split("/")[1] 55 if "deqp" in split[0]: 56 if version == "1": 57 # There's few enough that we'll just merge them with webgl1-ext. 58 category = "ext" 59 else: 60 category = "deqp" 61 elif "conformance" in split[0]: 62 if split[1] in ("glsl", "glsl3", "ogles"): 63 category = "ext" 64 elif split[1] == "textures" and split[2] != "misc": 65 category = "ext" 66 67 return "webgl{}-{}".format(version, category) 68 69 70######################################################################## 71# GetTestList 72 73 74def GetTestList(): 75 split = BASE_TEST_LIST_PATHSTR.rsplit("/", 1) 76 basePath = "." 77 testListFile = split[-1] 78 if len(split) == 2: 79 basePath = split[0] 80 81 allowWebGL1 = True 82 allowWebGL2 = True 83 alwaysFailEntry = TestEntry("always-fail.html", True, False) 84 testList = [alwaysFailEntry] 85 AccumTests(basePath, testListFile, allowWebGL1, allowWebGL2, testList) 86 87 for x in testList: 88 x.path = os.path.relpath(x.path, basePath).replace(os.sep, "/") 89 continue 90 91 return testList 92 93 94############################## 95# Internals 96 97 98def IsVersionLess(a, b): 99 aSplit = [int(x) for x in a.split(".")] 100 bSplit = [int(x) for x in b.split(".")] 101 102 while len(aSplit) < len(bSplit): 103 aSplit.append(0) 104 105 while len(aSplit) > len(bSplit): 106 bSplit.append(0) 107 108 for i in range(len(aSplit)): 109 aVal = aSplit[i] 110 bVal = bSplit[i] 111 112 if aVal == bVal: 113 continue 114 115 return aVal < bVal 116 117 return False 118 119 120class TestEntry: 121 def __init__(self, path, webgl1, webgl2): 122 self.path = path 123 self.webgl1 = webgl1 124 self.webgl2 = webgl2 125 return 126 127 128def AccumTests(pathStr, listFile, allowWebGL1, allowWebGL2, out_testList): 129 listPathStr = pathStr + "/" + listFile 130 131 listPath = listPathStr.replace("/", os.sep) 132 assert os.path.exists(listPath), "Bad `listPath`: " + listPath 133 134 with open(listPath, "r") as fIn: 135 lineNum = 0 136 for line in fIn: 137 lineNum += 1 138 139 line = line.rstrip() 140 if not line: 141 continue 142 143 curLine = line.lstrip() 144 if curLine.startswith("//"): 145 continue 146 if curLine.startswith("#"): 147 continue 148 149 webgl1 = allowWebGL1 150 webgl2 = allowWebGL2 151 while curLine.startswith("--"): # '--min-version 1.0.2 foo.html' 152 (flag, curLine) = curLine.split(" ", 1) 153 if flag == "--min-version": 154 (minVersion, curLine) = curLine.split(" ", 1) 155 if not IsVersionLess(minVersion, "2.0.0"): # >= 2.0.0 156 webgl1 = False 157 break 158 elif flag == "--max-version": 159 (maxVersion, curLine) = curLine.split(" ", 1) 160 if IsVersionLess(maxVersion, "2.0.0"): 161 webgl2 = False 162 break 163 elif flag == "--slow": 164 continue # TODO 165 else: 166 text = "Unknown flag '{}': {}:{}: {}".format( 167 flag, listPath, lineNum, line 168 ) 169 assert False, text 170 continue 171 172 assert webgl1 or webgl2 173 174 split = curLine.rsplit(".", 1) 175 assert len(split) == 2, "Bad split for `line`: " + line 176 (name, ext) = split 177 178 if ext == "html": 179 newTestFilePathStr = pathStr + "/" + curLine 180 entry = TestEntry(newTestFilePathStr, webgl1, webgl2) 181 out_testList.append(entry) 182 continue 183 184 assert ext == "txt", "Bad `ext` on `line`: " + line 185 186 split = curLine.rsplit("/", 1) 187 nextListFile = split[-1] 188 nextPathStr = "" 189 if len(split) != 1: 190 nextPathStr = split[0] 191 192 nextPathStr = pathStr + "/" + nextPathStr 193 AccumTests(nextPathStr, nextListFile, webgl1, webgl2, out_testList) 194 continue 195 196 return 197 198 199######################################################################## 200# Templates 201 202 203def FillTemplate(inFilePath, templateDict, outFilePath): 204 templateShell = ImportTemplate(inFilePath) 205 OutputFilledTemplate(templateShell, templateDict, outFilePath) 206 return 207 208 209def ImportTemplate(inFilePath): 210 with open(inFilePath, "r") as f: 211 return TemplateShell(f) 212 213 214def OutputFilledTemplate(templateShell, templateDict, outFilePath): 215 spanStrList = templateShell.Fill(templateDict) 216 217 with open(outFilePath, "w", newline="\n") as f: 218 f.writelines(spanStrList) 219 return 220 221 222############################## 223# Internals 224 225 226def WrapWithIndent(lines, indentLen): 227 split = lines.split("\n") 228 if len(split) == 1: 229 return lines 230 231 ret = [split[0]] 232 indentSpaces = " " * indentLen 233 for line in split[1:]: 234 ret.append(indentSpaces + line) 235 236 return "\n".join(ret) 237 238 239templateRE = re.compile("(%%.*?%%)") 240assert templateRE.split(" foo = %%BAR%%;") == [" foo = ", "%%BAR%%", ";"] 241 242 243class TemplateShellSpan: 244 def __init__(self, span): 245 self.span = span 246 247 self.isLiteralSpan = True 248 if self.span.startswith("%%") and self.span.endswith("%%"): 249 self.isLiteralSpan = False 250 self.span = self.span[2:-2] 251 252 return 253 254 def Fill(self, templateDict, indentLen): 255 if self.isLiteralSpan: 256 return self.span 257 258 assert self.span in templateDict, "'" + self.span + "' not in dict!" 259 260 filling = templateDict[self.span] 261 262 return WrapWithIndent(filling, indentLen) 263 264 265class TemplateShell: 266 def __init__(self, iterableLines): 267 spanList = [] 268 curLiteralSpan = [] 269 for line in iterableLines: 270 split = templateRE.split(line) 271 272 for cur in split: 273 isTemplateSpan = cur.startswith("%%") and cur.endswith("%%") 274 if not isTemplateSpan: 275 curLiteralSpan.append(cur) 276 continue 277 278 if curLiteralSpan: 279 span = "".join(curLiteralSpan) 280 span = TemplateShellSpan(span) 281 spanList.append(span) 282 curLiteralSpan = [] 283 284 assert len(cur) >= 4 285 286 span = TemplateShellSpan(cur) 287 spanList.append(span) 288 continue 289 continue 290 291 if curLiteralSpan: 292 span = "".join(curLiteralSpan) 293 span = TemplateShellSpan(span) 294 spanList.append(span) 295 296 self.spanList = spanList 297 return 298 299 # Returns spanStrList. 300 301 def Fill(self, templateDict): 302 indentLen = 0 303 ret = [] 304 for span in self.spanList: 305 span = span.Fill(templateDict, indentLen) 306 ret.append(span) 307 308 # Get next `indentLen`. 309 try: 310 lineStartPos = span.rindex("\n") + 1 311 312 # let span = 'foo\nbar' 313 # len(span) is 7 314 # lineStartPos is 4 315 indentLen = len(span) - lineStartPos 316 except ValueError: 317 indentLen += len(span) 318 continue 319 320 return ret 321 322 323######################################################################## 324# Output 325 326 327def IsWrapperWebGL2(wrapperPath): 328 return wrapperPath.startswith(GENERATED_PATHSTR + "/test_" + WEBGL2_TEST_MANGLE) 329 330 331def WriteWrapper(entryPath, webgl2, templateShell, wrapperPathAccum): 332 mangledPath = entryPath.replace("/", PATH_SEP_MANGLING) 333 maybeWebGL2Mangle = "" 334 if webgl2: 335 maybeWebGL2Mangle = WEBGL2_TEST_MANGLE 336 337 # Mochitests must start with 'test_' or similar, or the test 338 # runner will ignore our tests. 339 # The error text is "is not a valid test". 340 wrapperFileName = "test_" + maybeWebGL2Mangle + mangledPath 341 342 wrapperPath = GENERATED_PATHSTR + "/" + wrapperFileName 343 print("Adding wrapper: " + wrapperPath) 344 345 args = "" 346 if webgl2: 347 args = "?webglVersion=2" 348 349 templateDict = { 350 "TEST_PATH": entryPath, 351 "ARGS": args, 352 } 353 354 OutputFilledTemplate(templateShell, templateDict, wrapperPath) 355 356 if webgl2: 357 assert IsWrapperWebGL2(wrapperPath) 358 359 wrapperPathAccum.append(wrapperPath) 360 return 361 362 363def WriteWrappers(testEntryList): 364 templateShell = ImportTemplate(WRAPPER_TEMPLATE_FILE) 365 366 generatedDirPath = GENERATED_PATHSTR.replace("/", os.sep) 367 if not os.path.exists(generatedDirPath): 368 os.mkdir(generatedDirPath) 369 assert os.path.isdir(generatedDirPath) 370 371 wrapperPathList = [] 372 for entry in testEntryList: 373 if entry.webgl1: 374 WriteWrapper(entry.path, False, templateShell, wrapperPathList) 375 if entry.webgl2: 376 WriteWrapper(entry.path, True, templateShell, wrapperPathList) 377 continue 378 379 print("{} wrappers written.\n".format(len(wrapperPathList))) 380 return wrapperPathList 381 382 383kManifestRelPathStr = os.path.relpath(".", os.path.dirname(DEST_MANIFEST_PATHSTR)) 384kManifestRelPathStr = kManifestRelPathStr.replace(os.sep, "/") 385 386 387def ManifestPathStr(pathStr): 388 pathStr = kManifestRelPathStr + "/" + pathStr 389 return os.path.normpath(pathStr).replace(os.sep, "/") 390 391 392def WriteManifest(wrapperPathStrList, supportPathStrList): 393 destPathStr = DEST_MANIFEST_PATHSTR 394 print("Generating manifest: " + destPathStr) 395 396 errataMap = LoadErrata() 397 398 # DEFAULT_ERRATA 399 defaultSectionName = "DEFAULT" 400 401 defaultSectionLines = [] 402 if defaultSectionName in errataMap: 403 defaultSectionLines = errataMap[defaultSectionName] 404 del errataMap[defaultSectionName] 405 406 defaultSectionStr = "\n".join(defaultSectionLines) 407 408 # SUPPORT_FILES 409 supportPathStrList = [ManifestPathStr(x) for x in supportPathStrList] 410 supportPathStrList = sorted(supportPathStrList) 411 supportFilesStr = "\n".join(supportPathStrList) 412 413 # MANIFEST_TESTS 414 manifestTestLineList = [] 415 wrapperPathStrList = sorted(wrapperPathStrList) 416 for wrapperPathStr in wrapperPathStrList: 417 wrapperManifestPathStr = ManifestPathStr(wrapperPathStr) 418 sectionName = "[" + wrapperManifestPathStr + "]" 419 manifestTestLineList.append(sectionName) 420 421 errataLines = [] 422 423 subsuite = ChooseSubsuite(wrapperPathStr) 424 errataLines.append("subsuite = " + subsuite) 425 426 if wrapperPathStr in errataMap: 427 assert subsuite 428 errataLines += errataMap[wrapperPathStr] 429 del errataMap[wrapperPathStr] 430 431 manifestTestLineList += errataLines 432 continue 433 434 if errataMap: 435 print("Errata left in map:") 436 for x in errataMap.keys(): 437 print(" " * 4 + x) 438 assert False 439 440 manifestTestsStr = "\n".join(manifestTestLineList) 441 442 # Fill the template. 443 templateDict = { 444 "DEFAULT_ERRATA": defaultSectionStr, 445 "SUPPORT_FILES": supportFilesStr, 446 "MANIFEST_TESTS": manifestTestsStr, 447 } 448 449 destPath = destPathStr.replace("/", os.sep) 450 FillTemplate(MANIFEST_TEMPLATE_FILE, templateDict, destPath) 451 return 452 453 454############################## 455# Internals 456 457 458kManifestHeaderRegex = re.compile(r"[[]([^]]*)[]]") 459 460 461def LoadINI(path): 462 curSectionName = None 463 curSectionMap = {} 464 465 lineNum = 0 466 467 ret = {} 468 ret[curSectionName] = (lineNum, curSectionMap) 469 470 with open(path, "r") as f: 471 for line in f: 472 lineNum += 1 473 474 line = line.strip() 475 if not line: 476 continue 477 478 if line[0] in [";", "#"]: 479 continue 480 481 if line[0] == "[": 482 assert line[-1] == "]", "{}:{}".format(path, lineNum) 483 484 curSectionName = line[1:-1] 485 assert ( 486 curSectionName not in ret 487 ), "Line {}: Duplicate section: {}".format(lineNum, line) 488 489 curSectionMap = {} 490 ret[curSectionName] = (lineNum, curSectionMap) 491 continue 492 493 split = line.split("=", 1) 494 key = split[0].strip() 495 val = "" 496 if len(split) == 2: 497 val = split[1].strip() 498 499 curSectionMap[key] = (lineNum, val) 500 continue 501 502 return ret 503 504 505def LoadErrata(): 506 iniMap = LoadINI(ERRATA_FILE) 507 508 ret = {} 509 510 for (sectionName, (sectionLineNum, sectionMap)) in iniMap.items(): 511 curLines = [] 512 513 if sectionName is None: 514 continue 515 elif sectionName != "DEFAULT": 516 path = sectionName.replace("/", os.sep) 517 assert os.path.exists(path), "Errata line {}: Invalid file: {}".format( 518 sectionLineNum, sectionName 519 ) 520 521 for (key, (lineNum, val)) in sectionMap.items(): 522 assert key in ACCEPTABLE_ERRATA_KEYS, "Line {}: {}".format(lineNum, key) 523 524 curLine = "{} = {}".format(key, val) 525 curLines.append(curLine) 526 continue 527 528 ret[sectionName] = curLines 529 continue 530 531 return ret 532 533 534######################################################################## 535 536 537def GetSupportFileList(): 538 ret = EXTRA_SUPPORT_FILES[:] 539 540 for pathStr in SUPPORT_DIRS: 541 ret += GetFilePathListForDir(pathStr) 542 continue 543 544 for pathStr in ret: 545 path = pathStr.replace("/", os.sep) 546 assert os.path.exists(path), path + "\n\n\n" + "pathStr: " + str(pathStr) 547 continue 548 549 return ret 550 551 552def GetFilePathListForDir(baseDir): 553 ret = [] 554 for root, folders, files in os.walk(baseDir): 555 for f in files: 556 filePath = os.path.join(root, f) 557 filePath = filePath.replace(os.sep, "/") 558 ret.append(filePath) 559 560 return ret 561 562 563if __name__ == "__main__": 564 file_dir = Path(__file__).parent 565 os.chdir(str(file_dir)) 566 shutil.rmtree(file_dir / "generated", True) 567 568 testEntryList = GetTestList() 569 wrapperPathStrList = WriteWrappers(testEntryList) 570 571 supportPathStrList = GetSupportFileList() 572 WriteManifest(wrapperPathStrList, supportPathStrList) 573 574 print("Done!") 575