Численное регрессионное тестирование

Я работаю над научным вычислительным кодом (написанным на C++), и в дополнение к выполнению модульных тестов для более мелких компонентов я хотел бы провести регрессионное тестирование некоторых числовых выходных данных, сравнив их с «заведомо хорошим» ответ из предыдущих редакций. Есть несколько функций, которые я хотел бы:

  • Разрешить сравнение чисел с заданным допуском (как для ошибки округления, так и для более слабых ожиданий)
  • Возможность различать целые числа, двойные числа и т. д. и игнорировать текст при необходимости.
  • Хорошо отформатированный вывод, чтобы указать, что и где пошло не так: в многостолбцовой таблице данных показывать только ту запись столбца, которая отличается
  • Возвращает EXIT_SUCCESS или EXIT_FAILURE в зависимости от того, совпадают ли файлы

Существуют ли какие-либо хорошие сценарии или приложения, которые делают это, или мне придется создавать свои собственные на Python для чтения и сравнения выходных файлов? Конечно, я не первый человек с такими требованиями.

[Следующее не имеет строгого отношения к делу, но может повлиять на решение о том, что делать. Я использую CMake и его встроенную функциональность CTest для запуска модульных тестов, использующих платформу Google Test. Я полагаю, что не составит труда добавить несколько операторов add_custom_command в мой CMakeLists.txt для вызова любого необходимого мне регрессионного программного обеспечения.]


person Seth Johnson    schedule 28.06.2009    source источник
comment
Большая часть того, что вы описываете, является стандартными функциями модульного тестирования. Вы просите о чем-то помимо юниттеста?   -  person S.Lott    schedule 29.06.2009
comment
Модульное тестирование, как я понимаю, рассматривает минимальные тесты с априорными ответами. (Если я напишу функцию returnTwo, предназначенную для возврата значения, равного двум, я могу выполнить модульный тест, чтобы проверить правильность возвращаемого значения.) Под регрессионным тестированием я подразумеваю генерацию данных на гораздо более высоком уровне и сравнение данные, сгенерированные будущими версиями с этими более старыми данными.   -  person Seth Johnson    schedule 29.06.2009
comment
Извините, ваше впечатление от регрессионного тестирования не очень полезно. Вы не сравниваете эту версию с предыдущей версией. Вы сравниваете обе версии с известными хорошими результатами. Всегда можно создать модульный тест с волшебным ответом, полученным из предыдущей версии без объяснения причин. Это может служить проверкой того, что результаты предыдущей версии сохраняются, даже если никто не знает, правильны они или нет.   -  person S.Lott    schedule 30.06.2009
comment
Я ненавижу звучать аргументированно, но регрессионное тестирование в научных вычислениях — где на приблизительный ответ уравнения в частных производных может повлиять ошибка дискретизации, ошибка округления при вычислениях с плавающей запятой, статистическая ошибка, неполная сходимость и т. д. — отличается от в других вычислительных областях, где существуют точные ответы. Несмотря на то, что у меня есть эталонные данные с точностью до восьми цифр для сотни точек данных, чтобы решить их самостоятельно с такой точностью, потребуются часы. Таким образом, мои регрессионные тесты будут сравниваться с ранее найденным, менее точным приближением.   -  person Seth Johnson    schedule 01.07.2009
comment
@SethJohnson Я думаю, С. Лотт имел в виду именно то, что ты сказал. Он предлагал думать о вашем регрессионном тесте как о модульном тесте; и пусть модульный тест проверяет, что результаты новой версии достаточно близки к результатам новой версии (где понятие достаточно близко является частью модульного теста и может быть настолько сложным, насколько это необходимо).   -  person max    schedule 20.02.2012


Ответы (4)


Вам следует выбрать PyUnit, который теперь является частью стандартной библиотеки под названием < a href="http://docs.python.org/library/unittest.html" rel="nofollow noreferrer">unittest. Он поддерживает все, что вы просили. Например, проверка допуска выполняется с помощью assertAlmostEqual().

person wr.    schedule 28.06.2009
comment
Извините, я не дал понять раньше, но код, который я проверяю, не является кодом Python. Я бы написал внешний скрипт Python для чтения в файле данных и проверки значений там. - person Seth Johnson; 29.06.2009
comment
В этом случае scipy может помочь в чтении данных: scipy.org/Cookbook/InputOutput - person wr.; 29.06.2009

Утилита ndiff может быть близка к тому, что вы ищете: это похоже на diff, но оно будет сравнивать текстовые файлы чисел с желаемым допуском.

person Seth Johnson    schedule 01.07.2009

В итоге я написал скрипт на Python, чтобы делать более или менее то, что хотел.

#!/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)
person Seth Johnson    schedule 15.07.2009

Я знаю, что опоздал на вечеринку, но несколько месяцев назад я написал утилиту nrtest, пытаясь упростить этот рабочий процесс. Похоже, это может помочь и вам.

Вот краткий обзор. Каждый тест определяется своими входными файлами и ожидаемыми выходными файлами. После выполнения выходные файлы сохраняются в переносимом каталоге тестов. Затем на втором этапе этот тест сравнивается с эталонным тестом. Недавнее обновление включило пользовательские расширения, поэтому вы можете определить функции сравнения для своих пользовательских данных.

Я надеюсь, что это помогает.

person David Hall    schedule 12.03.2016