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