Path: blob/master/libmupen64plus/mupen64plus-core/tools/regtests/regression-video.py
2 views
#!/usr/bin/env python12#/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *3# * Mupen64plus - regression-video.py *4# * Mupen64Plus homepage: http://code.google.com/p/mupen64plus/ *5# * Copyright (C) 2008-2012 Richard Goedeken *6# * *7# * This program is free software; you can redistribute it and/or modify *8# * it under the terms of the GNU General Public License as published by *9# * the Free Software Foundation; either version 2 of the License, or *10# * (at your option) any later version. *11# * *12# * This program is distributed in the hope that it will be useful, *13# * but WITHOUT ANY WARRANTY; without even the implied warranty of *14# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *15# * GNU General Public License for more details. *16# * *17# * You should have received a copy of the GNU General Public License *18# * along with this program; if not, write to the *19# * Free Software Foundation, Inc., *20# * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *21# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */2223from optparse import OptionParser24from threading import Thread25from datetime import date26import subprocess27import commands28import shutil29import stat30import sys31import os3233# set global report string34report = "Mupen64Plus Regression Test report\n----------------------------------\n"3536#******************************************************************************37# main functions38#3940def main(rootdir, cfgfile, nobuild, noemail):41global report42# set up child directory paths43srcdir = os.path.join(rootdir, "source")44shotdir = os.path.join(rootdir, "current")45refdir = os.path.join(rootdir, "reference")46archivedir = os.path.join(rootdir, "archive")47# run the test procedure48tester = RegTester(rootdir, srcdir, shotdir)49rval = 050while True:51# Step 1: load the test config file52if not tester.LoadConfig(cfgfile):53rval = 154break55# Step 2: check out from Mercurial56if not nobuild:57if not tester.CheckoutSource(srcdir):58rval = 259break60# Step 3: run test builds61if not nobuild:62for modname in tester.modulesAndParams:63module = tester.modulesAndParams[modname]64if "testbuilds" not in module:65continue66modurl = module["url"]67modfilename = modurl.split('/')[-1]68testlist = [ name.strip() for name in module["testbuilds"].split(',') ]69makeparams = [ params.strip() for params in module["testbuildparams"].split(',') ]70if len(testlist) != len(makeparams):71report += "Config file error for test builds in %s. Build name list and makefile parameter list have different lengths.\n" % modname72testbuilds = min(len(testlist), len(makeparams))73for i in range(testbuilds):74buildname = testlist[i]75buildmake = makeparams[i]76BuildSource(srcdir, modfilename, modname, buildname, buildmake, module["outputfiles"], True)77# Step 4: build the binary for the video regression test78if not nobuild:79for modname in tester.modulesAndParams:80module = tester.modulesAndParams[modname]81modurl = module["url"]82modfilename = modurl.split('/')[-1]83videobuild = module["videobuild"]84videomake = module["videobuildparams"]85if not BuildSource(srcdir, modfilename, modname, videobuild, videomake, module["outputfiles"], False):86rval = 387break88if rval != 0:89break90# Step 5: run the tests, check the results91if not tester.RunTests():92rval = 493break94if not tester.CheckResults(refdir):95rval = 596break97# test procedure is finished98break99# Step 6: send email report and archive the results100if not noemail:101if not tester.SendReport():102rval = 6103if not tester.ArchiveResults(archivedir):104rval = 7105# all done with test process106return rval107108#******************************************************************************109# Checkout & build functions110#111112def BuildSource(srcdir, moddir, modname, buildname, buildmake, outputfiles, istest):113global report114makepath = os.path.join(srcdir, moddir, "projects", "unix")115# print build report message and clear counters116testbuildcommand = "make -C %s %s" % (makepath, buildmake)117if istest:118report += "Running %s test build \"%s\"\n" % (modname, buildname)119else:120report += "Building %s \"%s\" for video test\n" % (modname, buildname)121warnings = 0122errors = 0123# run make and capture the output124output = commands.getoutput(testbuildcommand)125makelines = output.split("\n")126# print warnings and errors127for line in makelines:128if "error:" in line:129report += " " + line + "\n"130errors += 1131if "warning:" in line:132report += " " + line + "\n"133warnings += 1134report += "%i errors. %i warnings.\n" % (errors, warnings)135if errors > 0 and not istest:136return False137# check for output files138for filename in outputfiles.split(','):139if not os.path.exists(os.path.join(makepath, filename)):140report += "Build failed: '%s' not found\n" % filename141errors += 1142if errors > 0 and not istest:143return False144# clean up if this was a test145if istest:146os.system("make -C %s clean" % makepath)147# if this wasn't a test, then copy our output files and data files148if not istest:149for filename in outputfiles.split(','):150shutil.move(os.path.join(makepath, filename), srcdir)151datapath = os.path.join(srcdir, moddir, "data")152if os.path.isdir(datapath):153copytree(datapath, os.path.join(srcdir, "data"))154# build was successful!155return True156157#******************************************************************************158# Test execution classes159#160class RegTester:161def __init__(self, rootdir, bindir, screenshotdir):162self.rootdir = rootdir163self.bindir = bindir164self.screenshotdir = screenshotdir165self.generalParams = { }166self.gamesAndParams = { }167self.modulesAndParams = { }168self.videoplugins = [ "mupen64plus-video-rice.so" ]169self.thisdate = str(date.today())170171def LoadConfig(self, filename):172global report173# read the config file174report += "\nLoading regression test configuration.\n"175try:176cfgfile = open(os.path.join(self.rootdir, filename), "r")177cfglines = cfgfile.read().split("\n")178cfgfile.close()179except Exception, e:180report += "Error in RegTestConfigParser::LoadConfig(): %s" % e181return False182# parse the file183GameFilename = None184ModuleName = None185for line in cfglines:186# strip leading and trailing whitespace187line = line.strip()188# test for comment189if len(line) == 0 or line[0] == '#':190continue191# test for new game filename192if line[0] == '[' and line [-1] == ']':193GameFilename = line[1:-1]194if GameFilename in self.gamesAndParams:195report += " Warning: Config file '%s' contains duplicate game entry '%s'\n" % (filename, GameFilename)196else:197self.gamesAndParams[GameFilename] = { }198continue199# test for new source module build200if line[0] == '{' and line [-1] == '}':201ModuleName = line[1:-1]202if ModuleName in self.modulesAndParams:203report += " Warning: Config file '%s' contains duplicate source module '%s'\n" % (filename, ModuleName)204else:205self.modulesAndParams[ModuleName] = { }206continue207# print warning and continue if it's not a (key = value) pair208pivot = line.find('=')209if pivot == -1:210report += " Warning: Config file '%s' contains unrecognized line: '%s'\n" % (filename, line)211continue212# parse key, value213key = line[:pivot].strip().lower()214value = line[pivot+1:].strip()215if ModuleName is None:216paramDict = self.generalParams217elif GameFilename is None:218paramDict = self.modulesAndParams[ModuleName]219else:220paramDict = self.gamesAndParams[GameFilename]221if key in paramDict:222report += " Warning: duplicate key '%s'\n" % key223continue224paramDict[key] = value225# check for required parameters226if "rompath" not in self.generalParams:227report += " Error: rompath is not given in config file\n"228return False229# config is loaded230return True231232def CheckoutSource(self, srcdir):233global report234# remove any current source directory235if not deltree(srcdir):236return False237os.mkdir(srcdir)238os.mkdir(os.path.join(srcdir, "data"))239# loop through all of the source modules240for modname in self.modulesAndParams:241module = self.modulesAndParams[modname]242if "url" not in module:243report += "Error: no Hg repository URL for module %s\n\n" % modname244return False245modurl = module["url"]246modfilename = modurl.split("/")[-1]247# call Hg to checkout Mupen64Plus source module248output = commands.getoutput("hg clone --cwd %s %s" % (srcdir, modurl))249# parse the output250lastline = output.split("\n")[-1]251if "0 files unresolved" not in lastline:252report += "Hg Error: %s\n\n" % lastline253return False254# get the revision info255RevFound = False256output = commands.getoutput("hg tip -R %s" % os.path.join(srcdir, modfilename))257for line in output.split('\n'):258words = line.split()259if len(words) == 2 and words[0] == 'changeset:':260report += "Hg Checkout %s: changeset %s\n" % (modfilename, words[1])261RevFound = True262if not RevFound:263report += "Hg Error: couldn't find revision information\n\n"264return False265return True266267def RunTests(self):268global report269rompath = self.generalParams["rompath"]270if not os.path.exists(rompath):271report += " Error: ROM directory '%s' does not exist!\n" % rompath272return False273# Remove any current screenshot directory274if not deltree(self.screenshotdir):275return False276# Data initialization and start message277os.mkdir(self.screenshotdir)278for plugin in self.videoplugins:279videoname = plugin[:plugin.find('.')]280os.mkdir(os.path.join(self.screenshotdir, videoname))281report += "\nRunning regression tests on %i games.\n" % len(self.gamesAndParams)282# loop over each game filename given in regtest config file283for GameFilename in self.gamesAndParams:284GameParams = self.gamesAndParams[GameFilename]285# if no screenshots parameter given for this game then skip it286if "screenshots" not in GameParams:287report += " Warning: no screenshots taken for game '%s'\n" % GameFilename288continue289# make a list of screenshots and check it290shotlist = [ str(int(framenum.strip())) for framenum in GameParams["screenshots"].split(',') ]291if len(shotlist) < 1 or (len(shotlist) == 1 and shotlist[0] == '0'):292report += " Warning: invalid screenshot list for game '%s'\n" % GameFilename293continue294# run a test for each video plugin295for plugin in self.videoplugins:296videoname = plugin[:plugin.find('.')]297# check if this plugin should be skipped298if "skipvideo" in GameParams:299skipit = False300skiplist = [ name.strip() for name in GameParams["skipvideo"].split(',') ]301for skiptag in skiplist:302if skiptag.lower() in plugin.lower():303skipit = True304if skipit:305continue306# construct the command line307exepath = os.path.join(self.bindir, "mupen64plus")308exeparms = [ "--corelib", os.path.join(self.bindir, "libmupen64plus.so.2") ]309exeparms += [ "--testshots", ",".join(shotlist) ]310exeparms += [ "--sshotdir", os.path.join(self.screenshotdir, videoname) ]311exeparms += [ "--plugindir", self.bindir ]312exeparms += [ "--datadir", os.path.join(self.bindir, "data") ]313myconfig = os.path.join(self.rootdir, "config")314exeparms += [ "--configdir", myconfig ]315exeparms += [ "--gfx", plugin ]316exeparms += [ "--emumode", "2" ]317exeparms += [ os.path.join(rompath, GameFilename) ]318# run it, but if it takes too long print an error and kill it319testrun = RegTestRunner(exepath, exeparms)320testrun.start()321testrun.join(60.0)322if testrun.isAlive():323report += " Error: Test run timed out after 60 seconds: '%s'\n" % " ".join(exeparms)324os.kill(testrun.pid, 9)325testrun.join(10.0)326327# all tests have been run328return True329330def CheckResults(self, refdir):331global report332# print message333warnings = 0334errors = 0335report += "\nChecking regression test results\n"336# get lists of files in the reference folders337refshots = { }338if not os.path.exists(refdir):339os.mkdir(refdir)340for plugin in self.videoplugins:341videoname = plugin[:plugin.find('.')]342videodir = os.path.join(refdir, videoname)343if not os.path.exists(videodir):344os.mkdir(videodir)345refshots[videoname] = [ ]346else:347refshots[videoname] = [ filename for filename in os.listdir(videodir) ]348# get lists of files produced by current test runs349newshots = { }350for plugin in self.videoplugins:351videoname = plugin[:plugin.find('.')]352videodir = os.path.join(self.screenshotdir, videoname)353if not os.path.exists(videodir):354newshots[videoname] = [ ]355else:356newshots[videoname] = [ filename for filename in os.listdir(videodir) ]357# make list of matching ref/test screenshots, and look for missing reference screenshots358checklist = { }359for plugin in self.videoplugins:360videoname = plugin[:plugin.find('.')]361checklist[videoname] = [ ]362for filename in newshots[videoname]:363if filename in refshots[videoname]:364checklist[videoname] += [ filename ]365else:366report += " Warning: reference screenshot '%s/%s' missing. Copying from current test run\n" % (videoname, filename)367shutil.copy(os.path.join(self.screenshotdir, videoname, filename), os.path.join(refdir, videoname))368warnings += 1369# look for missing test screenshots370for plugin in self.videoplugins:371videoname = plugin[:plugin.find('.')]372for filename in refshots[videoname]:373if filename not in newshots[videoname]:374report += " Error: Test screenshot '%s/%s' missing.\n" % (videoname, filename)375errors += 1376# do image comparisons377for plugin in self.videoplugins:378videoname = plugin[:plugin.find('.')]379for filename in checklist[videoname]:380refimage = os.path.join(refdir, videoname, filename)381testimage = os.path.join(self.screenshotdir, videoname, filename)382diffimage = os.path.join(self.screenshotdir, videoname, os.path.splitext(filename)[0] + "_DIFF.png")383cmd = ("/usr/bin/compare", "-metric", "PSNR", refimage, testimage, diffimage)384pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout385similarity = pipe.read().strip()386pipe.close()387try:388db = float(similarity)389except:390db = 0391if db > 60.0:392os.unlink(diffimage)393else:394report += " Warning: test image '%s/%s' does not match reference. PSNR = %s\n" % (videoname, filename, similarity)395warnings += 1396# give report and return397report += "%i errors. %i warnings.\n" % (errors, warnings)398return True399400def SendReport(self):401global report402# if there are no email addresses in the config file, then just we're done403if "sendemail" not in self.generalParams:404return True405if len(self.generalParams["sendemail"]) < 5:406return True407# construct the email message header408emailheader = "To: %s\n" % self.generalParams["sendemail"]409emailheader += "From: [email protected]\n"410emailheader += "Subject: %s Regression Test Results for Mupen64Plus\n" % self.thisdate411emailheader += "Reply-to: [email protected]\n"412emailheader += "Content-Type: text/plain; charset=UTF-8\n"413emailheader += "Content-Transfer-Encoding: 8bit\n\n"414# open a pipe to sendmail and dump our report415try:416pipe = subprocess.Popen(("/usr/sbin/sendmail", "-t"), stdin=subprocess.PIPE).stdin417pipe.write(emailheader)418pipe.write(report)419pipe.close()420except Exception, e:421report += "Exception encountered when calling sendmail: '%s'\n" % e422report += "Email header:\n%s\n" % emailheader423return False424return True425426def ArchiveResults(self, archivedir):427global report428# create archive dir if it doesn't exist429if not os.path.exists(archivedir):430os.mkdir(archivedir)431# move the images into a subdirectory of 'archive' given by date432subdir = os.path.join(archivedir, self.thisdate)433if os.path.exists(subdir):434if not deltree(subdir):435return False436if os.path.exists(self.screenshotdir):437shutil.move(self.screenshotdir, subdir)438# copy the report into the archive directory439f = open(os.path.join(archivedir, "report_%s.txt" % self.thisdate), "w")440f.write(report)441f.close()442# archival is complete443return True444445446class RegTestRunner(Thread):447def __init__(self, exepath, exeparms):448self.exepath = exepath449self.exeparms = exeparms450self.pid = 0451self.returnval = None452Thread.__init__(self)453454def run(self):455# start the process456testprocess = subprocess.Popen([self.exepath] + self.exeparms)457# get the PID of the new test process458self.pid = testprocess.pid459# wait for the test to complete460self.returnval = testprocess.wait()461462463#******************************************************************************464# Generic helper functions465#466467def deltree(dirname):468global report469if not os.path.exists(dirname):470return True471try:472for path in (os.path.join(dirname, filename) for filename in os.listdir(dirname)):473if os.path.isdir(path):474if not deltree(path):475return False476else:477os.unlink(path)478os.rmdir(dirname)479except Exception, e:480report += "Error in deltree(): %s\n" % e481return False482483return True484485def copytree(srcpath, dstpath):486if not os.path.isdir(srcpath) or not os.path.isdir(dstpath):487return False488for filename in os.listdir(srcpath):489filepath = os.path.join(srcpath, filename)490if os.path.isdir(filepath):491subdstpath = os.path.join(dstpath, filename)492os.mkdir(subdstpath)493copytree(filepath, subdstpath)494else:495shutil.copy(filepath, dstpath)496return True497498#******************************************************************************499# main function call for standard script execution500#501502if __name__ == "__main__":503# parse the command-line arguments504parser = OptionParser()505parser.add_option("-n", "--nobuild", dest="nobuild", default=False, action="store_true",506help="Assume source code is present; don't check out and build")507parser.add_option("-e", "--noemail", dest="noemail", default=False, action="store_true",508help="don't send email or archive results")509parser.add_option("-t", "--testpath", dest="testpath",510help="Set root of testing directory to PATH", metavar="PATH")511parser.add_option("-c", "--cfgfile", dest="cfgfile", default="daily-tests.cfg",512help="Use regression test config file FILE", metavar="FILE")513(opts, args) = parser.parse_args()514# check test path515if opts.testpath is None:516# change directory to the directory containing this script and set root test path to "."517scriptdir = os.path.dirname(sys.argv[0])518os.chdir(scriptdir)519rootdir = "."520else:521rootdir = opts.testpath522# call the main function523rval = main(rootdir, opts.cfgfile, opts.nobuild, opts.noemail)524sys.exit(rval)525526527528529