Контроль целостности кода функций

в 10:58, , рубрики: Блог компании Positive Technologies, тестирование, метки:

В процессе разработки многокомпонентной системы автоматизированного тестирования сканера безопасности мы столкнулись с проблемой контроля целостности кода отдельных тестовых функций и проведения ревизий.

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

Между тем в процессе разработки других тестовых функций часто возникает необходимость рефакторинга. Причем этот процесс по невнимательности тестировщика-автоматизатора может затронуть и уже готовые отлаженные тесты.

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

Чтобы избежать негативных последствий подобных ситуаций, мы разработали механизм ревизий кода тестовых функций, с помощью которого можно одновременно и контролировать целостность функций, и дублировать их код.

Контроль целостности кода функций

Механизм

Для каждой функции может быть определена ревизия, набор из хэша и кода функции:

(func_hash, func_source)

Все критические функции могут быть добавлены в словарь ревизий:

{"funcName1": (funcName1_hash, funcName1_source),
"funcName2": (funcName2_hash, funcName2_source), ...}

Для нас, к примеру, критическими являются все функции с уже разработанными тестами. Хранить все ревизии можно в специальном текстовом файле (файл ревизий), в котором хранится список с датой последней ревизии и словарем ревизий:

[revision's last date-n-time, {revisions}]

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

Разумеется, существуют и альтернативные варианты решения проблемы, например, инспекции кода и использование инструментов в репозиториях (например, GIT, SVN). Однако инспекции бесполезны в случае внесения автоматических изменений в сотни тестов, а отслеживание изменений в коде с помощью инструментов репозитория после нескольких мержей — процесс трудоемкий и долгий. Кроме того, обычно на тестовые функции не пишут модульные тесты, однако необходимость контроля качества и неизменности функций сохраняется — эту проблему также позволяет решить механизм ревизий.

Код

Для реализации описанной выше идеи на Python был написан небольшой модуль FileRevision.py. Имеющийся в нем класс Revision() можно импортировать в свой проект и добавить ревизии для нужных именно вам функций.

Немного доработав модуль, можно дополнительно реализовать, к примеру, сжатие файла ревизий, хотя для небольших по объему проектов это не так уж критично.

Код доступен по ссылке.

Реализация модуля

class Revision():

__init__() # Инициализация параметров

def __init__(self, fileRevision='revision.txt'):
    self.fileRevision = fileRevision
    self.mainRevision = self._ReadFromFile(self.fileRevision)  # get main revision first

_ReadFromFile() # Получение ревизий из файла

def _ReadFromFile(self, file=None):
    """
    Helper function that parse and return revision from file.
    """
    revision = [None, {}]
    if file == None:
        file = self.fileRevision
    try:
        if os.path.exists(file) and os.path.isfile(file):
            with open(file) as fH:
                revision = eval(fH.read())
    except:
        traceback.print_exc()
    finally:
        return revision

_WriteToFile() # Запись ревизий в файл.

def _WriteToFile(self, revision=[None, {}], file=None):
    """
    Helper procedure than trying to write given revision to file.
    """
    status = False
    if file == None:
        file = self.fileRevision
    try:
        with open(file, "w") as fH:
            fH.write(str(revision))
        status = True
    except:
        traceback.print_exc()
    finally:
        return status

_GetOld() # Получение предыдущей ревизии для функции.

def _GetOld(self, func=None):
    """
    Get old revision for given function and return tuple: (old_hash, old_source).
    """
    funcHashOld = None  # old code is None if function not exist in previous revision
    funcSourceOld = None  # old hash is None if function not exist in previous revision
    try:
        if func.__name__ in self.mainRevision[1]:
            funcHashOld = self.mainRevision[1][func.__name__][0]  # field with old hash of function
            funcSourceOld = self.mainRevision[1][func.__name__][1]  # field with old code of function
    except:
        traceback.print_exc()
    finally:
        return (funcHashOld, funcSourceOld)

_GetNew() # Получение новой ревизии для функции.

def _GetNew(self, func=None):
    """
    Get new revision for given function and return tuple: (new_hash, new_source).
    """
    funcSourceNew = None  # if function doesn't exist, its also doesn't have code
    funcHashNew = None  # hash is None if function not exist
    try:
        funcSourceNew = inspect.getsource(func)  # get function's source
        funcHashNew = hash(funcSourceNew)  # new hash of function
    except:
        traceback.print_exc()
    finally:
        return (funcHashNew, funcSourceNew)

_Similar() # Сравнение двух ревизий.

def _Similar(self, hashOld, sourceOld, hashNew, sourceNew):
    """
    Checks if given params for modified then return tuple with revision's diff:
    (old_revision, new_revision), otherwise return None.
    """
    similar = True  # old and new functions are similar, by default
    if hashNew != hashOld:
        if sourceOld != sourceNew:
            similar = False # modified if hashes are not similar and functions not contains similar code
    return similar

Update() # Обновление ревизии для указанной функции.

def Update(self, func=None):
    """
    Set new revision for function.
    revision = [revision date-n-time,
                {"funcName1": (funcName1_hash, funcName1_source),
                {"funcName2": (funcName2_hash, funcName2_source), ...}]
    """
    status = False
    if func:
        try:
            funcSourceNew = inspect.getsource(func)  # get function's source
            funcHashNew = hash(funcSourceNew)  # new hash of function
            revisionDateNew = datetime.now().strftime('%d.%m.%Y %H:%M:%S')  # revision's date
            funcRevisionNew = {func.__name__: [funcHashNew, funcSourceNew]}  # form for function's revision
            self.mainRevision[0] = revisionDateNew  # set new date for main revision
            self.mainRevision[1].update(funcRevisionNew)  # add function's revision to main revision
            if self._WriteToFile(self.mainRevision):  # write main revision to file
                status = True
        except:
            traceback.print_exc()
        finally:
            return status

DeleteAll() # Удаление всех ревизий из файла.

def DeleteAll(self):
    """
    Helper function that parse and return revision from file.
    """
    status = False
    try:
        self.mainRevision = [None, {}]  # clean revision
        if self._WriteToFile(self.mainRevision):  # write main revision to file
            status = True
    except:
        traceback.print_exc()
    finally:
        return status

ShowOld() # Вывод информации о предыдущей ревизии для функции.

def ShowOld(self, func=None):
    """
    Function return old revision for given function.
    """
    funcHashOld, funcSourceOld = self._GetOld(func)  # get old revision for given function
    dateStr = "Last revision: " + str(self.mainRevision[0])
    hashStr = "nOld function's hash: " + str(funcHashOld)
    codeStr = "nOld function's code:n" + "- " * 30 + "n" + str(funcSourceOld) + "n" + "- " * 30
    oldRevision = dateStr + hashStr + codeStr
    return oldRevision

ShowNew() # Вывод информации о новой ревизии для функции.

def ShowNew(self, func=None):
    """
    Function return old revision for given function.
    """
    funcHashNew, funcSourceNew = self._GetNew(func)  # get old revision for given function
    hashStr = "New function's hash: " + str(funcHashNew)
    codeStr = "nNew function's code:n" + "- " * 30 + "n" + str(funcSourceNew) + "n" + "- " * 30
    newRevision = hashStr + codeStr
    return newRevision

Diff() # Сравнение ревизий и вывод диффа для функции при необходимости.

def Diff(self, func=None):
    """
    Checks if given function modified then return tuple with revision's diff:
    (old_revision, new_revision), otherwise return None.
    """
    funcHashOld, funcSourceOld = self._GetOld(func)  # get old revision for given function
    funcHashNew, funcSourceNew = self._GetNew(func)  # get new revision for given function
    # check old and new revisions:
    if self._Similar(funcHashOld, funcSourceOld, funcHashNew, funcSourceNew):
        diff = None  # not difference
    else:
        diff = ("Last revision: " + str(self.mainRevision[0]) +
                "nOld function's hash: " + str(funcHashOld) +
                "nOld function's code:n" + "- " * 30 + "n" +
                str(funcSourceOld) + "n" + "- " * 30,
                "nNew function's hash: " + str(funcHashNew) +
                "nNew function's code:n" + "- " * 30 + "n" +
                str(funcSourceNew)  + "n" + "- " * 30)  # if new function not similar old function
    return diff

_testFunction() # Фейковая функция для проверки работы модуля

def _testFunction(a=None):
    """
    This is fake test function for module.
    """
    # this is comment
    if a:
        return True
    else:
        return False

if __name__ == '__main__':() # Примеры использования модуля, при его отдельном запуске.

func = _testFunction  # set function for review in revision
revision = Revision('revision.txt')  # init revision class for using with revision.txt
 
# how to use this module for review revision of function:
print(MSG_CHECK, func.__name__)
funcModified = revision.Diff(func)  # get function's diff as tuple (old_revision, new_revision)
if funcModified:
    print(MSG_MODIFIED)
    print(funcModified[0])  # old revision
    print(funcModified[1])  # new revision
else:
    print(MSG_NOT_MODIFIED)
 
# how to use this module for update revision:
action = input("Update function's revision? [y/n]: ")
if action == 'y':
    print(MSG_UPDATE, func.__name__)
    if revision.Update(func):
        print(MSG_UPDATED)
    else:
        print(MSG_UPDATE_ERROR)
 
# how to use this module for clean file-revision:
action = input("Clean file-revision now? [y/n]: ")
if action == 'y':
    print(MSG_DELETE)
    if revision.DeleteAll():
        print(MSG_DELETED)
    else:
        print(MSG_DELETE_ERROR)
 
# how to use this module for show old review:
action = input('Show old revision for function? [y/n]: ')
if action == 'y':
    print(revision.ShowOld(func))
 
# how to use this module for show new review:
action = input('Show new revision for function? [y/n]: ')
if action == 'y':
    print(revision.ShowNew(func))

Чтобы посмотреть примеры использования данного модуля, нужно лишь запустить его, используя Python 3.2.3:

python FileRevision.py

Контроль целостности кода функций

При первом запуске скрипт обнаружит отсутствие ревизии для фейковой функции, реализованной в примере, предложит обновить информацию о ней, очистить файл ревизий, а также вывести информацию о предыдущей и новой ревизиях. Затем рядом с .py-файлом будет создан файл revision.txt с ревизиями для примеров.

Таким образом, при использовании нашего модуля и наличии сотрудника, ответственного за формирование файла ревизий кода, увеличится степень защищенности тестовых функций.

На сегодня все. Ждем ваших вопросов и предложений в комментариях. Спасибо за внимание!

Автор: Тимур Гильмуллин, группа автоматизированного тестирования Positive Technologies.

Автор: ptsecurity

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js