diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10c7d1c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.13" + cache: pip + cache-dependency-path: ПетрухинОИ/requirements.txt + + - name: Install dependencies + working-directory: ./ПетрухинОИ + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install -e . + + - name: Build project + working-directory: ./ПетрухинОИ + run: python -m build + + - name: Run tests + working-directory: ./ПетрухинОИ + run: pytest -v \ No newline at end of file diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/.dockerignore" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/.dockerignore" new file mode 100644 index 0000000..403c603 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/.dockerignore" @@ -0,0 +1,6 @@ +.git +.venv +__pycache__ +.pytest_cache +dist +build diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/.gitignore" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/.gitignore" new file mode 100644 index 0000000..a7b6a31 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/.gitignore" @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +venv/ +build/ +dist/ +*.egg-info/ +.idea/ +.vscode/ +.DS_Store +.env diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/README.md" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/README.md" new file mode 100644 index 0000000..6b70561 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/README.md" @@ -0,0 +1,196 @@ +# Лабораторная работа №106 + +## Continuous Integration (CI) + +**Выполнил:** Олег Петрухин + +## Цель + +Настроить автоматическую проверку изменений при Pull Request в ветку `main`. + +## Проект + +Сделан небольшой Python-проект для анализа текста. Программа считает количество символов, слов и уникальных слов, а также определяет самые частые слова. + +Для тестирования используется `pytest`. + +## CI + +В качестве CI используется **GitHub Actions**. + +Workflow находится здесь: + +```text +.github/workflows/ci.yml +``` + +В репозитории workflow должен находиться именно в корневом `.github/workflows`, иначе GitHub Actions его не запустит. + +Pipeline запускается при Pull Request в `main` и выполняется по шагам: + +```text +Checkout +↓ +Python +↓ +Установка зависимостей +↓ +Build +↓ +Tests +``` + +В workflow используются `actions/checkout@v6` и `actions/setup-python@v7`. + +## Локальная проверка + +Нужны Python 3.11+ и Git. + +Проверка: + +```bash +python --version +git --version +``` + +Создание окружения: + +### Windows + +```powershell +python -m venv .venv +.venv\\Scripts\\activate +``` + +### Linux/macOS + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Установка зависимостей: + +```bash +pip install -r requirements.txt +``` + +Сборка: + +```bash +python -m build +``` + +Тесты: + +```bash +pytest -v +``` + +## Проверка CI + +После создания Pull Request открыть в GitHub: + +```text +Actions → CI +``` + +Для успешного запуска нужен зелёный статус. На скриншоте желательно показать название workflow, Pull Request и шаги установки зависимостей, сборки и тестов. + +### Успешный запуск + +Сначала оставить код без ошибок, отправить его в ветку PR и дождаться завершения workflow. + +**Сюда вставить реальный скриншот успешного запуска GitHub Actions.** + +## Проверка ошибки + +Чтобы показать работу CI при проблеме, временно изменить в `src/text_analyzer/analyzer.py`: + +```python +"words": len(words), +``` + +на: + +```python +"words": len(words) + 1, +``` + +После этого: + +```bash +git add ПетрухинОИ/ +git commit -m "Проверка падения тестов" +git push +``` + +Новый запуск Actions должен завершиться ошибкой на шаге `Run tests`. + +**Сюда вставить реальный скриншот failed pipeline.** + +После скриншота вернуть правильный код: + +```python +"words": len(words), +``` + +И выполнить: + +```bash +git add ПетрухинОИ/ +git commit -m "Исправление тестов" +git push +``` + +Финальная версия PR должна проходить тесты. + +## Git и Pull Request + +Если используется fork: + +```bash +git clone https://github.com/ВАШ_ЛОГИН/Practice106.git +cd Practice106 +git checkout -b practice106-petrukhin-oi +``` + +После копирования проекта: + +```bash +git add ПетрухинОИ/ +git add .github/workflows/ci.yml +git commit -m "Лабораторная работа №106" +git push -u origin practice106-petrukhin-oi +``` + +Pull Request создаётся из: + +```text +ВАШ_FORK/Practice106 +practice106-petrukhin-oi +``` + +в: + +```text +SoftwareEngineering2026/Practice106 +main +``` + +## Вывод + +В ходе работы был настроен CI-процесс на GitHub Actions. При Pull Request workflow устанавливает зависимости, собирает проект и запускает тесты. Отдельно проверяется сценарий с ошибкой в коде: тесты перестают проходить, и pipeline завершается с ошибкой. После исправления кода проверка снова проходит успешно. + +## Проверка требований + +| Требование | Реализация | +|---|---| +| GitHub Actions | `.github/workflows/ci.yml` | +| Pull Request → `main` | `on: pull_request` | +| Установка зависимостей | `pip install -r requirements.txt` | +| Сборка | `python -m build` | +| Тесты | `pytest -v` | +| Ошибочный pipeline | временная ошибка в коде | +| Каталог сдачи | `ПетрухинОИ/` | +| README | `ПетрухинОИ/README.md` | diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/pyproject.toml" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/pyproject.toml" new file mode 100644 index 0000000..22f5a20 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/pyproject.toml" @@ -0,0 +1,15 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "petrukhin-text-analyzer" +version = "1.0.0" +description = "Небольшой учебный проект для демонстрации Continuous Integration" +requires-python = ">=3.11" +authors = [ + {name = "Олег Петрухин"} +] + +[tool.setuptools.packages.find] +where = ["src"] diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/requirements.txt" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/requirements.txt" new file mode 100644 index 0000000..172bae6 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/requirements.txt" @@ -0,0 +1,2 @@ +pytest>=8,<9 +build>=1,<2 diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/all-ci-results.png" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/all-ci-results.png" new file mode 100644 index 0000000..c551fb9 Binary files /dev/null and "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/all-ci-results.png" differ diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/ci-failed.png" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/ci-failed.png" new file mode 100644 index 0000000..87e4efa Binary files /dev/null and "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/ci-failed.png" differ diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/ci-success.png" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/ci-success.png" new file mode 100644 index 0000000..a14eb27 Binary files /dev/null and "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/screenshots/ci-success.png" differ diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/src/text_analyzer/__init__.py" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/src/text_analyzer/__init__.py" new file mode 100644 index 0000000..3616b04 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/src/text_analyzer/__init__.py" @@ -0,0 +1,3 @@ +from .analyzer import analyze_text, normalize_text + +__all__ = ["analyze_text", "normalize_text"] diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/src/text_analyzer/analyzer.py" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/src/text_analyzer/analyzer.py" new file mode 100644 index 0000000..f25b70e --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/src/text_analyzer/analyzer.py" @@ -0,0 +1,25 @@ +import re +from collections import Counter + + +def normalize_text(text: str) -> str: + if not isinstance(text, str): + raise TypeError("text должен быть строкой") + return text.strip() + + +def analyze_text(text: str) -> dict: + text = normalize_text(text) + + if not text: + raise ValueError("text не должен быть пустым") + + words = re.findall(r"[A-Za-zА-Яа-яЁё0-9]+", text.lower()) + counter = Counter(words) + + return { + "characters": len(text), + "words": len(words), + "unique_words": len(counter), + "top_words": counter.most_common(5), + } diff --git "a/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/tests/test_analyzer.py" "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/tests/test_analyzer.py" new file mode 100644 index 0000000..c36dbd0 --- /dev/null +++ "b/\320\237\320\265\321\202\321\200\321\203\321\205\320\270\320\275\320\236\320\230/tests/test_analyzer.py" @@ -0,0 +1,33 @@ +import pytest + +from text_analyzer import analyze_text, normalize_text + + +def test_normalize_text(): + assert normalize_text(" hello ") == "hello" + + +def test_analyze_text_basic(): + result = analyze_text("Hello world hello") + + assert result["characters"] == 17 + assert result["words"] == 3 + assert result["unique_words"] == 2 + assert result["top_words"][0] == ("hello", 2) + + +def test_analyze_text_handles_punctuation(): + result = analyze_text("Hello, hello! Python.") + + assert result["words"] == 3 + assert result["unique_words"] == 2 + + +def test_analyze_text_empty(): + with pytest.raises(ValueError): + analyze_text("") + + +def test_normalize_text_wrong_type(): + with pytest.raises(TypeError): + normalize_text(123)