Test de régression numérique
-
20-08-2019 - |
Question
Je travaille sur un code informatique scientifique (écrit en C ++) et, en plus de la réalisation de tests unitaires pour les composants plus petits, je souhaite effectuer des tests de régression sur certaines sorties numériques en les comparant à un
- Autoriser la comparaison de nombres avec une tolérance spécifiée (pour l’erreur d’arrondi et les attentes plus souples)
- Possibilité de faire la distinction entre ints, doubles, etc. et d'ignorer le texte si nécessaire
- Sortie bien formatée pour indiquer ce qui ne va pas et où: dans un tableau de données à plusieurs colonnes, affichez uniquement l'entrée de colonne différente
- Retourne
EXIT_SUCCESS
ouEXIT_FAILURE
selon que les fichiers correspondent
Existe-t-il de bons scripts ou applications pour cela, ou devrai-je rouler le mien en Python pour lire et comparer les fichiers de sortie? Je ne suis sûrement pas la première personne à avoir ce genre d'exigences.
[Ce qui suit n’est pas strictement pertinent, mais il peut être pris en compte dans la décision. J'utilise CMake et sa fonctionnalité CTest intégrée pour piloter des tests unitaires utilisant le framework Google Test. J'imagine qu'il ne devrait pas être difficile d'ajouter quelques add_custom_command
déclarations dans mon CMakeLists.txt
appel du logiciel de régression dont j'ai besoin.]
La solution 3
J'ai fini par écrire un script Python pour faire plus ou moins ce que je voulais.
#!/usr/bin/env python
import sys
import re
from optparse import OptionParser
from math import fabs
splitPattern = re.compile(r',|\s+|;')
class FailObject(object):
def __init__(self, options):
self.options = options
self.failure = False
def fail(self, brief, full = ""):
print ">>>> ", brief
if options.verbose and full != "":
print " ", full
self.failure = True
def exit(self):
if (self.failure):
print "FAILURE"
sys.exit(1)
else:
print "SUCCESS"
sys.exit(0)
def numSplit(line):
list = splitPattern.split(line)
if list[-1] == "":
del list[-1]
numList = [float(a) for a in list]
return numList
def softEquiv(ref, target, tolerance):
if (fabs(target - ref) <= fabs(ref) * tolerance):
return True
#if the reference number is zero, allow tolerance
if (ref == 0.0):
return (fabs(target) <= tolerance)
#if reference is non-zero and it failed the first test
return False
def compareStrings(f, options, expLine, actLine, lineNum):
### check that they're a bunch of numbers
try:
exp = numSplit(expLine)
act = numSplit(actLine)
except ValueError, e:
# print "It looks like line %d is made of strings (exp=%s, act=%s)." \
# % (lineNum, expLine, actLine)
if (expLine != actLine and options.checkText):
f.fail( "Text did not match in line %d" % lineNum )
return
### check the ranges
if len(exp) != len(act):
f.fail( "Wrong number of columns in line %d" % lineNum )
return
### soft equiv on each value
for col in range(0, len(exp)):
expVal = exp[col]
actVal = act[col]
if not softEquiv(expVal, actVal, options.tol):
f.fail( "Non-equivalence in line %d, column %d"
% (lineNum, col) )
return
def run(expectedFileName, actualFileName, options):
# message reporter
f = FailObject(options)
expected = open(expectedFileName)
actual = open(actualFileName)
lineNum = 0
while True:
lineNum += 1
expLine = expected.readline().rstrip()
actLine = actual.readline().rstrip()
## check that the files haven't ended,
# or that they ended at the same time
if expLine == "":
if actLine != "":
f.fail("Tested file ended too late.")
break
if actLine == "":
f.fail("Tested file ended too early.")
break
compareStrings(f, options, expLine, actLine, lineNum)
#print "%3d: %s|%s" % (lineNum, expLine[0:10], actLine[0:10])
f.exit()
################################################################################
if __name__ == '__main__':
parser = OptionParser(usage = "%prog [options] ExpectedFile NewFile")
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose", default=True,
help="Don't print status messages to stdout")
parser.add_option("--check-text",
action="store_true", dest="checkText", default=False,
help="Verify that lines of text match exactly")
parser.add_option("-t", "--tolerance",
action="store", type="float", dest="tol", default=1.e-15,
help="Relative error when comparing doubles")
(options, args) = parser.parse_args()
if len(args) != 2:
print "Usage: numdiff.py EXPECTED ACTUAL"
sys.exit(1)
run(args[0], args[1], options)
Autres conseils
Vous devriez choisir PyUnit , qui fait maintenant partie de la bibliothèque standard sous le nom unittest
. Il prend en charge tout ce que vous avez demandé. La vérification de la tolérance, par exemple, est effectuée avec assertAlmostEqual()
.
L'utilitaire ndiff peut être proche de ce que vous êtes cherche: c'est comme diff, mais il comparera les fichiers texte de nombres à la tolérance souhaitée.
Je sais que je suis assez en retard pour la fête, mais il y a quelques mois, j'ai écrit l'utilitaire nrtest dans une tentative de rendre ce flux de travail plus facile. Il semble que cela pourrait vous aider aussi.
Voici un aperçu rapide. Chaque test est défini par ses fichiers d'entrée et ses fichiers de sortie attendus. Après exécution, les fichiers de sortie sont stockés dans un répertoire de référence portable. Une seconde étape compare ensuite ce repère à un repère de référence. Une mise à jour récente a activé les extensions utilisateur. Vous pouvez donc définir des fonctions de comparaison pour vos données personnalisées.
J'espère que cela vous aidera.