From a2702a68a740c5e50cd40434794598d8aead9fef Mon Sep 17 00:00:00 2001 From: sethorpe Date: Thu, 29 May 2025 09:41:34 -0700 Subject: [PATCH 1/6] Add test reporting, logging, Makefile, and CI integration --- .github/workflows/ci.yml | 60 +++++++++++++++++++ .gitignore | 4 ++ Makefile | 31 ++++++++++ generate_and_serve_allure_report.py | 45 +++++++++++++++ poetry.lock | 54 ++++++++++++++++- pyproject.toml | 1 + pytest.ini | 1 + scripts/__init__.py | 0 scripts/allure_helper.py | 90 +++++++++++++++++++++++++++++ src/api_testing_framework/logger.py | 12 ++++ 10 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Makefile create mode 100644 generate_and_serve_allure_report.py create mode 100644 scripts/__init__.py create mode 100755 scripts/allure_helper.py create mode 100644 src/api_testing_framework/logger.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..65b17a4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: Continuous Integration + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +jobs: + test-reporting: + name: Test & Report + runs-on: ubuntu-latest + + 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@v3 + 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..e6f82ef --- /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 --no-root + +# Run code style and lint checks +lint: + black --check src tests + isort --check-only src tests + flake8 src tests + +# Run unit tests +test: + pytest + +# Generate Allure results and HTML report +report: + 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/generate_and_serve_allure_report.py b/generate_and_serve_allure_report.py new file mode 100644 index 0000000..193675f --- /dev/null +++ b/generate_and_serve_allure_report.py @@ -0,0 +1,45 @@ +import os +import shutil +import subprocess + + +def clean_allure_report(allure_report_path: str): + """Clean the existing Allure report directory""" + if os.path.exists(allure_report_path): + print(f"Cleaning up old Allure report at: {allure_report_path}") + shutil.rmtree(allure_report_path) + + +def generate_and_serve_allure_report(): + """Generate and serve the Allure report.""" + project_root = os.path.dirname(os.path.abspath(__file__)) + allure_results = os.path.join(project_root, "allure-results") + allure_report = os.path.join(project_root, "allure-report") + + # Check if allure-results exists + if not os.path.exists(allure_results) or not os.listdir(allure_results): + print( + f"No Allure results found in {allure_results}. Run your tests with --alluredir first." + ) + return + + # Step 1: Clean the old Allure report + clean_allure_report(allure_report) + + # Step 2: Generate the Allure report + print("Generating Allure report...") + subprocess.run( + ["allure", "generate", allure_results, "--clean", "-o", allure_report], + check=True, + ) + + # Step 3: Serve the Allure report + print("Serving Allure report...") + subprocess.Popen(["allure", "serve", allure_results]) + + +if __name__ == "__main__": + try: + generate_and_serve_allure_report() + except Exception as e: + print(f"Error: {e}") 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..d0695c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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") From 661cf38de7cece990fa9a97f434b80f3a3dffe30 Mon Sep 17 00:00:00 2001 From: sethorpe Date: Thu, 29 May 2025 15:14:34 -0700 Subject: [PATCH 2/6] Remove unnecessary script --- generate_and_serve_allure_report.py | 45 ----------------------------- 1 file changed, 45 deletions(-) delete mode 100644 generate_and_serve_allure_report.py diff --git a/generate_and_serve_allure_report.py b/generate_and_serve_allure_report.py deleted file mode 100644 index 193675f..0000000 --- a/generate_and_serve_allure_report.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import shutil -import subprocess - - -def clean_allure_report(allure_report_path: str): - """Clean the existing Allure report directory""" - if os.path.exists(allure_report_path): - print(f"Cleaning up old Allure report at: {allure_report_path}") - shutil.rmtree(allure_report_path) - - -def generate_and_serve_allure_report(): - """Generate and serve the Allure report.""" - project_root = os.path.dirname(os.path.abspath(__file__)) - allure_results = os.path.join(project_root, "allure-results") - allure_report = os.path.join(project_root, "allure-report") - - # Check if allure-results exists - if not os.path.exists(allure_results) or not os.listdir(allure_results): - print( - f"No Allure results found in {allure_results}. Run your tests with --alluredir first." - ) - return - - # Step 1: Clean the old Allure report - clean_allure_report(allure_report) - - # Step 2: Generate the Allure report - print("Generating Allure report...") - subprocess.run( - ["allure", "generate", allure_results, "--clean", "-o", allure_report], - check=True, - ) - - # Step 3: Serve the Allure report - print("Serving Allure report...") - subprocess.Popen(["allure", "serve", allure_results]) - - -if __name__ == "__main__": - try: - generate_and_serve_allure_report() - except Exception as e: - print(f"Error: {e}") From 7f32f21ccad012146f193655d2074e8f6d0966cb Mon Sep 17 00:00:00 2001 From: sethorpe Date: Thu, 29 May 2025 15:25:37 -0700 Subject: [PATCH 3/6] Update artifact actions to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65b17a4..5e06f90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: run: make all - name: Upload Allure Report artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: allure-report path: ./allure-report From ca04ebd6c5e1a613b479a716e1b7e9f0cee492f1 Mon Sep 17 00:00:00 2001 From: sethorpe Date: Thu, 29 May 2025 15:33:07 -0700 Subject: [PATCH 4/6] Update Makefile commands to use --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e6f82ef..bdca1d3 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,11 @@ lint: # Run unit tests test: - pytest + poetry run pytest # Generate Allure results and HTML report report: - pytest --alluredir=allure-results + poetry run pytest --alluredir=allure-results allure generate allure-results --clean -o allure-report # Serve the Allure report interactively From 4e227c2bcfe68d66afe052000bc41ac62a930b2a Mon Sep 17 00:00:00 2001 From: sethorpe Date: Thu, 29 May 2025 15:49:16 -0700 Subject: [PATCH 5/6] Fix Makefile, pyproject.toml, and import stmt --- Makefile | 2 +- pyproject.toml | 4 ++-- tests/spotify/test_integration_spotify.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index bdca1d3..031f948 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Install project dependencies without installing the root package install: - poetry install --no-root + poetry install # Run code style and lint checks lint: diff --git a/pyproject.toml b/pyproject.toml index d0695c9..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" 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 From 67979924fe47dcf008b4112c0ea89bee4751cd6f Mon Sep 17 00:00:00 2001 From: sethorpe Date: Fri, 30 May 2025 14:37:21 -0700 Subject: [PATCH 6/6] Update to include environment variables and secret --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e06f90..3129151 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,10 @@ jobs: 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