← До списку уроків
Заняття 12 з 13Тести й безпечне оновлення
курс 0% виконано0%
Заняття 12 · місяць 3

Тести й безпечне оновлення

90 хвилин

Навчити бота перевіряти самого себе: написати автотести, які за секунду ловлять поломку, зробити скрипт deploy.sh, що не розгортає код із червоними тестами й сам повертає стару версію, якщо нова не запустилася, і підключити GitHub, який проганяє ті самі тести та ставить зелену галочку.

Мій сайт
http://91.219.61.4/u/<логін>/
Мій сабдомен
https://<логін>.<домен>/
Мій кабінет
http://91.219.61.4/k/<логін>/
Вхід на сервер
ssh <логін>@91.219.61.4

Замість <логін> підставляєш свій логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться. У прикладах на цій сторінці скрізь стоїть bohdan.g9 — читай це як свій логін і виправляй у кожній команді, яку копіюєш. Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні.

<домен> — назва, яку ти записав у файл ~/app/project.yaml на занятті 10, розділ server, поле url. Сабдомен (із заняття 10) — це частина адреси перед доменом: в імені bohdan.g9.klub.example домен — klub.example, а сабдомен — bohdan.g9. Саме цю адресу ти сьогодні даєш гостям, тому перевір її першою справою.

Перші два рядки ведуть на одне й те саме: за обома адресами nginx віддає файли з твоєї теки ~/www. Бот у цьому не бере участі: він сам звертається до Telegram.

Що ти зробиш сьогодні

Твій бот уже вміє багато: вітається, показує підказку, слухається лише власника, запускає скрипти, пише першим за розкладом. Кожна нова вправа ризикує зламати щось зі старого. Досі ти перевіряв усе очима: писав боту (або запускав try_bot.py) і дивився на відповідь. Чим більше команд, тим довше так перевіряти — і тим легше щось пропустити.

Сьогодні цю роботу візьме програма. У теці ~/app/tests з’являться автотести: короткі функції, які самі «пишуть» боту повідомлення й звіряють відповіді з очікуваними. Дванадцять тестів перевіряють /start, /help, /whoami, відмову чужому, створення й запуск скрипта, зупинку скрипта, що завис, і те, що розклад не спрацьовує двічі за день. Одна команда — і за секунду видно: або все гаразд, або що саме зламалося.

Друга половина заняття — безпечне оновлення. Скрипт deploy.sh перед розгортанням нової версії проганяє тести й відмовляється працювати, якщо хоч один червоний. А якщо тести зелені, але нова версія не запустилася, скрипт сам повертає код до останньої справної версії. Наприкінці GitHub почне сам проганяти ті самі тести після кожного git push і ставити біля коміту зелену галочку.

До кінця заняття в тебе буде:

Telegram на цьому занятті не потрібен: тести розмовляють із функцією handle_update напряму, так само, як try_bot.py. Усе зробить і той, хто ще не має акаунта в Telegram.

Теорія

1. Навіщо боту автотести

Тарас на занятті 09 вирішив зробити код гарнішим і переписав у bot.py рядок із перевіркою власника. Перевірив: написав боту від свого імені /run privit — працює. Зберіг комітом. Тиждень потому виявилося, що тепер /run виконує будь-хто: у новому рядку Тарас випадково переплутав порівняння. Власника він перевіряв, чужого — ні, бо чужого в нього під рукою не було.

Така помилка має назву — регресія (від слова «регрес», рух назад): зламалося те, що раніше працювало, часто зовсім не там, де ти правив. Очима її шукати тяжко, бо перевіряєш лише щойно змінене.

Автотест — програма, яка сама виконує дію у твоєму коді й звіряє результат з очікуваним. Очікуване записують командою assert («стверджую, що це правда»):

assert 2 + 2 == 4          # правда: нічого не відбувається, програма йде далі
assert 2 + 2 == 5          # неправда: помилка AssertionError, тест «впав»
assert "/ask" in "/ask питання"   # слово in для рядків: чи є шматок усередині

Набір тестів перевіряє все щоразу, зокрема те, чого ти не чіпав, і за секунду. Тому регресію видно ще до коміту. Схоже на ключ відповідей до контрольної: вчитель не розв’язує задачу заново, а звіряє твою відповідь із ключем.

Оля змінила привітання в /start, перевірила його у try_bot.py — гарно — і зберегла комітом. Через день виявилося, що бот перестав записувати людей у базу: разом із привітанням вона стерла сусідній рядок remember(sender). Що помітило б це одразу?

2. Як pytest знаходить і запускає тести

pytest — програма-«перевіряльник» для Python (вона вже стоїть у спільному середовищі сервера). Правила прості: бере файли test_*.py з теки tests, у кожному — функції, назва яких починається з test_, і по черзі їх виконує. Ось справжній тест із сьогоднішнього файлу:

def test_start_greets_by_name():
    assert "Оля" in bot.handle_update(message("/start"))

Функція message("/start") збирає таке саме повідомлення, яке Telegram надіслав би боту, від користувачки Олі. bot.handle_update(...) — та сама функція, що відповідає людям (заняття 02): вона отримує повідомлення й повертає текст. Тест перевіряє, що в привітанні є ім’я «Оля».

Результат pytest показує одним рядком: крапка — тест зелений (пройшов), літера F — червоний (упав). Нижче pytest пише, у якому рядку що не збіглося. Дванадцять крапок і 12 passed — усе гаразд.

3. Тест має падати, коли бот зламаний

Тест, який зелений завжди, нічого не стереже. Ось тест, що виглядає солідно, але марний:

def test_help():
    assert bot.handle_update(message("/help"))      # лише «відповідь не порожня»

Він пройде, навіть якщо підказка стане словом «привіт». Справжній тест порівнює з тим, що саме має бути: assert "/ask" in bot.handle_update(message("/help")).

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

Який тест краще стереже перевірку власника?

4. Тести не чіпають справжнього бота

Тести запускаються на тому самому сервері, де живе твій бот. Якби вони писали у справжню базу чи справжні скрипти, то кожен запуск лишав би сміття, а тест «чужий» міг би щось зіпсувати. Тому на початку файлу стоїть підготовка, до того як bot імпортується. Вона складається зі змінних оточення — налаштувань, які програма читає не з коду, а із середовища, де її запустили; bot.py читає їх на старті:

Ще одна хитрість — підміна (в pytest її роблять через monkeypatch): на час одного тесту підставляємо інше значення. Скрипт, що завис, зупиняється через 10 секунд — чекати стільки в кожному запуску довго, тож у тесті ліміт тимчасово стає 1 секунда. А замість справжнього надсилання в Telegram bot.tg підміняється функцією, яка лише складає повідомлення в список — і тест бачить, що саме бот «надіслав», нічого нікуди не відправляючи.

Побічна вигода: тести швидкі й повторювані. Запусти їх хоч сто разів, хоч уночі — результат той самий.

5. Безпечне оновлення: два запобіжники й тег last-good

Розгортання (по-англійськи deploy) — це момент, коли нова версія коду починає працювати замість старої. У нашому випадку: код лежить у ~/app, а перезапуск служби club-bot змушує її прочитати нові файли. До перезапуску служба працює зі старим кодом у пам’яті, навіть якщо файли вже змінено.

Скрипт deploy.sh робить кроки в такому порядку, і порядок важливий:

КрокЩо робитьЯкщо не вдалося
1. Чи все збережено комітомперевіряє, що тестується саме той код, який у Gitзупиняється й просить спершу git commit
2. Забрати новеgit pull з репозиторію на серверіне страшно: може не бути нічого нового
3. Запобіжник 1: тестипроганяє pytestвиходить з кодом 1, служба не чіпається: працює стара справна версія
4. Перезапускперезапускає службу—
5. Запобіжник 2: перевіркадо 30 секунд чекає, поки бот оживе: служба active, а в last_poll свіжий час (бот уже спитав Telegram після перезапуску)відкат на last-good, ще один перезапуск, вихід з кодом 1
6. Міткаякщо служба відповіла — ставить тег git tag -f last-good на цей коміт—

Тег (англ. tag) — це постійне ім’я для конкретного коміту, як закладка в книзі. last-good означає «остання версія, яка запустилась і відповідала». Відкат — це git reset --hard last-good: гілка main і файли в теці стають такими, як у цій закладці. Саме це слово означає «відкат» у нашому курсі; про повернення до випущеної версії 1.0 буде на занятті 13. Зламану версію скрипт не губить: ставить на неї тег broken, і потім можна глянути, що було не так: git diff last-good broken.

Чому при червоних тестах відкату немає, а при мовчазній службі є? У першому випадку служба ще не перезапускалась і далі працює зі старим кодом — відкочувати нічого. У другому — вже запущено новий код, який не завівся, і його треба прибрати. А ще скрипт вимагає, щоб усе було збережено комітом: reset --hard стирає незбережені зміни, а правила безпеки такі — не стирати те, чого немає в історії.

Змінні оточення DEPLOY_RESTART, DEPLOY_CHECK, DEPLOY_WAIT дозволяють підмінити «як перезапустити», «як перевірити» і «скільки чекати» ззовні, не змінюючи файлу. Так ми перевірятимемо червоний тест і відкат на копії, не торкаючись справжньої служби.

Ти зберіг зміну комітом, запустив ./deploy.sh. Тести зелені, службу перезапущено, але бот 30 секунд не оживає (last_poll не оновлюється). Що робить скрипт?

Один тест червоний. Яке твердження про ./deploy.sh правильне?

6. GitHub Actions: той самий pytest, але в чистому місці

Тести на твоєму сервері проходять — і це приємно, але це ще не все. На сервері в тебе лежать файли, яких немає в Git: .env, база, може, забутий scripts.py, що не потрапив у коміт. «У мене працює» — улюблене виправдання програмістів, і причина часто саме в цьому.

CI (безперервна перевірка, англ. continuous integration) вирішує це так: після кожного git push окремий комп’ютер береться з нуля, завантажує лише те, що є в репозиторії, і проганяє тести. На GitHub це робить GitHub Actions. Що саме робити, записано у файлі .github/workflows/tests.yml. Це YAML — формат, де структуру задають відступами (два пробіли на рівень):

Біля коміту на сторінці репозиторію з’являється: жовте коло — іде перевірка, зелена галочка — усе пройшло, червоний хрестик — щось упало (можна клацнути й побачити журнал, як у термінала). Хрестик — не ганьба, а сигнал: хтось, зазвичай ти сам, щось забув зберегти комітом.

Секрети (.env, токен бота) на GitHub не потрапляють узагалі — вони в .gitignore. Тому тести й не вимагають справжнього токена: це ще одна причина, чому вони працюють із тимчасовими значеннями.

На сервері всі тести зелені, а на GitHub біля коміту червоний хрестик. Найімовірніша причина?

Слова цього уроку

СловоЩо означає
автотестПрограма, що сама виконує дію у коді й звіряє результат з очікуваним
pytestПрограма, що знаходить тести (tests/test_*.py), запускає їх і показує підсумок
assert«Стверджую, що це правда». Неправда — тест червоний
зелений / червоний тестПройшов / упав. Крапка й літера F у виводі pytest
регресіяПоломка того, що раніше працювало, після неспоріднених правок
підміна (monkeypatch)На час тесту підставити інше значення чи функцію: ліміт 1 с замість 10, фальшиве надсилання
розгортання (deploy)Момент, коли нова версія починає працювати замість старої
тег (tag)Постійне ім’я конкретного коміту; last-good — остання справна версія
відкатПовернення гілки до попередньої справної версії: git reset --hard last-good
змінна оточенняНалаштування, яке програма читає із середовища запуску: DEPLOY_WAIT=3 ./deploy.sh
CIБезперервна перевірка: після кожної зміни окремий комп’ютер сам проганяє тести
workflow, job, stepСценарій GitHub Actions; завдання в ньому; крок завдання
зелена галочкаЗнак біля коміту на GitHub: усі тести там пройшли

Практика

Виконуй кроки по черзі. У прикладах <логін> заміни на свій, наприклад bohdan.g9 (дужки теж прибери). У кожного кроку є «що має вийти» — порівняй і, якщо не збіглося, дивись розділ «Якщо щось не працює» внизу. Тести запускай як /opt/club/venv/bin/python3 -m pytest -q: у цьому середовищі вже стоять pytest і потрібні пакети.

1 Зайти на сервер і подивитися, з чого починаємо

ssh <логін>@91.219.61.4
cd ~/app
git status --short
git log --oneline -3
systemctl --user is-active club-bot
cat ~/data/bot_state.json
ls /opt/club/kurs/12/ /opt/club/kurs/12/tests

2 Скопіювати тести й запустити їх уперше

mkdir -p ~/app/tests
cp /opt/club/kurs/12/tests/test_bot.py ~/app/tests/
echo ".pytest_cache/" >> ~/app/.gitignore
cd ~/app
/opt/club/venv/bin/python3 -m pytest -q

Що має вийти: рядок із дванадцяти крапок і підсумок на зразок 12 passed in 1.2s (час може відрізнятися). Рядок з echo просто додає службову теку .pytest_cache у .gitignore, щоб вона не потрапляла в коміти.

Якщо замість цього ModuleNotFoundError: No module named 'scripts' чи 'init_db' — не вистачає файлу з попереднього заняття: порівняй свою теку з ls /opt/club/kurs/09 і ls /opt/club/kurs/04. Червоні тести (F) при чужому коді — це не помилка pytest, а повідомлення про розбіжність: читай рядок після E у виводі, у ньому «чекали одне, отримали інше».

3 Прочитати, як тести готуються і що перевіряють

less ~/app/tests/test_bot.py
/opt/club/venv/bin/python3 -m pytest -v

У less прокручуй пробілом, вийти — q. Знайди очима: блок os.environ[…] = … на початку (розділ 4 теорії), функцію message(), яка збирає повідомлення, автофікстуру clean_state (перед кожним тестом вона стирає тимчасові скрипти й розклад, щоб тести не заважали один одному; спершу вона перевіряє, що стирає саме тимчасову теку, а не твої справжні скрипти), а далі дванадцять тестів у чотирьох групах: «вітання й підказка», «замок власника», «ліміт часу» і «розклад».

Команда з -v (від verbose, «докладно») друкує кожен тест окремим рядком: tests/test_bot.py::test_stranger_is_refused PASSED і так дванадцять разів, а в кінці знову 12 passed. Знайди тест, який перевіряє, що розклад не спрацьовує двічі, і прочитай його: він викликає bot.run_due("09:00") двічі й чекає рівно одне надіслане повідомлення.

4 Навмисно зламати підказку — і побачити червоний тест

cd ~/app
sed -i 's|"/whoami — твій|"/whoamy — твій|' bot.py
git diff bot.py
/opt/club/venv/bin/python3 -m pytest -q

Команда sed -i 's|було|стало|' файл замінює в файлі було на стало (символ | тут лише розділяє частини). Вона замінила в підказці /whoami на /whoamy (одна літера). git diff bot.py показує цю заміну: мінус — було, плюс — стало. Що має вийти від pytest:

.F..........                                                             [100%]
=================================== FAILURES ===================================
___________________________ test_help_lists_commands ___________________________
    def test_help_lists_commands():
        reply = bot.handle_update(message("/help"))
>       assert "/ask" in reply and "/whoami" in reply
E       AssertionError: assert (...)
FAILED tests/test_bot.py::test_help_lists_commands
1 failed, 11 passed in 1.2s

Перша крапка — зелений тест, друга позиція — F. Стрілка > вказує на рядок, де не збіглося. Одинадцять інших тестів зелені — отже, тест стереже саме підказку. Тепер поверни, як було:

git restore bot.py
/opt/club/venv/bin/python3 -m pytest -q

Знову 12 passed. git restore повертає файл до останнього коміту (заняття 03). Саме тому на початку було важливо зберегти все комітом.

5 Зламати замок власника: регресія наживо

Тепер повтори те, що трапилося з Тарасом: «спрощуємо» перевірку власника так, що вона вимикається.

cd ~/app
sed -i 's|if not OWNER or str(chat_id) != OWNER:|if False:|' bot.py
git diff bot.py
/opt/club/venv/bin/python3 -m pytest -q

Що має вийти: 1 failed, 11 passed і червоний test_stranger_is_refused. У рядку після E видно: чекали «лише для власника», а бот відповів Створено scripts/hack.sh… — чужий щойно створив скрипт. Без тестів ти б цього не побачив: як власник усе працювало б. Поверни замок:

git restore bot.py
/opt/club/venv/bin/python3 -m pytest -q
git status --short

Має бути 12 passed, а git status --short не друкує нічого про bot.py. Там можуть бути лише рядки про tests/ і .gitignore — це нормально: головне, що bot.py повернувся до справного стану.

6 Дописати свій тест

Додай у кінець файлу тест на команду, якої ще немає в наборі: порожній розклад.

cat >> ~/app/tests/test_bot.py <<'EOF'


def test_schedule_is_empty_at_start():
    assert "порожній" in bot.handle_update(message("/schedule"))
EOF
cd ~/app
/opt/club/venv/bin/python3 -m pytest -q

Що має вийти: 13 passed. Запис <<'EOF' означає «усе до рядка EOF — це текст, який треба дописати до файлу». Обов’язково переконайся, що твій тест може почервоніти. Для цього зламай не тест, а бота (тест має стерегти саме його): sed -i 's/Розклад порожній/Розклад ZZZ/' ~/app/scripts.py і знову pytest. Має бути 1 failed. Потім поверни: cd ~/app && git restore scripts.py і перевір, що знову 13 passed.

Як користуватися ШІ: можна попросити безкоштовний ШІ запропонувати тести для якоїсь команди. Але тест, який він написав, перевір так само: зламай бота й подивись, чи тест почервонів. Бувають тести, що «зеленіють» завжди, — а це гірше за відсутність тесту, бо дає хибну впевненість.

7 Зберегти тести комітом

cd ~/app
git add -A
git commit -m "Заняття 12: автотести бота"
git status --short

Після коміту git status --short порожній. Це потрібно далі: deploy.sh тестує лише те, що збережено комітом, а автоперевірка бере копію саме з Git.

8 Поставити deploy.sh і розібрати його

cp /opt/club/kurs/12/deploy.sh ~/app/
chmod +x ~/app/deploy.sh
cd ~/app
less deploy.sh

У less гортай пробілом, вийти — q. Прочитай скрипт по частинах і знайди відповідники з таблицею з розділу 5 теорії:

Збережи його комітом, бо скрипт вимагає, щоб усе було збережено:

git add deploy.sh
git commit -m "Заняття 12: deploy.sh із тестами й відкатом"

chmod +x дозволяє запускати файл як програму. Git запам’ятав це «право запуску» разом із комітом: без нього в копії (кроки 10–11) вийшло б Permission denied.

9 Перше розгортання і друге одразу за ним

cd ~/app
./deploy.sh
echo "код виходу: $?"
./deploy.sh
echo "код виходу: $?"
git tag

Увага: скрипт справді перезапускає бота на кілька секунд, а якщо нова версія не запуститься, сам поверне гілку main до тега last-good (reset --hard). Тому перед запуском усе має бути збережено комітом. Що має вийти після першого запуску (рядки From … та Already up to date. від git pull можуть відрізнятися):

==> перевіряю, що все збережено комітом
==> забираю нову версію
Already up to date.
==> тести
.............                                                            [100%]
13 passed in 1.3s
==> перезапуск служби
==> перевірка
    бот живий; тег last-good тепер на 8afb66e
код виходу: 0

Другий запуск друкує ті самі кроки (==>) і так само закінчується кодом 0 — це й означає «двічі поспіль однаковий результат»: скрипт можна запускати скільки завгодно, він нічого не накопичує і не ламає. git tag покаже last-good.

Якщо бачиш «є незбережені зміни» — у тебе лишилися правки без коміту: git status --short, потім git add -A і git commit. Якщо бачиш «бот не ожив за 30 с» — подивись tail -n 30 ~/logs/club-bot.log: часто це помилка в коді, і вона ж залишиться в журналі.

10 Репетиція на копії: червоний тест не пройде

Ламати справжнього бота не будемо. Зробимо копію репозиторію у своїй домашній теці (~/proba) і випробуємо скрипт там. Змінна DEPLOY_RESTART підміняє «перезапуск» на безпечний echo — тож справжню службу ми не зачеплемо.

rm -rf ~/proba
git clone -q ~/app ~/proba
cd ~/proba
printf 'def test_zlamanyi():\n    assert 1 == 2\n' > tests/test_zlamanyi.py
git add -A
git commit -qm "навмисно червоний тест"
DEPLOY_RESTART="echo ЯК-БИ-ПЕРЕЗАПУСТИВ" ./deploy.sh
echo "код виходу: $?"

Команда printf створила в копії новий тест, який завжди падає (1 == 2), а git commit зберіг його. Що має вийти: кроки ==> до ==> тести, далі pytest з F і 1 failed, 13 passed, потім рядок ТЕСТИ ЧЕРВОНІ — не розгортаю. Служба далі працює зі старою справною версією. і код виходу: 1. Слів ЯК-БИ-ПЕРЕЗАПУСТИВ на екрані немає — перезапуск до нього не дійшов. Це і є запобіжник 1.

11 Репетиція на копії: служба мовчить — відкат

У цій самій копії приберемо червоний тест і зробимо коміт, після якого «бот не ожив». Для цього DEPLOY_CHECK підмінимо на команду «успіх, якщо файлу BROKEN немає». Спочатку git reset --hard HEAD~1 повертає копію на один коміт назад (HEAD~1 — «попередній коміт») і так прибирає червоний тест, а touch BROKEN створює порожній файл BROKEN:

cd ~/proba
git reset -q --hard HEAD~1
touch BROKEN
git add BROKEN
git commit -qm "служба мовчить"
DEPLOY_RESTART="echo перезапуск" DEPLOY_CHECK="test ! -e BROKEN" DEPLOY_WAIT=3 ./deploy.sh
echo "код виходу: $?"
ls BROKEN
git tag
git diff --stat last-good broken

Що має вийти: тести зелені, перезапуск, потім бот не ожив за 3 с (чекали 3 секунди, бо DEPLOY_WAIT=3), далі ==> відкат до last-good, ще один перезапуск, відкотили: стара версія знову жива… і код виходу: 1. Команда ls BROKEN скаже No such file or directory — файл зник разом із поганим комітом. git tag покаже два теги: broken і last-good, а git diff --stat — що саме було в поганій версії (BROKEN | 0).

cd ~
rm -rf ~/proba

Копію видалено. Справжня служба весь цей час працювала без перерви.

Зберегти роботу на GitHub

Остання частина — щоб GitHub сам перевіряв кожну зміну. Файл сценарію називається tests.yml і лежить у прихованій теці .github/workflows (крапка на початку імені робить теку прихованою).

Немає акаунта на GitHub (тобі ще немає 13 років або батьки не дозволили)? Зроби все до git commit включно, а git push і крок 13 пропусти: файл workflow однаково має бути збережений комітом, і автоперевірка його зарахує. Галочку тоді покаже викладач на своєму прикладі.

12 Додати workflow і відправити на GitHub

mkdir -p ~/app/.github/workflows
cp /opt/club/kurs/12/tests.yml ~/app/.github/workflows/
cd ~/app
cat .github/workflows/tests.yml
git add -A
git commit -m "Заняття 12: тести, deploy.sh з відкатом, GitHub Actions"
git push origin main
git push github main

cat покаже сценарій: name: tests, on: [push, pull_request], завдання test на ubuntu-latest і чотири кроки. Дивись, щоб відступи були пробілами (клавішу Tab у YAML вживати не можна). Перевір, що файл читається як YAML: /opt/club/venv/bin/python3 -c "import yaml; print(list(yaml.safe_load(open('.github/workflows/tests.yml'))['jobs']))" має надрукувати ['test']; помилка означає збиті відступи. Кожен git push закінчується рядком main -> main; якщо в тебе гілка master, пиши master. Бачиш у git status --short файл .env — зупинись і скажи викладачеві: його не мало бути (заняття 03).

13 Знайти зелену галочку

Відкрий у браузері свій репозиторій club-app на GitHub (https://github.com/<логін-github>/club-app) і вкладку Actions. Там з’явився запуск з назвою твого останнього коміту. Спершу жовте коло (іде перевірка, 20–40 секунд), потім зелена галочка. Така сама галочка стоїть поруч із комітом на головній сторінці репозиторію.

Що перевіряє автоматика

Автоперевірка не чіпає ні твоїх справжніх скриптів, ні бази, ні Telegram, ні служби: усе ламає в тимчасових копіях репозиторію й бере лише те, що збережено комітом. Спершу запускає твої тести й рахує зелені (поріг — шість, хоча в наборі їх дванадцять). Потім по черзі ламає в копії бота — підказку, /whoami, замок власника, запис у базу, ліміт часу скрипта, розклад — і дивиться, що тести хоч раз почервоніли. Далі в копії двічі запускає deploy.sh (результат має збігтися), додає червоний тест (скрипт має відмовитися) і «службу, що мовчить» (скрипт має відкотитися на last-good). Ще перевіряє файл tests.yml і те, що бот живий (служба club-bot active, last_poll свіжий). Галочку на GitHub і розповідь про два випадки (червоні тести, мовчазна служба) оцінює викладач.

Натисни «Перевірити практичну частину» в розділі «Перевірка виконаних завдань» нижче на сторінці. Перед цим збережи все комітом (git status --short має бути порожнім).

КодЩо перевіряєтьсяЯкщо не зараховано
L12.1автотести бота: щонайменше 6 проходять (у наборі їх 12)
автоперевірка
Скопіюй тести: mkdir -p ~/app/tests && cp /opt/club/kurs/12/tests/test_bot.py ~/app/tests/ . Запусти cd ~/app && /opt/club/venv/bin/python3 -m pytest -q — побачиш, який тест червоний і чому.
L12.2тести справжні: зламаний бот червонить хоч один
автоперевірка
Перевірка ламає копію бота (підказку, /whoami, перевірку власника, запис у базу, ліміт часу, розклад) і дивиться, чи хоч один тест почервонів. «Тести лишилися зеленими» — додай assert, що порівнює з очікуваним текстом, а не лише перевіряє «не порожньо». «У чистій копії тести червоні» — вони червоні вже зараз: запусти pytest у ~/app і виправ, потім збережи комітом. «Не знайшов рядків, які я ламаю» — ти сильно переписав bot.py чи scripts.py: порівняй з /opt/club/kurs/09/.
L12.3deploy.sh двічі поспіль дає однаковий результат
автоперевірка
cp /opt/club/kurs/12/deploy.sh ~/app/ && chmod +x ~/app/deploy.sh; збережи зміни комітом (git add -A && git commit) — скрипт відмовляється працювати з кодом, не збереженим комітом. Запусти двічі: ./deploy.sh && ./deploy.sh
L12.4deploy.sh відмовляє розгортати, коли тест червоний
автоперевірка
Перевірка в тимчасовій копії додає червоний тест і запускає deploy.sh: він має зупинитися з кодом 1 і НЕ перезапускати службу. Перевір, що рядок restart стоїть після pytest і що pytest у if ! … ; then exit 1.
L12.5якщо служба не відповіла, deploy.sh відкочується на last-good
автоперевірка
У копії виходить так: вдалий deploy ставить тег git tag -f last-good, наступний «поганий» (служба не відповідає) має зробити git reset --hard last-good, перезапустити службу й вийти з кодом 1. Візьми оригінал з /opt/club/kurs/12/deploy.sh.
L12.6є .github/workflows/tests.yml, що запускає pytest
автоперевірка
mkdir -p ~/app/.github/workflows && cp /opt/club/kurs/12/tests.yml ~/app/.github/workflows/ ; потім збережи комітом: git add -A && git commit. У файлі мають бути on: [push, …], jobs → … runs-on, крок uses: actions/checkout і крок run: python -m pytest; відступи — пробіли (python -c "import yaml…" з кроку 12 покаже помилку).
L12.7після розгортання служба club-bot active і last_poll свіжий
автоперевірка
Бот не ожив. systemctl --user status club-bot і tail -n 30 ~/logs/club-bot.log; якщо щойно був deploy.sh, він мав відкотитися — перевір git log --oneline -3 і git tag.
L12.8на GitHub біля останнього коміту зелена галочка
викладач
Позначку ставить викладач на занятті або після нього.
L12.9пояснив, що робить deploy.sh при червоних тестах і при мовчазній службі
викладач
Позначку ставить викладач на занятті або після нього.

Твої відповіді на питання уроку

Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.

Перевірка виконаних завдань

Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.

Домашнє завдання

  1. Ще три тести. Напиши й додай у tests/test_bot.py: (а) /every 25:00 privit від власника відповідає помилкою з «ГГ:ХХ»; (б) після /new x1 echo 1, /every 09:00 x1 команда /stop x1 відповідає «прибрано», а /schedule знову показує «порожній»; (в) /new Великі echo 1 відмовляє із «латиницею». Кожен тест перевір на живучість: тимчасово зламай відповідний рядок у scripts.py і переконайся, що тест червоніє, потім поверни. Має бути не менше 16 зелених.
  2. Побач червоний хрестик і прибери його. Додай у tests/ файл test_zlamanyi.py з assert 1 == 2, збережи комітом і зроби git push github main. Подивись у вкладці Actions хрестик і журнал. Прибери файл (git rm tests/test_zlamanyi.py), збережи комітом, відправ — і дочекайся галочки. Увага: не запускай ./deploy.sh, поки цей файл лежить у репозиторії. Наприкінці в тебе має бути зелена галочка: її бачить викладач.
  3. Поясни своїми словами (напиши 3–4 речення в README.md під заголовком «## Тести й оновлення»): що робить deploy.sh при червоних тестах і що — коли служба не відповіла; чому в першому випадку відкату немає.
  4. Підготуйся до заняття 13. На ньому ти показуватимеш бота гостям і випускатимеш версію 1.0. Подумай, які 4–5 команд покажеш за три хвилини і що зробиш, якщо Telegram або ШІ раптом не відповідатимуть.
  5. Збережи. git add -A, git commit -m "Заняття 12: домашня робота", git push origin main і git push github main.

Якщо щось не працює

pytest: command not found, No module named pytest або No module named 'yaml'

Ти запустив системний Python, а потрібні пакети стоять у спільному середовищі. Завжди пиши повну команду: /opt/club/venv/bin/python3 -m pytest -q. Щоб не набирати щоразу, у тій самій теці можна запускати ./deploy.sh: він уже використовує потрібний Python.

L12.1: ERROR collecting tests/test_bot.py або червоний тест, хоча бот працює

L12.2: «я зламав бота, а тести лишилися зеленими» (і ще два повідомлення)

Перевірка пише одне з трьох. «Я зламав бота, а тести лишилися зеленими» — назвала, що саме зламала. Знайди тест на це місце й подивись, що він порівнює. «У чистій копії тести червоні» — тести червоні вже зараз, ще до поломки: запусти pytest у ~/app і виправ (і переконайся, що все збережено комітом). «Не знайшов рядків, які я зазвичай ламаю» — ти сильно переписав bot.py чи scripts.py; порівняй з /opt/club/kurs/09/. Типова причина першого випадку: assert reply (відповідь «не порожня») замість assert "лише для власника" in reply. Додавай порівняння з конкретним текстом або наслідком (файл не створився, повідомлення пішло одне, а не два). Якщо ти видалив один із готових тестів — поверни його з /opt/club/kurs/12/tests/.

L12.3: ./deploy.sh — Permission denied, є незбережені зміни, служба не відповіла

L12.4 або L12.5: «у репозиторії немає deploy.sh», «не користується змінною DEPLOY_RESTART», «відкату немає»

Перевірка працює з копією того, що збережено комітом: що лежить лише на диску, вона не бачить. Виконай git status --short (маєш побачити порожньо) і git ls-files | grep -E "deploy.sh|tests" (обидва мають бути в списку). Якщо ти переписував скрипт сам — порівняй з оригіналом: diff ~/app/deploy.sh /opt/club/kurs/12/deploy.sh. Скрипт має: (1) зупинятися з exit 1 після червоного pytest до перезапуску; (2) після вдалої перевірки ставити git tag -f last-good; (3) у разі мовчазної служби робити git reset --hard last-good, перезапускати й виходити з кодом 1; (4) брати «як перезапустити» й «як перевірити» зі змінних DEPLOY_RESTART і DEPLOY_CHECK.

L12.6: workflow не зараховано або не запускається на GitHub

L12.7: бот не живий після розгортання

systemctl --user status club-bot і tail -n 30 ~/logs/club-bot.log. Якщо щойно був ./deploy.sh і він відкотився, то код уже той, що працював; подивись git log --oneline -3 та git tag. Служба не стоїть після перезапуску — запусти її: systemctl --user start club-bot, зачекай 5 секунд і повтори cat ~/data/bot_state.json: last_poll має бути свіжим (не старшим за дві хвилини); немає токена в .env — бот не питатиме Telegram і last_poll не з’явиться.

На GitHub червоний хрестик, а на сервері тести зелені

Відкрий запуск у вкладці Actions, клацни на червоний крок і прочитай рядок після E чи ModuleNotFoundError. Найчастіше це файл, що є на сервері, але не в Git: git status --short покаже його знаком ??. Додай git add, збережи комітом і відправ. Другий варіант — тест залежить від чогось зі свого сервера (наприклад, від справжнього .env): тести мають використовувати лише тимчасові значення з початку файлу.