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

от автора

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

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

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

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

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

Механизм

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

(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.

ссылка на оригинал статьи http://habrahabr.ru/company/pt/blog/177189/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *