Pruebas de regresión numérica.
-
20-08-2019 - |
Pregunta
Estoy trabajando en un código informático científico (escrito en C ++), y además de realizar pruebas unitarias para los componentes más pequeños, me gustaría hacer pruebas de regresión en algunos de los resultados numéricos comparándolos con un quot; conocido-bueno " respuesta de revisiones anteriores. Hay algunas características que me gustaría:
- Permitir comparar números con una tolerancia especificada (tanto para error de redondeo como para expectativas más flexibles)
- Capacidad para distinguir entre ints, dobles, etc., e ignorar el texto si es necesario
- Salida bien formateada para saber qué salió mal y dónde: en una tabla de datos de varias columnas, solo muestra la entrada de columna que difiere
- Volver
EXIT_SUCCESS
oEXIT_FAILURE
dependiendo de si los archivos coinciden
¿Hay algún buen script o aplicación que haga esto, o tendré que usar el mío en Python para leer y comparar archivos de salida? Seguramente no soy la primera persona con este tipo de requisitos.
[Lo siguiente no es estrictamente relevante, pero puede tener en cuenta la decisión de qué hacer. Utilizo CMake y su funcionalidad CTest incorporada para conducir pruebas unitarias que usan el marco de prueba de Google. Me imagino que no debería ser difícil agregar algunas add_custom_command
declaraciones en mi CMakeLists.txt
para llamar al software de regresión que necesito.]
Solución 3
Terminé escribiendo un script de Python para hacer más o menos lo que quería.
#!/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)
Otros consejos
Debería elegir PyUnit , que ahora forma parte de la biblioteca estándar en el nombre unittest
. Es compatible con todo lo que pediste. La verificación de tolerancia, por ejemplo, se realiza con assertAlmostEqual()
.
La utilidad ndiff puede estar cerca de lo que usted es buscando: es como diff, pero comparará archivos de texto de números con la tolerancia deseada.
Sé que llego bastante tarde a la fiesta, pero hace unos meses escribí la utilidad nrtest en un intento de facilitar este flujo de trabajo. Parece que también podría ayudarte.
Aquí hay una descripción general rápida. Cada prueba se define por sus archivos de entrada y sus archivos de salida esperados. Después de la ejecución, los archivos de salida se almacenan en un directorio de referencia portátil. Un segundo paso luego compara este punto de referencia con un punto de referencia. Una actualización reciente ha habilitado extensiones de usuario, por lo que puede definir funciones de comparación para sus datos personalizados.
Espero que ayude.