moscow python conf 2016. Почему 100% покрытие это плохо?

Post on 22-Jan-2018

435 Views

Category:

Engineering

4 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Цыганов Иван Positive Technologies

Почему 100% покрытие это плохо

Обо мне

✤ Спикер PyCon Russia 2016, PiterPy, PyCon Siberia 2016

✤ Люблю OpenSource

✤ Не умею frontend

✤ 15 лет практического опыта на рынке ИБ

✤ Более 650 сотрудников в 9 странах

✤ Каждый год находим более 200 уязвимостей нулевого дня

✤ Проводим более 200 аудитов безопасности в крупнейших компаниях мира ежегодно

MaxPatrol

✤ Pentest. Тестирование на проникновение.

✤ Audit. Системные проверки.

✤ Compliance. Соответствие стандартам.

✤ Одна из крупнейших баз знаний в мире

Система контроля защищенности и соответствия стандартам.

✤ Тестирование на проникновение (Pentest)

✤ Системные проверки (Audit)

✤ Соответствие стандартам (Compliance)

✤ Одна из крупнейших баз знаний в мире

Система контроля защищенности и соответствия стандартам.

✤Системные проверки (Audit)

MaxPatrol

> 50 000 строк кода

Зачем мы тестируем?

✤ Уверенность, что написанный код работает

✤ Ревью кода становится проще

✤ Гарантия, что ничего не сломалось при изменениях

Зачем проверять покрытие?

✤ Видно какой именно код протестирован

✤ Позволяет увидеть все ветви исполнения

Зачем проверять покрытие?

✤ Видно какой именно код протестирован

✤ Позволяет увидеть все ветви исполнения

✤ Метрика качества тестов (?)

Зачем нам 100%?

✤ Ачивка «У нас в проекте 100% coverage»

Зачем нам 100%?

✤ Ачивка «У нас в проекте 100% coverage»

✤ Уверенность, что код протестирован полностью

100% coverage != 100% протестировано

coverage.py

✤ Позволяет проверить покрытие кода тестами

✤ Есть плагин для pytest

✤ В основном работает

coverage.py

def get_longest(a, b): if len(a) > len(b): return a return b

assert get_longest([1,2,3], [4,5]) == [1,2,3] assert get_longest([1,2], [3,4,5]) == [3,4,5] 

coverage.py

def apply_discount(prices): result = {'Total': sum(prices)} if result['Total'] >= 1000: result['Discount'] = result['Total'] * 0.25 return result['Total'] - result['Discount']   assert apply_discount([400, 600]) == 750

Name Stmts Miss Cover Missing ---------------------------------------------------------- samples/apply_discount.py 5 0 100%

coverage.py

def apply_discount(prices): result = {'Total': sum(prices)} if result['Total'] >= 1000: result['Discount'] = result['Total'] * 0.25 return result['Total'] - result['Discount']   assert apply_discount([400, 600]) == 750

>>> apply_discount([200])

coverage.py

def apply_discount(prices): result = {'Total': sum(prices)} if result['Total'] >= 1000: result['Discount'] = result['Total'] * 0.25 return result['Total'] - result['Discount']   assert apply_discount([400, 600]) == 750

>>> apply_discount([200]) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in apply_discount KeyError: 'Discount'

coverage.py --branch

1 def apply_discount(prices): 2 result = {'Total': sum(prices)} 3 if result['Total'] >= 1000: 4 result['Discount'] = result['Total'] * 0.25 5 return result['Total'] - result['Discount'] 6   7 assert apply_discount([400, 600]) == 750

Name Stmts Miss Branch BrPart Cover Missing --------------------------------------------------------------------- samples/apply_discount.py 5 0 2 1 85.71% 3 ->5

coverage.py --branch

1 def apply_discount(prices): 2 result = {'Total': sum(prices)} 3 if result['Total'] >= 1000: 4 result['Discount'] = result['Total'] * 0.25 5 return result['Total'] - result['Discount'] 6   7 assert apply_discount([400, 600]) == 750

Name Stmts Miss Branch BrPart Cover Missing --------------------------------------------------------------------- samples/apply_discount.py 5 0 2 1 85.71% 3 ->5

Как считать покрытие?

Все строкиРеально выполненные

строки- Непокрытые строки=

Все строки

Source

coverage.parser.PythonParser

Statements

coverage.parser.PythonParser

✤ Обходит все токены и отмечает «интересные» факты

✤ Компилирует код. Обходит code-object и сохраняет номера строк

Обход токенов

✤ Запоминает определения классов

✤ «Сворачивает» многострочные выражения

✤ Исключает комментарии

Обход байткода

✤ Полностью повторяет метод dis.findlinestarts

✤ Анализирует code_obj.co_lnotab

✤ Генерирует пару (номер байткода, номер строки)

Как считать coverage --branch?

Все переходыРеально выполненные

переходы- Непокрытые переходы=

Все переходы

Source

coverage.parser.AstArcAnalyzer

(from_line, to_line)

coverage.parser.PythonParser

coverage.parser.AstArcAnalyzer

✤ Обходит AST с корневой ноды

✤ Обрабатывает отдельно каждый тип нод отдельно

Обработка ноды

class While(stmt): _fields = ( 'test', 'body', 'orelse', )

while i<10: print(i) i += 1

Обработка ноды

class While(stmt): _fields = ( 'test', 'body', 'orelse', )

while i<10: print(i) i += 1 else: print('All done')

Выполненные строки

sys.settrace(tracefunc)Set the system’s trace function, which allows you to implement a Python source code debugger in Python.

Trace functions should have three arguments: frame, event, and arg. frame is the current stack frame. event is a string: 'call', 'line', 'return', 'exception', 'c_call', 'c_return', or 'c_exception'. arg depends on the event type.

PyTracer «call» event

✤ Сохраняем данные предыдущего контекста

✤ Начинаем собирать данные нового контекста

✤ Учитываем особенности генераторов

PyTracer «line» event

✤ Запоминаем выполняемую строку

✤ Запоминаем переход между строками

PyTracer «return» event

✤ Отмечаем выход из контекста

✤ Помним о том, что yield это тоже return

Отчет

✤ Что выполнялось

✤ Что должно было выполниться

✤ Ругаемся

Зачем такие сложности?

1 for i in some_list: 2 if i == 'Hello': 3 print(i + ' World!') 4 elif i == 'Skip': 5 continue 6 else: 7 break 8 else: 9 print(r'¯\_(ツ)_/¯')

Серебряная пуля?

Не совсем…

Что может пойти не так?

def make_user(name, email): return dict( ID=get_id(), Name=name, Role='Editor' if check_employee(email) else 'Guest' )  

Что может пойти не так?

def positive_squares(items): return map(lambda x: x **2 if x>0 else x, items)

Что может пойти не так?

def positive_squares(items): return [ item **2 for item in items if item>0 ]

Что может пойти не так?

def positive_squares(items): return [ item **2 for item in items if item>0 ]

def positive_squares(items): return map(lambda x: x **2 if x>0 else x, items)

def make_user(name, email): return dict( ID=get_id(), Name=name, Role='Editor' if check_employee(email) else 'Guest' )  

Что может пойти не так?

def positive_squares(items): return [ item **2 for item in items if item>0 ]

def positive_squares(items): return map(lambda x: x **2 if x>0 else x, items)

def make_user(name, email): return dict( ID=get_id(), Name=name, Role='Editor' if check_employee(email) else 'Guest' )  

Непокрываемый код

def some_method(a, b, c): if a and b or c: return True return False

sys.settrace(tracefunc)

✤ Устанавливаем свою функцию трассировки

✤ Смотрим что происходит и делаем выводы

sys.settrace(tracefunc)

Ограниченное количество событий:✤ call ✤ line ✤ return ✤ exception

sys.settrace(tracefunc)

Ограниченное количество событий:✤ call ✤ line ✤ return ✤ exception

ast.NodeTransformer

✤ Обходим ноды

✤ Оборачиваем в «нечто» каждую ноду

✤ Запускаем и отслеживаем что выполнялось

ast.NodeTransformer

✤ Сложно обернуть код, не изменив логику

✤ Не все ноды можно обернуть

ast.NodeTransformer

✤ Сложно обернуть код, не изменив логику

✤ Не все ноды можно обернуть

Идея

✤ Перехватить контроль во время импорта

✤ Обойти байткод модуля

✤ Добавить вызов функции

✤ Собрать code-object

Идея

✤ Перехватить контроль во время импорта

✤ Обойти байткод модуля

✤ Добавить вызов функции

✤ Собрать code-object

OpTracehttps: //github.com/tsyganov-ivan/OpTrace

План

✤ Устанавливаем import hook

✤ Модифицируем и подменяем code-object

✤ Запускаем тесты

✤ Анализируем результаты

Import hook. Finder.

✤ Пропускаем неинтересные модули

✤ Создаем свой Loader для нужных модулей

Import hook. Loader.

✤ Получаем байт-код модуля

✤ Получаем исходный код модуля

✤ Модифицируем байт-код

✤ Возвращаем измененный байт-код

План

✤ Устанавливаем import hook

✤ Модифицируем и подменяем code-object

✤ Запускаем тесты

✤ Анализируем результаты

Wrapper. Модифицируем байт-код.

# ... wrapper = Wrapper( trace_func=self.make_visitor(module_name), mark_func=self.make_marker(module_name, source) ) new_code = wrapper.wrap_code(code) return new_code # ...

Wrapper. Callbacks.

def make_marker(self, module, source): self.module_opcodes[module] = FileOpcode(module, source) def mark(codeobj_id, opcode): self.module_opcodes[module].add(codeobj_id, opcode.offset, opcode) return mark   def make_visitor(self, module): def visit(codeobj_id, opcode): self.module_opcodes[module].visit(codeobj_id, opcode.offset, opcode) return visit

dis.dis(some_method)

def some_method(a, b, c): if a and b or c: return True return False

2 0 LOAD_FAST 0 (a) 3 POP_JUMP_IF_FALSE 18 6 LOAD_FAST 1 (b) 9 POP_JUMP_IF_TRUE 18 12 LOAD_FAST 2 (c) 15 POP_JUMP_IF_FALSE 22

3 >> 18 LOAD_CONST 1 (True) 21 RETURN_VALUE

4 >> 22 LOAD_CONST 2 (False) 25 RETURN_VALUE

dis.get_instructions(some_method)

def some_method(a, b, c): if a and b or c: return True return False

Instruction(opname='LOAD_FAST', opcode=124, arg=0, ... Instruction(opname='POP_JUMP_IF_FALSE', opcode=114, ...Instruction(opname='LOAD_FAST', opcode=124, arg=1, ... Instruction(opname='POP_JUMP_IF_TRUE', opcode=115, ... Instruction(opname='LOAD_FAST', opcode=124, arg=2, ... Instruction(opname='POP_JUMP_IF_FALSE', opcode=114, ... Instruction(opname='LOAD_CONST', opcode=100, arg=1, ... Instruction(opname='RETURN_VALUE', opcode=83, arg=None ... Instruction(opname='LOAD_CONST', opcode=100, arg=2, ... Instruction(opname='RETURN_VALUE', opcode=83, arg=None ...

Wrap code. Все опкоды.

✤ Просто вызываем функцию, переданную из Loader’a

self.mark(codeobj_id, st)

Wrap code. Трассировка.

✤ Добавляем lambda-функцию в константы

constants.append( lambda co_id=codeobj_id, opcode=st: self.visit(co_id, opcode) )

Wrap code. Трассировка.

✤ Добавляем lambda-функцию в константы

constants.append( lambda co_id=codeobj_id, opcode=st: self.visit(co_id, opcode) )

PyCodeObject* PyCode_New( /* ... */ PyObject *code, PyObject *consts, PyObject *names, /* ... */)

Wrap code. Трассировка.

✤ Добавляем lambda-функцию в константы

✤ Добавляем байт-код для вызова

def make_trace(self, constant_index): yield opcode.opmap['LOAD_CONST'] yield from self.make_args(constant_index) yield opcode.opmap['CALL_FUNCTION'] yield from self.make_args(0) yield opcode.opmap['POP_TOP']

Wrap code. Трассировка.

✤ Добавляем lambda-функцию в константы

✤ Добавляем байт-код для вызова

✤ Не забываем про оригинальный опкод и его параметры!

Wrap code. Трассировка.

✤ Добавляем lambda-функцию в константы

✤ Добавляем байт-код для вызова

✤ Не забываем про оригинальный опкод и его параметры!

✤ Учитываем смещение в последующих опкодах

Wrap сode. Результат.

def some_method(a, b, c): if a and b or c: return True return False

2 0 LOAD_FAST 0 (a) 3 POP_JUMP_IF_FALSE 18 6 LOAD_FAST 1 (b) 9 POP_JUMP_IF_TRUE 18 12 LOAD_FAST 2 (c) 15 POP_JUMP_IF_FALSE 22

3 >> 18 LOAD_CONST 1 (True) 21 RETURN_VALUE

4 >> 22 LOAD_CONST 2 (False) 25 RETURN_VALUE

Wrap сode. Результат.

6 LOAD_FAST 1 (b) 9 POP_JUMP_IF_TRUE 18 . . . 3 >> 18 LOAD_CONST 1 (True) 21 RETURN_VALUE

def some_method(a, b, c): if a and b or c: return True return False

Wrap сode. Результат.

20 LOAD_CONST 5 (<function ...<locals>.<lambda>) 23 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 26 POP_TOP 27 LOAD_FAST 1 (b) 30 LOAD_CONST 6 (<function ...<locals>.<lambda>) 33 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 36 POP_TOP 37 POP_JUMP_IF_TRUE 60 . . . >> 60 LOAD_CONST 9 (<function ...<locals>.<lambda>) 63 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 66 POP_TOP 67 LOAD_CONST 1 (True) 70 LOAD_CONST 10 (<function ...<locals>.<lambda>) 73 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 76 POP_TOP 77 RETURN_VALUE

План

✤ Устанавливаем import hook

✤ Модифицируем и подменяем code-object

✤ Запускаем тесты

✤ Анализируем результаты

Тестируем. Все опкоды.

def some_method(a, b, c): if a and b or c: return True return False   some_method(1, 1, 0)

Instruction(opname='LOAD_FAST', opcode=124, arg=0, ... Instruction(opname='POP_JUMP_IF_FALSE', opcode=114, ...Instruction(opname='LOAD_FAST', opcode=124, arg=1, ... Instruction(opname='POP_JUMP_IF_TRUE', opcode=115, ... Instruction(opname='LOAD_FAST', opcode=124, arg=2, ... Instruction(opname='POP_JUMP_IF_FALSE', opcode=114, ... Instruction(opname='LOAD_CONST', opcode=100, arg=1, ... Instruction(opname='RETURN_VALUE', opcode=83, arg=None ... Instruction(opname='LOAD_CONST', opcode=100, arg=2, ... Instruction(opname='RETURN_VALUE', opcode=83, arg=None ...

Тестируем. Непокрытые опкоды.

def some_method(a, b, c): if a and b or c: return True return False   some_method(1, 1, 0)

Instruction(opname='LOAD_FAST', arg=2, argval='c', argrepr='c', offset=12)Instruction(opname='POP_JUMP_IF_FALSE', arg=22, argval=22, argrepr='')Instruction(opname='LOAD_CONST', arg=2, argval=False, starts_line=3)Instruction(opname='RETURN_VALUE', arg=None, argval=None)

Тестируем. Непокрытые опкоды.

def some_method(a, b, c): if a and b or c: return True return False   some_method(1, 1, 0)

Instruction(opname='LOAD_FAST', arg=2, argval='c', argrepr='c', offset=12)Instruction(opname='POP_JUMP_IF_FALSE', arg=22, argval=22, argrepr='')Instruction(opname='LOAD_CONST', arg=2, argval=False, starts_line=3)Instruction(opname='RETURN_VALUE', arg=None, argval=None)

План

✤ Устанавливаем import hook

✤ Модифицируем и подменяем code-object

✤ Запускаем тесты

✤ Анализируем результаты

Способа однозначно перевести любой опкод к строке кода не существует

Способа однозначно перевести любой опкод к строке кода не существует

Отчет. Ищем строки.

✤ При обходе сохраняем текущую строку

✤ При выводе опкода выводим текущую строку

Отчет. Ищем строки.

if a and b or c: Instruction(opname='LOAD_FAST', arg=2, argval='c', argrepr='c', offset=12)

if a and b or c: Instruction(opname='POP_JUMP_IF_FALSE', arg=22, argval=22, argrepr='')

return False Instruction(opname='LOAD_CONST', arg=2, argval=False, starts_line=3)

return FalseInstruction(opname='RETURN_VALUE', arg=None, argval=None)

✤ При обходе сохраняем текущую строку

✤ При выводе опкода выводим текущую строку

✤ При обходе сохраняем текущую строку

✤ При выводе опкода выводим текущую строку

Отчет. Ищем строки.

if a and b or c: Instruction(opname='LOAD_FAST', arg=2, argval='c', argrepr='c', offset=12)

if a and b or c: Instruction(opname='POP_JUMP_IF_FALSE', arg=22, argval=22, argrepr='')

return False Instruction(opname='LOAD_CONST', arg=2, argval=False, starts_line=3)

return FalseInstruction(opname='RETURN_VALUE', arg=None, argval=None)

Отчет. Позиция в строке.

✤ Строка уже известна

✤ Вычислим позицию в строке для каждого типа опкода

Отчет. Позиция в строке.

if a and b or c:

Instruction( opname='LOAD_FAST', opcode=124, offset=12, starts_line=None, is_jump_target=True, arg=2, argval='c', argrepr=‘c' )

Отчет. Позиция в строке.

if a and b or c:

Instruction( opname='LOAD_FAST', opcode=124, offset=12, starts_line=None, is_jump_target=True, arg=2, argval='c', argrepr=‘c' )

Instruction( opname='POP_JUMP_IF_FALSE', opcode=114, offset=15, starts_line=None, is_jump_target=False, arg=22, argval=22, argrepr='' )

Отчет. Позиция в строке.

✤ Покрыв 70 типов опкодов удалось получить отчет

✤ Многие опкоды невозможно покрыть

Отчет. Позиция в строке.

✤ Покрыв 70 типов опкодов удалось получить отчет

✤ Многие опкоды невозможно покрыть

----------- Report tests.test_code -------------- 1: if a and b or c: ^ LOAD_FAST 1: if a and b or c: ^^^^^^^^^^^^^^^^ POP_JUMP_IF_FALSE 3: return False ^^^^^ LOAD_CONST 3: return False ^^^^^^^^^^^^ RETURN_VALUE

Отчет. Позиция в строке.

----------- Report tests.test_code -------------- 1: if a and b or c: ^ LOAD_FAST 1: if a and b or c: ^^^^^^^^^^^^^^^^ POP_JUMP_IF_FALSE 3: return False ^^^^^ LOAD_CONST 3: return False ^^^^^^^^^^^^ RETURN_VALUE

✤ Покрыв 70 типов опкодов удалось получить отчет

✤ Многие опкоды невозможно покрыть

OpTrace. Что не так?

✤ Переменные в отчете не всегда отмечаются правильно

✤ Часть опкодов приходится пропускать

✤ Производительность неизвестна

OpTrace. Что так?

✤ Трассировка работает хорошо

✤ Идея имеет право на жизнь

OpTrace. Планы.

✤ Услышать мнение и критику сообщества

OpTrace. Планы.

✤ Услышать мнение и критику сообщества

✤ Рефакторинг

✤ Тестирование

✤ Работа над улучшением отчета

✤ Плагин для pytest

К чему это все?

Библиотеки несовершенны

100% coverage расслабляет команду

Библиотеки несовершенны

100% coverage расслабляет команду

Библиотеки несовершенны

100% coverage - просто ачивка

Спасибо за внимание! Вопросы?mi.0-0.im

tsyganov-ivan.com

top related