Якість і надійність коду
Застосунок: Python Course · Дата: 1 вересня 2026
Обсяг перевірки: вбудований Python-інтерпретатор, авторський контент курсу (2194+ вправ, 48 уроків, 17 модулів, розділ «Лайфхаки» — 15 карток, 6 мов), алгоритм інтервального повторення, і надійність самого тестового захисту (мутаційне тестування) для 5 найкритичніших модулів логіки
Коротко: два незалежні одне від одного питання, дві відповіді. Чи коректний код? 9710 кандидатів звірено проти CPython та інших еталонів — 0 розбіжностей. Чи справді тести це захищають? 3694 навмисно внесених дефектів (мутаційне тестування, охоплення виросло разом із кодовою базою) — усі 3694 впіймані. Друге питання — новий розділ цього звіту; воно і є те, "чи можна довіряти" першому.
Мутаційне тестування — чи справді тести ловлять баги
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 |
Інструменти
strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes.Як насправді дійшли до цих цифр — чесно, без прикрас
Ці показники — не те, з чим стартували. Перший прогін мутаційного тестування на найбільшому й найскладнішому модулі (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.
Примітки
У процесі ітеративної перевірки знайдено й виправлено три вузькі баги інтерпретатора — жоден з них ніколи не впливав на реальний контент курсу, і всі три тепер назавжди закріплені в тестах, які стежать, щоб ці помилки не повторилися:
- цілочисельне ділення виводилось як ціле число замість справжнього дробового результату Python (
11замість11.0); - рідкісний артефакт округлення в операторі остачі (
%) на дробових операндах; boolне порівнювався як числово рівнийintу==/!=/in/list.remove().
Верхня межа інтервалу в алгоритмі інтервального повторення коректно спрацьовувала в кількох сотнях кроків, спеціально розрахованих на її досягнення, — без жодного винятку. Наведені результати — з останнього, найсвіжішого раунду перевірки, без відомих відкритих проблем.