QA Fest 2017. Не смешите мой coverage!

QA Fest 2017. Не смешите мой coverage!

Я расскажу о тестировании с точки зрения разработчика. Каждый разработчик рано или поздно приходит к выводу, что тесты необходимы. Следующий вопрос который он себе задает - а насколько хороши мои тесты. Я расскажу об инструментах и библиотеках, которые помогают оценить качество тестов в мире Python. Мы посмотрим как работает библиотека coverage.py, в каких ситуациях она бессильна и самое главное - почему. Узнаем что такое мутационное тестирование, как его можно применять в реальных проектах и как оно помогает оценить качество тестов. Увидим как работает библиотека Hypothesis и поймем в каких ситуациях она может оказаться нам полезной. В докладе будут внутренности Python-библиотек и объектов, много примеров и конечно же котики!

15563b4bb24076f1801cd862f74ed3fe?s=128

Ivan Tsyganov

September 27, 2017
Tweet

Transcript

  1. Не смешите мой coverage! Цыганов Иван Positive Technologies

  2. Обо мне ✤ Люблю OpenSource ✤ Не умею frontend ✤

    Добровольно пишу тесты
  3. ✤ Более 15 лет практического опыта на рынке ИБ ✤

    Более 700 сотрудников в 9 странах ✤ Каждый год находим более 200 уязвимостей нулевого дня ✤ Проводим более 200 аудитов безопасности в крупнейших компаниях мира ежегодно
  4. MaxPatrol ✤ Pentest. Тестирование на проникновение. ✤ Audit. Системные проверки.

    ✤ Compliance. Соответствие стандартам. ✤ Одна из крупнейших баз знаний в мире Система контроля защищенности и соответствия стандартам.
  5. MaxPatrol ✤ Pentest. Тестирование на проникновение. ✤ Compliance. Соответствие стандартам.

    ✤ Одна из крупнейших баз знаний в мире Система контроля защищенности и соответствия стандартам. ✤ Audit. Системные проверки.
  6. > 50 000 строк кода

  7. Зачем мы тестируем? ✤ Уверенность, что написанный код работает ✤

    Ревью кода становится проще ✤ Гарантия, что ничего не сломалось при изменениях
  8. Давайте писать тесты! def normalize_digits(digits_list): for digit in digits_list: if

    isinstance(digit, str): if digit.startswith('0x'): result = int(digit, base=16) else: result = int(digit) elif isinstance(digit, (int, float)): result = digit yield result
  9. Плохой тест assert list(normalize_digits(['1', '0x1'])) == [1, 1] def normalize_digits(digits_list):

    for digit in digits_list: if isinstance(digit, str): if digit.startswith('0x'): result = int(digit, base=16) else: result = int(digit) elif isinstance(digit, (int, float)): result = digit yield result
  10. Неожиданные данные >>> list(normalize_digits(['1', '2', '¯\_(ツ)_/¯']))

  11. Неожиданные данные >>> list(normalize_digits(['1', '2', '¯\_(ツ)_/¯'])) Traceback (most recent call

    last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in normalize_digits ValueError: invalid literal for int() with base 10: '¯\_(ツ)_/¯'
  12. Неожиданные данные >>> list(normalize_digits(['1', '2', '¯\_(ツ)_/¯'])) Traceback (most recent call

    last): File "<stdin>", line 1, in <module> File "<stdin>", line 7, in normalize_digits ValueError: invalid literal for int() with base 10: '¯\_(ツ)_/¯' def normalize_digits(digits_list): for digit in digits_list: if isinstance(digit, str): try: if digit.startswith('0x'): result = int(digit, base=16) else: result = int(digit) except ValueError: continue elif isinstance(digit, (int, float)): result = digit yield result
  13. Наличие тестов - не показатель качества

  14. ✤ Проверить покрытие кода тестами ✤ ... ✤ ... Как

    сделать тесты лучше?
  15. coverage.py ✤ Позволяет проверить покрытие кода тестами ✤ Есть плагин

    для pytest ✤
  16. coverage.py ✤ Позволяет проверить покрытие кода тестами ✤ Есть плагин

    для pytest ✤ В основном работает
  17. coverage.ini [report]
 show_missing = True
 precision = 2 py.test --cov-config=coverage.ini

    --cov=target test.py
  18. def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] Name Stmts

    Miss Cover Missing -------------------------------------------- src.py 12 4 66.67% 9-12 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result
  19. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] Name Stmts Miss Cover Missing -------------------------------------------- src.py 12 4 66.67% 9-12
  20. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result Name Stmts Miss Cover Missing -------------------------------------------- src.py 12 4 66.67% 9-12 def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0'])) == [1]
  21. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0'])) == [1] Name Stmts Miss Cover Missing -------------------------------------------- src.py 12 0 100.00%
  22. 1 def get_total_price(cart_prices): 2 if len(cart_prices) == 0: 3 return

    0 4 5 result = {'TotalPrice': sum(cart_prices)} 6 if len(cart_prices) >= 2: 7 result['Discount'] = result['TotalPrice'] * 0.25 8 9 return result['TotalPrice'] - result.get('Discount') def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 Name Stmts Miss Cover Missing -------------------------------------------- src.py 12 0 100.00%
  23. >>> sum(normalize_digits([1, 2, None])) == 3 False 1 def normalize_digits(digits_list):

    2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result
  24. >>> sum(normalize_digits([1, 2, None])) == 3 False >>> sum(normalize_digits([1, 2,

    None])) 5 >>> list(normalize_digits([1, 2, None])) [1, 2, 2] 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result
  25. None
  26. coverage.ini [report]
 show_missing = True
 precision = 2
 [run]
 branch

    = True py.test --cov-config=coverage.ini --cov=target test.py
  27. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0'])) == [1] Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- src.py 12 0 8 1 95.00% 11 ->13
  28. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 yield result def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0'])) == [1] Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- src.py 12 0 8 1 95.00% 11 ->13
  29. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0'])) == [1] assert list(normalize_digits([1, None])) == [1] Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- src.py 12 0 8 1 95.00% 11 ->13
  30. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0'])) == [1] assert list(normalize_digits([1, None])) == [1] Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- src.py 12 0 8 1 100.00%
  31. None
  32. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 base = 16 if digit.startswith('0x') else 10 5 try: 6 result = int(digit, base=base) 7 except ValueError: 8 continue 9 elif isinstance(digit, (int, float)): 10 result = digit 11 else: continue 12 yield result 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result
  33. def test(): assert list(normalize_digits(['1', 1, None, 'o_0'])) == [1, 1]

    Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- src.py 12 0 8 1 100.00% 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 base = 16 if digit.startswith('0x') else 10 5 try: 6 result = int(digit, base=base) 7 except ValueError: 8 continue 9 elif isinstance(digit, (int, float)): 10 result = digit 11 else: continue 12 yield result
  34. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 base = 16 if digit.startswith('0x') else 10 5 try: 6 result = int(digit, base=base) 7 except ValueError: 8 continue 9 elif isinstance(digit, (int, float)): 10 result = digit 11 else: continue 12 yield result def test(): assert list(normalize_digits(['1', 1, None, 'o_0'])) == [1, 1] Name Stmts Miss Branch BrPart Cover Missing -------------------------------------------------------- src.py 12 0 8 1 100.00%
  35. None
  36. Как считать coverage? Все строки Реально выполненные строки - Непокрытые

    строки =
  37. Все строки Source coverage.parser.PythonParser Statements

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

    Компилирует код. Обходит code-object и сохраняет номера строк
  39. Обход токенов ✤ Запоминает определения классов ✤ «Сворачивает» многострочные выражения

    ✤ Исключает комментарии
  40. Обход байткода ✤ Рекурсивно обходит все code object в code.co_const

    ✤ Полностью повторяет метод dis.findlinestarts ✤ Анализирует code_obj.co_lnotab ✤ Генерирует пару (номер байткода, номер строки)
  41. Как считать coverage --branch? Все переходы Реально выполненные переходы -

    Непокрытые переходы =
  42. Все переходы Source coverage.parser.AstArcAnalyzer (from_line, to_line) coverage.parser.PythonParser

  43. coverage.parser.AstArcAnalyzer ✤ Обходит все AST-дерево с корневой ноды ✤ В

    зависимости от типа ноды генерирует варианты перехода между строками
  44. Обработка ноды class While(stmt): _fields = ( 'test', 'body', 'orelse',

    ) while i<10: print(i) i += 1
  45. Обработка ноды class While(stmt): _fields = ( 'test', 'body', 'orelse',

    ) while i<10: print(i) i += 1 else: print('Done!')
  46. Выполненные строки 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.
  47. PyTracer «call» event ✤ Сохраняем данные предыдущего контекста ✤ Начинаем

    собирать данные нового контекста ✤ Учитываем особенности генераторов
  48. PyTracer «line» event ✤ Запоминаем выполняемую строку ✤ Запоминаем переход

    между строками
  49. PyTracer «return» event ✤ Отмечаем выход из контекста ✤ Помним

    о том, что yield это тоже выход из контекста
  50. Отчет ✤ Что выполнялось ✤ Что должно было выполниться

  51. Отчет ✤ Что выполнялось ✤ Что должно было выполниться ✤

    Ругаемся
  52. Зачем такие сложности? for i in some_list: if i ==

    'Hello': print(i + ' World!') elif i == 'Skip': continue else: break else: print(r'¯\_(ツ)_/¯')
  53. Серебряная пуля?

  54. Не совсем…

  55. Что может пойти не так? def make_dict(a, b, c): return

    { 'a': a, 'b': b if a > 1 else 0, 'c': [ i for i in range(c) if i < (a * 10) ] }
  56. None
  57. ✤ Проверить покрытие кода тестами ✤ ... ✤ ... Как

    сделать тесты лучше?
  58. ✤ Проверить покрытие кода тестами ✤ Попробовать мутационное тестирование ✤

    ... Как сделать тесты лучше?
  59. Мутационное тестирование ✤ Берем тестируемый код ✤ Мутируем ✤ Тестируем

    мутантов нашими тестами ✤ Тест не упал -> плохой тест
  60. Мутационное тестирование ✤ Берем тестируемый код ✤ Мутируем ✤ Тестируем

    мутантов нашими тестами ✤ Если тест не упал -> это плохой тест ✤ Тест не упал -> плохой тест
  61. Идея def mul(a, b): return a * b def test_mul():

    assert mul(2, 2) == 4
  62. Идея def mul(a, b): return a * b def test_mul():

    assert mul(2, 2) == 4 def mul(a, b): return a ** b
  63. Идея def mul(a, b): return a * b def test_mul():

    assert mul(2, 2) == 4 def mul(a, b): return a ** b def mul(a, b): return a + b
  64. Идея def mul(a, b): return a * b def mul(a,

    b): return a ** b def mul(a, b): return a + b def test_mul(): assert mul(2, 2) == 4 assert mul(2, 3) == 6
  65. mutpy ✤ Анализирует исходный код ✤ Модифицирует некоторые AST-ноды ✤

    Запускает тесты ✤ Проверяет результат запуска тестов
  66. Реализация Source NodeTransformer compile run test

  67. Мутации 2 for digit in digits_list: 3 if (not isinstance(digit,

    str)): 4 try: 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result
  68. Мутации 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3

    if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result 4 try: 5 if digit.startswith('mutpy'): 6 result = int(digit, base=16)
  69. Мутации 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3

    if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result 8 result = int(digit) 9 except ValueError: 10 pass
  70. [*] Mutation score [1.28949 s]: 90.0% - all: 10 -

    killed: 7 (90.0%) - survived: 1 (10.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%) 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result def test(): assert list(normalize_digits(['100', '0xA'])) == [100, 10] assert list(normalize_digits([1, 'o_0', None])) == [1]
  71. [*] Mutation score [1.28949 s]: 70.0% - all: 10 -

    killed: 7 (70.0%) - survived: 3 (30.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%) 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 digit = int(digit, base=16) 7 else: 8 digit = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 digit = digit 13 else: continue 14 yield digit def test(): assert list(normalize_digits(['1', '0x1'])) == [1, 1] assert list(normalize_digits([1, 'o_0', None])) == [1] - survived: 1 (10.0%)
  72. … --------------------------------------------------------------------- 6: result = int(digit, base=16) 7: else: 8:

    result = int(digit) 9: except ValueError: ~10: break 11: elif isinstance(digit, (int, float)): 12: result = digit 13: else: 14: continue 15: yield result ---------------------------------------------------------------------- [0.03628 s] survived … [*] Mutation score [1.28949 s]: 90.0% - all: 10 - killed: 7 (90.0%) - survived: 1 (10.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%)
  73. [*] Mutation score [1.07053 s]: 100.0% - all: 10 -

    killed: 10 (100.0%) - survived: 0 (0.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%) 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result def test(): assert list(normalize_digits(['100', '0xA'])) == [100, 10] assert list(normalize_digits([None, 'o_0', 1])) == [1]
  74. Идея имеет право на жизнь и работает! Но требует много

    ресурсов.
  75. Name Stmts Miss Cover Missing ---------------------------------------------------- src.py 13 0 100.00%

    def test(): assert list(normalize_digits(['100', '0xA'])) == [100, 10] assert list(normalize_digits([None, 'o_0', 1])) == [1] 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result
  76. Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------ src.py 13

    0 8 0 100.00% def test(): assert list(normalize_digits(['100', '0xA'])) == [100, 10] assert list(normalize_digits([None, 'o_0', 1])) == [1] 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result
  77. 1 def get_total_price(cart_prices): 2 result = {'TotalPrice': sum(cart_prices)} 3 if

    len(cart_prices) >= 2: 4 result['Discount'] = result['TotalPrice'] * 0.25 5 6 return result['TotalPrice'] - result.get(‘Discount’, 0) def test_get_total_price(): assert get_total_price([90, 10]) == 75 assert get_total_price( []) == 0 assert get_total_price([90]) == 90 Name Stmts Miss Branch BrPart Cover Missing ---------------------------------------------------------- target.py 5 0 2 0 100.00%
  78. ✤ Проверить покрытие кода тестами ✤ Попробовать мутационное тестирование ✤

    ... Как сделать тесты лучше?
  79. ✤ Проверить покрытие кода тестами ✤ Попробовать мутационное тестирование ✤

    Генерировать входные данные Как сделать тесты лучше?
  80. Hypothesis ✤ Возможность генерации данных ✤ Позволяет проверить поведение кода

  81. Hypothesis ✤ Возможность генерации данных ✤ Позволяет проверить поведение кода

    ✤ Подходит не для всех задач :(
  82. Hypothesis. Генерация данных >>> from hypothesis import strategies as st

    >>> st.integers().example() 132598732931307445807900680032693714775
  83. Hypothesis. Генерация данных >>> from hypothesis import strategies as st

    >>> st.integers().example() 132598732931307445807900680032693714775 >>> st.integers(min_value=0, max_value=100).example() 26
  84. Hypothesis. Генерация данных >>> from hypothesis import strategies as st

    >>> st.integers().example() 132598732931307445807900680032693714775 >>> st.integers(min_value=0, max_value=100).example() 26 >>> st.text().example() '\U0007223dﶝ\U000d4ab2\U000a477a\U000c54e1# ' >>> st.text().example() '傖'
  85. 
 keys = (
 st.integers()
 |
 st.floats()
 |
 st.text(alphabet=string.ascii_letters)
 )

  86. 
 keys = (
 st.integers()
 |
 st.floats()
 |
 st.text(alphabet=string.ascii_letters)
 )

    
 simple_values = (
 keys
 |
 st.booleans()
 |
 st.none()
 |
 st.binary()
 |
 st.text()
 )
  87. 
 keys = st.integers()|st.floats()|st.text(alphabet= ... simple_values = keys|st.booleans()|st.none()| ... x

    = st.dictionaries(keys, simple_values) x.example()
 {
 '': b'\x9cR',
 'bHNdRVEdJBM': '\U000b2611\U00106014\x13',
 4.121937670036e+86: '.\x16\U0010e8c5\U000b3ae6-',
 -128805459898745084717413287023876194805: None
 }
  88. 
 keys = st.integers()|st.floats()|st.text(alphabet= ... simple_values = keys|st.booleans()|st.none()| ...
 


    nested_values = (
 simple_values
 |
 st.recursive(
 simple_values,
 lambda item: ( st.dictionaries(keys, item) | st.lists(item) )
 )
 )
  89. keys = st.integers()|st.floats()|st.text(alphabet= ... simple_values = keys|st.booleans()|st.none()|st.bin ...
 nested_values =

    simple_values|st.recursive(simple_ ... x = st.dictionaries(keys, nested_values)
 {
 'UqcOU': {
 -2.00001: 'x', 'gZlJOoWxQ': [
 -2.264270, False
 ]
 }, 
 'WwVKZScYmVm': '', 
 'sBt': -4508536492226585216256
 }
  90. ?

  91. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result
  92. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result values = ( st.text() | st.none() | st.integers() | st.floats() ) @given(st.lists(values)) def test_normalize_digits(digits_list): result = normalize_digits(digits_list) for item in result: assert isinstance(item, (int, float))
  93. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result values = ( st.text() | st.none() | st.integers() | st.floats() ) @given(st.lists(values)) def test_normalize_digits(digits_list): result = normalize_digits(digits_list) for item in result: assert isinstance(item, (int, float)) values = ( st.integers() | st.floats(allow_infinity=False, allow_nan=False) ) @given(st.lists(values)) def test_normalize_digits(digits_list): strings_list = list(map(str, digits_list)) result = list(normalize_digits(strings_list)) assert result == digits_list
  94. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result values = ( st.text() | st.none() | st.integers() | st.floats() ) @given(st.lists(values)) def test_normalize_digits(digits_list): result = normalize_digits(digits_list) for item in result: assert isinstance(item, (int, float)) values = ( st.integers() | st.floats(allow_infinity=False, allow_nan=False) ) @given(st.lists(values)) def test_normalize_digits(digits_list): strings_list = list(map(str, digits_list)) result = list(normalize_digits(strings_list)) assert result == digits_list digits_list = [0.0] @given(st.lists(values)) def test_normalize_digits(digits_list): strings_list = list(map(str, digits_list)) result = list(normalize_digits(strings_list)) > assert result == digits_list E assert [] == [0.0] E Right contains more items, first extra item: 0.0 E Use -v to get the full diff test.py: AssertionError Falsifying example: test_normalize_digits(digits_list=[0.0])
  95. 1 def normalize_digits(digits_list): 2 for digit in digits_list: 3 if

    isinstance(digit, str): 4 try: 5 if digit.startswith('0x'): 6 result = int(digit, base=16) 7 else: 8 result = int(digit) 9 except ValueError: 10 continue 11 elif isinstance(digit, (int, float)): 12 result = digit 13 else: continue 14 yield result values = ( st.text() | st.none() | st.integers() | st.floats() ) @given(st.lists(values)) def test_normalize_digits(digits_list): result = normalize_digits(digits_list) for item in result: assert isinstance(item, (int, float)) values = ( st.integers() | st.floats(allow_infinity=False, allow_nan=False) ) @given(st.lists(values)) def test_normalize_digits(digits_list): strings_list = list(map(str, digits_list)) result = list(normalize_digits(strings_list)) assert result == digits_list digits_list = [0.0] @given(st.lists(values)) def test_normalize_digits(digits_list): strings_list = list(map(str, digits_list)) result = list(normalize_digits(strings_list)) > assert result == digits_list E assert [] == [0.0] E Right contains more items, first extra item: 0.0 E Use -v to get the full diff test.py: AssertionError Falsifying example: test_normalize_digits(digits_list=[0.0]) >>> int('0.0') Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: invalid literal for int() with base 10: '0.0'
  96. Каким же должен был быть код?!

  97. def normalize_digits(digits_list): for digit in digits_list: if isinstance(digit, str): if

    digit.startswith('0x'): method = partial(int, base=16) elif '.' in digit or 'e' in digit: method = float else: method = int elif isinstance(digit, (int, float)): method = lambda x: x else: continue try: result = method(digit) except ValueError: continue yield result
  98. def normalize_digits(digits_list): for digit in digits_list: if isinstance(digit, str): if

    digit.startswith('0x'): method = partial(int, base=16) elif '.' in digit or 'e' in digit: method = float else: method = int elif isinstance(digit, (int, float)): method = lambda x: x else: continue try: result = method(digit) except ValueError: continue yield result def test_normalize(): assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10] assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05]
  99. def normalize_digits(digits_list): for digit in digits_list: if isinstance(digit, str): if

    digit.startswith('0x'): method = partial(int, base=16) elif '.' in digit or 'e' in digit: method = float else: method = int elif isinstance(digit, (int, float)): method = lambda x: x else: continue try: result = method(digit) except ValueError: continue yield result def test_normalize(): assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10] assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05] Name Stmts Miss Cover Missing ---------------------------------------------------- src.py 17 0 100.00%
  100. def normalize_digits(digits_list): for digit in digits_list: if isinstance(digit, str): if

    digit.startswith('0x'): method = partial(int, base=16) elif '.' in digit or 'e' in digit: method = float else: method = int elif isinstance(digit, (int, float)): method = lambda x: x else: continue try: result = method(digit) except ValueError: continue yield result def test_normalize(): assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10] assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05] Name Stmts Miss Cover Missing ---------------------------------------------------- src.py 17 0 100.00% Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------ src.py 17 0 12 0 100.00%
  101. def normalize_digits(digits_list): for digit in digits_list: if isinstance(digit, str): if

    digit.startswith('0x'): method = partial(int, base=16) elif '.' in digit or 'e' in digit: method = float else: method = int elif isinstance(digit, (int, float)): method = lambda x: x else: continue try: result = method(digit) except ValueError: continue yield result def test_normalize(): assert list(normalize_digits(['100', '1.1', '0xA'])) == [100, 1.1, 10] assert list(normalize_digits([None, 'o_0', 1, '1e-05'])) == [1, 1e-05] Name Stmts Miss Cover Missing ---------------------------------------------------- src.py 17 0 100.00% Name Stmts Miss Branch BrPart Cover Missing ------------------------------------------------------------------ src.py 17 0 12 0 100.00% [*] Mutation score [0.88045 s]: 100.0% - all: 18 - killed: 18 (100.0%) - survived: 0 (0.0%) - incompetent: 0 (0.0%) - timeout: 0 (0.0%)
  102. None
  103. >>> sum(list(normalize_digits([False, True, 2, 3, 4]))) 10

  104. >>> sum(list(normalize_digits([False, True, 2, 3, 4]))) 10 >>> isinstance(False, int)

    True >>> isinstance(True, int) True
  105. Библиотеки несовершенны

  106. 100% coverage расслабляет команду Библиотеки несовершенны

  107. 100% coverage расслабляет команду Библиотеки несовершенны Наличие тестов - не

    показатель качества кода
  108. Спасибо за внимание! mi.0-0.im