diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3129151 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: Continuous Integration + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +jobs: + test-reporting: + name: Test & Report + runs-on: ubuntu-latest + + env: + SPOTIFY_API_BASE_URL: ${{ vars.SPOTIFY_API_BASE_URL }} + SPOTIFY_CLIENT_ID: ${{ vars.SPOTIFY_CLIENT_ID }} + SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Cache Poetry + uses: actions/cache@v3 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Set up Java (Required for Allure CLI) + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - name: Install Allure CLI + run: | + set -eux + ALLURE_VERSION=2.28.0 + curl -Lo allure-commandline.zip \ + https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/${ALLURE_VERSION}/allure-commandline-${ALLURE_VERSION}.zip + unzip -o allure-commandline.zip -d allure-cli + echo "${PWD}/allure-cli/allure-${ALLURE_VERSION}/bin" >> $GITHUB_PATH + + - name: Setup Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install poetry + make install + + - name: Run tests and generate Allure report + run: make all + + - name: Upload Allure Report artifact + uses: actions/upload-artifact@v4 + with: + name: allure-report + path: ./allure-report diff --git a/.gitignore b/.gitignore index 93aa60f..db98dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ __pycache__/ .pytest_cache/ .env .vscode/settings.json + +# Ignore Allure results and reports +allure-results/ +allure-report/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..031f948 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +.PHONY: install lint test report serve-report clean all + +# Install project dependencies without installing the root package +install: + poetry install + +# Run code style and lint checks +lint: + black --check src tests + isort --check-only src tests + flake8 src tests + +# Run unit tests +test: + poetry run pytest + +# Generate Allure results and HTML report +report: + poetry run pytest --alluredir=allure-results + allure generate allure-results --clean -o allure-report + +# Serve the Allure report interactively +serve-report: + python scripts/allure_helper.py --serve + +# Clean test artifacts and reports +clean: + rm -rf .pytest_cache/ allure-results/ allure-report/ + +# Full flow: clean state, install, lint, test, and report +all: clean install test report \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index af0a933..51d84ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,37 @@ # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +[[package]] +name = "allure-pytest" +version = "2.14.2" +description = "Allure pytest integration" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "allure_pytest-2.14.2-py3-none-any.whl", hash = "sha256:18f3baa9ebd1b6148223cfa898bacfc2794bb9446221adac1be71deeb26ed79a"}, + {file = "allure_pytest-2.14.2.tar.gz", hash = "sha256:d387492178d27805863d95350bdc38b7feca3ed7165841997630fd4073cc9101"}, +] + +[package.dependencies] +allure-python-commons = "2.14.2" +pytest = ">=4.5.0" + +[[package]] +name = "allure-python-commons" +version = "2.14.2" +description = "Contains the API for end users as well as helper functions and classes to build Allure adapters for Python test frameworks" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "allure_python_commons-2.14.2-py3-none-any.whl", hash = "sha256:ad50385a4c601ec31c86eed773d8ccfdcc687fecbb6535c9768af3bf03b50a19"}, + {file = "allure_python_commons-2.14.2.tar.gz", hash = "sha256:7acdc4fe3efbe709604895e2393f082b2659d8e5653e77ff6367682e6e4a41bc"}, +] + +[package.dependencies] +attrs = ">=16.0.0" +pluggy = ">=0.4.0" + [[package]] name = "annotated-types" version = "0.7.0" @@ -33,6 +65,26 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + [[package]] name = "black" version = "25.1.0" @@ -837,4 +889,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" python-versions = ">=3.13" -content-hash = "7b6104e9a2cbfc25e82a61a598389b516483395d8b3d1882434e2a0219cffc22" +content-hash = "4b244f11d491da99d93e61023da8b16f01ba8524f203862fc421a495257db667" diff --git a/pyproject.toml b/pyproject.toml index fdfaa21..ba8bc10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ authors = [ license = {text = "MIT"} readme = "README.md" requires-python = ">=3.13" -package-mode = false dependencies = [ "httpx (>=0.28.1,<0.29.0)", "python-dotenv (>=1.1.0,<2.0.0)", @@ -16,7 +15,8 @@ dependencies = [ "pydantic-settings (>=2.9.1,<3.0.0)" ] - +[tool.poetry] +packages = [{ include="api_testing_framework", from="src" }] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" @@ -28,4 +28,5 @@ requests-mock = "^1.12.1" black = "^25.1.0" isort = "^6.0.1" flake8 = "^7.2.0" +allure-pytest = "^2.14.2" diff --git a/pytest.ini b/pytest.ini index 3bca685..7bb600f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] +addopts = --alluredir=allure-results markers = integration: mark test as an integration test (vs. unit) \ No newline at end of file diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/allure_helper.py b/scripts/allure_helper.py new file mode 100755 index 0000000..64baf8f --- /dev/null +++ b/scripts/allure_helper.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Script to clean, generate, and optionally serve an Allure report with parameterized paths""" +import argparse +import os +import shutil +import subprocess +import sys + +from api_testing_framework.logger import logger + + +def clean_allure_report(report_path: str) -> None: + """Clean the existing Allure report directory""" + if os.path.isdir(report_path): + logger.info("Cleaning old Allure report at: %s", report_path) + shutil.rmtree(report_path) + + +def generate_allure_report(results_dir: str, report_dir: str) -> None: + """Generate the Allure report from results""" + logger.info("Generating Allure report from %s to %s...", results_dir, report_dir) + try: + + subprocess.run( + ["allure", "generate", results_dir, "--clean", "-o", report_dir], + check=True, + capture_output=True, + ) + logger.info("Report generated at: %s/index.html", report_dir) + except subprocess.CalledProcessError as e: + stderr = e.stderr.decode().strip() if e.stderr else str(e) + logger.error("Allure generation failed: %s", stderr) + raise + + +def serve_allure_report(results_dir: str) -> None: + """Serve the Allure report interactively""" + logger.info("Serving Allure report from results: %s...", results_dir) + try: + subprocess.run( + ["allure", "serve", results_dir], check=True, capture_output=True + ) + except subprocess.CalledProcessError as e: + stderr = e.stderr.decode().strip() if e.stderr else str(e) + print("Allure serving failed: %s", stderr) + + +def main(): + project_root = os.path.dirname(os.path.abspath(__file__)) + parser = argparse.ArgumentParser( + description="Clean, generate, and serve Allure reports." + ) + parser.add_argument( + "--results-dir", + default=os.path.join(project_root, "allure-results"), + help="Directory containing pytest --alluredir output", + ) + parser.add_argument( + "--report-dir", + default=os.path.join(project_root, "allure-report"), + help="Directory where the HTML report will be generated", + ) + parser.add_argument( + "--serve", + action="store_true", + help="After generation, serve the report in a local web server", + ) + args = parser.parse_args() + + # Validate results dictionary + if not os.path.isdir(args.results_dir) or not os.listdir(args.results_dir): + logger.warning( + "No Allure results found in %s. Run pytest with --alluredir first.", + args.results_dir, + ) + sys.exit(0) + + # Clean, generate, and optionally serve + clean_allure_report(args.report_dir) + try: + generate_allure_report(args.results_dir, args.report_dir) + if args.serve: + serve_allure_report(args.results_dir) + except subprocess.CalledProcessError as e: + logger.error("Error during Allure operation: %s", str(e)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/api_testing_framework/logger.py b/src/api_testing_framework/logger.py new file mode 100644 index 0000000..66eab14 --- /dev/null +++ b/src/api_testing_framework/logger.py @@ -0,0 +1,12 @@ +# src/api_testing_framework/logger.py +import logging + +# Configure the root logger for the project +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) + +# Expose a pre-configured logger for convenience +logger = logging.getLogger("api_testing_framework") diff --git a/tests/spotify/test_integration_spotify.py b/tests/spotify/test_integration_spotify.py index 3444c98..f1e35f4 100644 --- a/tests/spotify/test_integration_spotify.py +++ b/tests/spotify/test_integration_spotify.py @@ -1,6 +1,6 @@ import pytest -from src.api_testing_framework.spotify.client import SpotifyClient +from api_testing_framework.spotify.client import SpotifyClient @pytest.mark.integration