Якість і надійність коду

Застосунок: Python Course · Дата: 1 вересня 2026

Обсяг перевірки: вбудований Python-інтерпретатор, авторський контент курсу (2194+ вправ, 48 уроків, 17 модулів, розділ «Лайфхаки» — 15 карток, 6 мов), алгоритм інтервального повторення, і надійність самого тестового захисту (мутаційне тестування) для 5 найкритичніших модулів логіки

Коротко: два незалежні одне від одного питання, дві відповіді. Чи коректний код? 9710 кандидатів звірено проти CPython та інших еталонів — 0 розбіжностей. Чи справді тести це захищають? 3694 навмисно внесених дефектів (мутаційне тестування, охоплення виросло разом із кодовою базою) — усі 3694 впіймані. Друге питання — новий розділ цього звіту; воно і є те, "чи можна довіряти" першому.

0 з 9710
розбіжностей із CPython/еталонами
3694 з 3694
навмисних дефектів впіймано тестами (mutation score 100%)

Мутаційне тестування — чи справді тести ловлять баги

Coverage-відсоток відповідає лише на питання "чи виконався цей рядок хоч раз", а не "чи тест взагалі здатен помітити, що рядок зламали". Мутаційне тестування відповідає саме на друге: інструмент навмисно вносить тисячі дрібних, окремих дефектів у код (змінює < на <=, && на ||, прибирає перевірку межі тощо) і перевіряє, чи хоч один тест після цього провалюється. Якщо жоден тест не ловить внесений дефект — це прогалина, навіть якщо формальне покриття коду показує 100%.

Модуль Мутантів Впіймано Score
Логіка розблокування уроків (gate-engine) 208 208 100.00%
Досягнення, XP, стрік, серця (gamification-engine) 267 267 100.00%
Алгоритм інтервального повторення (srs-engine) 47 47 100.00%
Python-інтерпретатор (токенізатор, парсер, виконавець — разом із import/файловим кодом модуля 17) 3123 3123 100.00%
Розділ «Лайфхаки» (lifehacks-engine + manage-lifehacks) 49 49 100.00%
Разом, 5 найкритичніших модулів 3694 3694 100.00%

"Впіймано" тут означає: мутант або зробив хоч один тест червоним, або призвів до нескінченного циклу/зависання (теж зараховується — дефект однозначно виявлено, лише іншим способом; рядок Python-інтерпретатора вище рахує саме так один такий випадок). Кожен окремий випадок, де мутант спершу виживав, розібраний вручну: або написано новий тест, що документує реальну поведінку, або математично доведено, що конкретна зміна коду взагалі не може вплинути на результат (і тоді відповідний рядок позначено в коді коментарем з поясненням — не приховано мовчки), або — де це виявилось найточнішим рішенням — сам код спрощено так, щоб зайва, нічим не підкріплена гілка логіки просто зникла. Один такий випадок детально розібраний нижче.

Методологія — коректність коду (CPython-звірка)

Три незалежні напрями перевірки, без спільного коду й без залежності одного результату від іншого. Інтерпретатор: застосунок виконує код через власний, написаний з нуля Python-інтерпретатор (не CPython і не стороння бібліотека) — кожен кандидат (авторський еталонний розв'язок, згенерований за шаблоном приклад чи випадково згенерована програма) запускався одночасно через інтерпретатор застосунку й через системно встановлений CPython 3.12.3, після чого результати звірялися побітно (значення або текст виводу) або за узгодженістю помилок. Структура курсу: повний, без вибірки, прохід по всіх файлах контенту. Алгоритм інтервального повторення: його результат звірявся з окремо написаною (не скопійованою з коду самого планувальника) реалізацією тієї самої математичної формули. Перевірка велась через тимчасовий, ізольований інструмент поза кодом самого застосунку, у кілька незалежних раундів, без опори на результати попередніх.

Результати проти еталону

Напрямок перевірки Перевірено Збіглося Розбіжностей
Інтерпретатор — контент курсу та розширений режим практики (еталонні + альтернативні розв'язки, fill-the-blank value-режим, хардкор статичні + параметризовані шаблони, реальне виконання import/open() модуля 17) та розділ «Лайфхаки» (before/after знімки коду) 393 393 0
Інтерпретатор — рандомізоване (fuzz) тестування (20 категорій конструкцій) 400 400 0
Алгоритм інтервального повторення (1020 сценаріїв, 8917 кроків перегляду) 8917 8917 0
Разом 9710 9710 0

Структурні та змістові перевірки

Перевірка Охоплення Знайдено проблем
Структура курсу (коректність розв'язків, цілісність графа передумов, унікальність id) 2194 файли вправ, 48 уроків, 17 модулів 0
Структура пулу розширеного режиму практики 64 записи пулу (53 статичних + 11 шаблонів) 0
Розділ «Лайфхаки» (кожна картка посилається на реальний концепт курсу; before/after код дає ідентичний вивід на реальному інтерпретаторі) 15 карток, 30 знімків коду 0
Паритет локалізації (ключі контенту, без неперекладених значень) 12214 ключів × EN, ES, FR, DE, PT (61 070 звірянь) 0

Інструменти

Stryker Mutator — мутаційне тестування (генерація й прогін тисяч навмисних дефектів проти наявного тестового сьюту).
Vitest — виконавець тестів (1112 юніт/інтеграційних тестів на момент цього звіту).
TypeScript, суворий режим — strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes.
CPython 3.12.3 — зовнішній еталон (oracle) для звірки поведінки інтерпретатора.
Власні ізольовані скрипти — генерація fuzz-тестів і звірка алгоритму інтервального повторення, поза кодом самого застосунку.

Як насправді дійшли до цих цифр — чесно, без прикрас

Ці показники — не те, з чим стартували. Перший прогін мутаційного тестування на найбільшому й найскладнішому модулі (Python-інтерпретатор, ~2500 рядків) дав 75.65% — тобто майже чверть навмисних дефектів взагалі ніхто не ловив. Дотягування до 100% зайняло сім послідовних проходів, кожен з яких закривав частину прогалин і виявляв нові — включно з власними методологічними помилками під час самої перевірки.

Знайдені й виправлені реальні баги коду застосунку (не лише прогалини в тестах) — виявлені саме завдяки цій ретельній перевірці: підрахунок балу уроку хибно занижувався після виправленої помилки (одна помилка гарантовано обмежувала максимальний бал, навіть якщо учень одразу виправлявся); placement-тест хибно нараховував досягнення "Без жодної помилки" й "Без пощади" за пропущені, а не реально пройдені уроки; три вузькі баги самого інтерпретатора (цілочисельне ділення, округлення в операторі остачі, порівняння bool з int).

Одна методологічна помилка виявлена й виправлена в процесі: проміжна перевірка (заміна цілого рядка тексту замість мутації за точними координатами) переоцінила кількість "еквівалентних" (тобто нездоланних) мутантів — з 35 таких припущень підтвердилось лише 1. Це виправлено переходом на точну, координатну верифікацію кожного окремого випадку.

Останні 5 мутантів (порівняння рядків для сортування списків) не піддавались ні доведенню еквівалентності, ні тесту через звичайний публічний інтерфейс — сам механізм сортування JavaScript робить надлишкові порівняння, що маскують одну неправильну пару. Розв'язано прямим тестуванням внутрішньої функції порівняння, в обхід непередбачуваної поведінки стороннього алгоритму сортування.

Підсумок: жодне число в цьому звіті не намальоване. Кожне — результат ітеративної, перевіреної роботи, включно з визнанням і виправленням власних помилок по дорозі. Саме тому цим цифрам можна довіряти: код застосунку не лише поводиться правильно на перевірених прикладах — тестовий захист навколо нього справді ловить майбутні регресії, а не просто формально існує.

Чому "2797 з 2797, 100%" на якийсь час стало "3373 з 3374, 99.97%", перш ніж повернутись до 100%: після першого досягнення 100% (запис вище) до інтерпретатора додались нові теми курсу — пряма ітерація for x in iterable: без range(), __str__/__eq__, @staticmethod/@classmethod, словники/множини/comprehensions/генератори — тобто сотні нових рядків коду, які ще ніколи не проходили через мутаційне тестування. Замість того, щоб залишити цей приріст неперевіреним (і мовчки показувати стару, вже неточну цифру "100%"), його прогнано через Stryker так само ретельно, як і решту: 32 нових регресійних тести, кожен написаний проти конкретного мутанта, що вижив (спершу прочитаний код навколо нього, підтверджено логічно, чому наявні тести його не ловлять, і лише потім дописано тест) — не "навмання розширене покриття". Заразом перевірено й весь інший код, що змінювався від дати попереднього мутаційного прогону (17 серпня): парсер (нова граматика для тих самих нових тем) і планувальник інтервального повторення (новий поріг перед interleaving) — обидва виявились уже на 100% без додаткових правок. Після цього лишався рівно один недоведений мутант, і саме тут звіт коротко показував "3373 з 3374, 99.97%" — з поясненням, чому це чесніше за штучну "100%": у функції пошуку локальної змінної (`Scope.tryGet`) була перевірка, яка теоретично мала б спрацювати для двох різних сценаріїв, але один із них виявився математично неможливим, зважаючи на те, як саме ця функція викликається в усьому застосунку — а другий, реально можливий сценарій, уже був покритий окремим тестом. Позначити обидва мутанти на цьому рядку як "еквівалентні" одним коментарем було б найшвидшим шляхом до "100%" — і саме тому цього спершу не зробили: інструмент мутаційного тестування вміє вимикати перевірку лише для рядка цілком, а не для одного конкретного мутанта, тож так само прибрало б з-під контролю й той сценарій, що реально тестувався.

Що змінилось після — і чому це вже не компроміс, а справжнє 100%: замість того щоб змиритись із 99.97% чи замаскувати залишок, поставлено інше питання — а чи не мертвий цей рядок узагалі? Повторний, ретельніший аналіз усіх викликів `tryGet()` у застосунку (їх рівно один) і всіх місць, де взагалі створюється значення, яке ця перевірка мала захищати, показав: так, рядок був справді зайвим — не "важко протестувати", а логічно недосяжний для будь-якого реального стану програми. Рядок видалено повністю. Це не "затичка": видалення дійсно мертвого коду не втрачає жодного реального покриття (на відміну від вимкнення мутанта коментарем, яке прибрало б заразом і мутант, що реально ловився тестом) — просто мутант, породжуваний тим рядком, перестав існувати разом із самим рядком. Повний тестовий сьют (на той момент 1035 тестів) підтвердив: поведінка застосунку не змінилась ні на йоту. Наступний прогін Stryker дав справжні, а не підроблені **100.00%**. Підсумок цього маленького відхилення в самому звіті: чесна цифра трохи нижче 100% і готовність її показати виявилась кращим шляхом до реального 100%, ніж спроба одразу намалювати ідеальний результат.

Звідки взялось зростання з 3371 до 3694 і з 9655 до 9710 (1 вересня 2026): курс виріс — новий 17-й модуль «Import, модулі та робота з файлами» додав реальне (не лише розпізнавання синтаксису) виконання import/open()/with проти симульованої віртуальної файлової системи, і новий розділ «Лайфхаки» (15 карток «було → стало» з поясненням, чому коротший запис працює так само). Обидва додавання прогнані через ту саму дисципліну, що й решта застосунку: interpreter.ts і parser.ts заново пройшли мутаційне тестування (+238 і +36 мутантів відповідно, усі впіймані), новий lifehacks-engine і manage-lifehacks — з нуля до 100% (2 реальні прогалини в тестах закриті, а не замасковані), усі 15 карток (30 знімків коду) звірені з CPython, 2 нові цикло-орієнтовані вправи модуля 17 — так само. Окремо цього ж дня закрито останній перекладацький борг: розділ «Лайфхаки» й усі 434 нові ключі модуля 17 перекладено на іспанську, французьку, німецьку та португальську одразу, тож усі 6 мов курсу тепер на 100% паритеті вмісту (12214 ключів кожна) — саме тому число звірянь локалізації виросло з 58635 до 61070.

Примітки

У процесі ітеративної перевірки знайдено й виправлено три вузькі баги інтерпретатора — жоден з них ніколи не впливав на реальний контент курсу, і всі три тепер назавжди закріплені в тестах, які стежать, щоб ці помилки не повторилися:

Верхня межа інтервалу в алгоритмі інтервального повторення коректно спрацьовувала в кількох сотнях кроків, спеціально розрахованих на її досягнення, — без жодного винятку. Наведені результати — з останнього, найсвіжішого раунду перевірки, без відомих відкритих проблем.