duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

check_regression_tests.py (9523B)


      1 import argparse
      2 import glob
      3 import sys
      4 import os
      5 import re
      6 import hashlib
      7 
      8 from pathlib import Path
      9 
     10 FILE_HEADER = """
     11 <!DOCTYPE html>
     12 <html>
     13 <head>
     14 <title>Comparison</title>
     15 <style>
     16 .modal {
     17   display: none;
     18   position: fixed;
     19   z-index: 1;
     20   padding-top: 100px;
     21   left: 0;
     22   top: 0;
     23   width: 100%;
     24   height: 100%;
     25   overflow: auto;
     26   background-color: black;
     27 }
     28 
     29 .modal-content {
     30   position: relative;
     31   margin: auto;
     32   padding: 0;
     33   width: 90%;
     34   max-width: 1200px;
     35 }
     36 
     37 .close {
     38   color: white;
     39   position: absolute;
     40   top: 10px;
     41   right: 25px;
     42   font-size: 35px;
     43   font-weight: bold;
     44 }
     45 
     46 .close:hover,
     47 .close:focus {
     48   color: #999;
     49   text-decoration: none;
     50   cursor: pointer;
     51 }
     52 
     53 .prev,
     54 .next {
     55   cursor: pointer;
     56   position: absolute;
     57   top: 50%;
     58   width: auto;
     59   padding: 16px;
     60   margin-top: -50px;
     61   color: white;
     62   font-weight: bold;
     63   font-size: 20px;
     64   transition: 0.6s ease;
     65   border-radius: 0 3px 3px 0;
     66   user-select: none;
     67   -webkit-user-select: none;
     68 }
     69 
     70 .next {
     71   right: 0;
     72   border-radius: 3px 0 0 3px;
     73 }
     74 
     75 .prev:hover,
     76 .next:hover {
     77   background-color: rgba(0, 0, 0, 0.8);
     78 }
     79 
     80 .item img {
     81     cursor: pointer;
     82 }
     83 
     84 #compareTitle {
     85     color: white;
     86     font-size: 20px;
     87     font-family: sans-serif;
     88     margin-bottom: 10px;
     89 }
     90 
     91 #compareCaption {
     92     color: white;
     93     font-family: sans-serif;
     94 }
     95 
     96 #compareState {
     97     display: block;
     98     position: absolute;
     99     right: 0;
    100     top: 0;
    101     color: red;
    102     font-family: sans-serif;
    103     font-size: 20px;
    104     font-weight: bold;
    105 }
    106 
    107 #compareImage {
    108     display: block;
    109     margin: 0 auto;
    110     width: 100%;
    111     height: auto;
    112 }
    113 </style>
    114 </head>
    115 <body>
    116 """
    117 
    118 FILE_FOOTER = """
    119 <div id="myModal" class="modal" tabindex="0">
    120   <span class="close cursor" onclick="closeModal()">&times;</span>
    121   <div class="modal-content">
    122     <div id="compareTitle"></div>
    123     <img id="compareImage" />
    124     <div id="compareState"></div>
    125     <a class="prev">&#10094;</a>
    126     <a class="next">&#10095;</a>
    127     <div id="compareCaption"></div>
    128   </div>
    129 </div>
    130 <script>
    131 /* Worst script known to man */
    132 /* Sources:
    133    https://www.w3schools.com/howto/howto_js_lightbox.asp
    134    https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
    135 */
    136 
    137 function openModal() {
    138   document.body.style.position = 'fixed';
    139   document.body.style.top = `-${window.scrollY}px`;
    140   document.getElementById("myModal").style.display = "block";
    141   document.getElementById("myModal").focus();
    142   setImageIndex(0);
    143 }
    144 
    145 function closeModal() {
    146     document.getElementById("myModal").style.display = "none";
    147     const scrollY = document.body.style.top;
    148     document.body.style.position = '';
    149     document.body.style.top = '';
    150     window.scrollTo(0, parseInt(scrollY || '0') * -1);
    151 }
    152 
    153 function isModalOpen() {
    154     return (document.getElementById("myModal").style.display == "block");
    155 }
    156 
    157 function formatLines(str) {
    158     let lines = str.split("\\n")
    159     lines = lines.filter(line => !line.startsWith("Difference in frames"))
    160     return lines.join("<br>")
    161 }
    162 
    163 function extractItem(elem) {
    164     var beforeSel = elem.querySelector(".before");
    165     var afterSel = elem.querySelector(".after");
    166     var preSel = elem.querySelector("pre");
    167     return {
    168         name: elem.querySelector("h1").innerText,
    169         beforeImg: beforeSel ? beforeSel.getAttribute("src") : null,
    170         afterImg: afterSel ? afterSel.getAttribute("src") : null,
    171         details: formatLines(preSel ? preSel.innerText : "")
    172     };
    173 }
    174 
    175 const items = [...document.querySelectorAll(".item")].map(extractItem)
    176 let currentImage = 0;
    177 let currentState = 0;
    178 
    179 function getImageIndexForUri(uri) {
    180     for (let i = 0; i < items.length; i++) {
    181         if (items[i].beforeImg == uri || items[i].afterImg == uri)
    182             return i;
    183     }
    184     return -1;
    185 }
    186 
    187 function setImageState(state) {
    188     const item = items[currentImage]
    189     const uri = (state === 0) ? item.beforeImg : item.afterImg;
    190     const stateText = (state === 0) ? "BEFORE" : "AFTER";
    191     const posText = "(" + (currentImage + 1).toString() + "/" + (items.length).toString() + ") ";
    192     document.getElementById("compareImage").setAttribute("src", uri);
    193     document.getElementById("compareState").innerText = stateText;
    194     document.getElementById("compareTitle").innerText = posText + item.name;
    195     document.getElementById("compareCaption").innerHTML = item.details;
    196     currentState = state;
    197 }
    198 
    199 function setImageIndex(index) {
    200     if (index < 0 || index > items.length)
    201         return;
    202 
    203     currentImage = index;
    204     setImageState(0);
    205 }
    206 
    207 function handleKey(key) {
    208     if (key == " ") {
    209         setImageState((currentState === 0) ? 1 : 0);
    210         return true;
    211     } else if (key == "ArrowLeft") {
    212         setImageIndex(currentImage - 1);
    213         return true;
    214     } else if (key == "ArrowRight") {
    215         setImageIndex(currentImage + 1);
    216         return true;
    217     } else if (key == "Escape") {
    218         closeModal();
    219         return true;
    220     } else {
    221         console.log(key);
    222         return false;
    223     }
    224 }
    225 
    226 document.getElementById("myModal").addEventListener("keydown", function(ev) {
    227     if (ev.defaultPrevented)
    228         return;
    229     
    230     if (handleKey(ev.key))
    231         ev.preventDefault();
    232 });
    233 
    234 document.querySelector("#myModal .prev").addEventListener("click", function() {
    235     setImageIndex(currentImage - 1);
    236 });
    237 document.querySelector("#myModal .next").addEventListener("click", function() {
    238     setImageIndex(currentImage + 1);
    239 });
    240 document.querySelectorAll(".item img").forEach(elem => elem.addEventListener("click", function() {
    241     if (!isModalOpen())
    242         openModal();
    243     setImageIndex(getImageIndexForUri(this.getAttribute("src")));
    244 }));
    245 </script>
    246 </body>
    247 </html>
    248 """
    249 
    250 MAX_DIFF_FRAMES = 9999
    251 
    252 outfile = None
    253 def write(line):
    254     outfile.write(line + "\n")
    255 
    256 
    257 def compare_frames(path1, path2):
    258     try:
    259         with open(path1, "rb") as f:
    260             hash1 = hashlib.md5(f.read()).digest()
    261         with open(path2, "rb") as f:
    262             hash2 = hashlib.md5(f.read()).digest()
    263 
    264         #print(hash1, hash2)
    265         return hash1 == hash2
    266     except:
    267         return False
    268 
    269 
    270 def check_regression_test(baselinedir, testdir, name):
    271     #print("Checking '%s'..." % name)
    272 
    273     dir1 = os.path.join(baselinedir, name)
    274     dir2 = os.path.join(testdir, name)
    275     if not os.path.isdir(dir2):
    276         #print("*** %s is missing in test set" % name)
    277         return False
    278 
    279     images = glob.glob(os.path.join(dir1, "frame_*.png"))
    280     diff_frames = []
    281     first_fail = True
    282     has_any = False
    283     
    284     for imagepath in images:
    285         imagename = Path(imagepath).name
    286         matches = re.match("frame_([0-9]+).png", imagename)
    287         if matches is None:
    288             continue
    289 
    290         framenum = int(matches[1])
    291 
    292         path1 = os.path.join(dir1, imagename)
    293         path2 = os.path.join(dir2, imagename)
    294         if not os.path.isfile(path2):
    295             print("--- Frame %u for %s is missing in test set" % (framenum, name))
    296             if first_fail:
    297                 write("<div class=\"item\">")
    298                 write("<h1>{}</h1>".format(name))
    299                 write("<table width=\"100%\">")
    300                 first_fail = False
    301             write("<p>--- Frame %u for %s is missing in test set</p>" % (framenum, name))
    302             continue
    303 
    304         has_any = True
    305         if not compare_frames(path1, path2):
    306             diff_frames.append(framenum)
    307 
    308             if first_fail:
    309                 write("<div class=\"item\">")
    310                 write("<h1>{}</h1>".format(name))
    311                 write("<table width=\"100%\">")
    312                 first_fail = False
    313 
    314             imguri1 = Path(path1).as_uri()
    315             imguri2 = Path(path2).as_uri()
    316             write("<tr><td colspan=\"2\">Frame %d</td></tr>" % (framenum))
    317             write("<tr><td><img class=\"before\" src=\"%s\" /></td><td><img class=\"after\" src=\"%s\" /></td></tr>" % (imguri1, imguri2))
    318 
    319             if len(diff_frames) == MAX_DIFF_FRAMES:
    320                 break
    321 
    322     if not first_fail:
    323         write("</table>")
    324         write("<pre>Difference in frames [%s] for %s</pre>" % (",".join(map(str, diff_frames)), name))
    325         write("</div>")
    326         print("*** Difference in frames [%s] for %s" % (",".join(map(str, diff_frames)), name))
    327         #assert has_any
    328 
    329     return len(diff_frames) == 0
    330 
    331 
    332 def check_regression_tests(baselinedir, testdir):
    333     gamedirs = glob.glob(baselinedir + "/*", recursive=False)
    334     
    335     success = 0
    336     failure = 0
    337 
    338     for gamedir in gamedirs:
    339         name = Path(gamedir).name
    340         if check_regression_test(baselinedir, testdir, name):
    341             success += 1
    342         else:
    343             failure += 1
    344 
    345     return (failure == 0)
    346 
    347 
    348 if __name__ == "__main__":
    349     parser = argparse.ArgumentParser(description="Check frame dump images for regression tests")
    350     parser.add_argument("-baselinedir", action="store", required=True, help="Directory containing baseline frames to check against")
    351     parser.add_argument("-testdir", action="store", required=True, help="Directory containing frames to check")
    352     parser.add_argument("-maxframes", type=int, action="store", required=False, default=9999, help="Max frames to compare")
    353     parser.add_argument("outfile", action="store", help="The file to write the output to")
    354 
    355     args = parser.parse_args()
    356     MAX_DIFF_FRAMES = args.maxframes
    357 
    358     outfile = open(args.outfile, "w")
    359     write(FILE_HEADER)
    360 
    361     if not check_regression_tests(os.path.realpath(args.baselinedir), os.path.realpath(args.testdir)):
    362         write(FILE_FOOTER)
    363         outfile.close()
    364         sys.exit(1)
    365     else:
    366         outfile.close()
    367         os.remove(args.outfile)
    368         sys.exit(0)
    369