diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1777c06 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,121 @@ +name: Tests + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Least-privilege default for the whole workflow. +permissions: + contents: read + +env: + PYTHON_VERSION: '3.13' + ANSIBLE_VERSION: '11.*' + +jobs: + tests: + name: Unit Tests + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install dependencies + run: | + set -euo pipefail + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + pip install "ansible==${{ env.ANSIBLE_VERSION }}" + + - name: Run unit tests + run: | + set -euo pipefail + pytest tests/unit/ -v --tb=short + + - name: Generate coverage report + run: | + set -euo pipefail + pytest tests/unit/ --cov=plugins --cov-report=xml --cov-report=term-missing + + lint: + name: Linting + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install dependencies + run: | + set -euo pipefail + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + pip install "ansible==${{ env.ANSIBLE_VERSION }}" + + - name: Lint with flake8 + run: | + set -euo pipefail + flake8 plugins/ tests/ --max-line-length=120 --exclude=__pycache__ + + - name: Lint with ansible-lint + run: | + set -euo pipefail + ansible-lint plugins/ + + - name: Type check with mypy + run: | + set -euo pipefail + mypy plugins/ --ignore-missing-imports + + test-integration: + name: Integration Tests + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: pip + + - name: Install dependencies + run: | + set -euo pipefail + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + pip install "ansible==${{ env.ANSIBLE_VERSION }}" + + - name: Run integration tests + run: | + set -euo pipefail + pytest tests/integration/ -v --tb=short diff --git a/.gitignore b/.gitignore index c99b206..4ae04ae 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,19 @@ inventory/* dynamic_inventory.log .vscode +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Testing +.pytest_cache +test-results.xml +.coverage +htmlcov/ +.tox/ + # Created by https://www.toptal.com/developers/gitignore/api/ansible,python # Edit at https://www.toptal.com/developers/gitignore?templates=ansible,python diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..b686226 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,480 @@ +# Development & Setup Guide + +This guide explains how to set up the development environment and work with the ansible-inventory-github collection. + +## Prerequisites + +- **Python 3.9+** (Supported: 3.9, 3.10, 3.11, 3.12) +- **pip** (Python package manager) +- **Ansible 2.10+** (Tested with 2.10 up to 11.x) +- **tox** (optional, for testing multiple Python/Ansible versions) +- **Git** + +## Quick Start + +### 1. Clone the Repository + +```bash +git clone https://github.com/saltyblu/ansible-inventory-github.git +cd ansible-inventory-github +``` + +### 2. Create a Virtual Environment (Recommended) + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### 3. Install Project with Dependencies + +```bash +# Install runtime and dev dependencies +pip install -r requirements.txt -r requirements-dev.txt + +# Or use the Make target +make install-dev +``` + +## Development Workflow + +### Running Tests + +```bash +# Run unit tests +make test + +# Run all tests with verbose output +make test-verbose + +# Run specific test suite +make test-unit +make test-integration + +# Run with coverage (generates HTML report) +make test-coverage + +# Run tests on all Python/Ansible versions with tox +make test-all +``` + +### Code Quality & Linting + +```bash +# Check code quality (flake8, ansible-lint) +make lint + +# Run type checking (mypy) +make type +``` + +### Managing Dependencies + +Dependencies are defined in standard `requirements.txt` files: + +```bash +# Install/update dependencies +pip install -r requirements.txt -r requirements-dev.txt + +# Upgrade all packages +pip install --upgrade -r requirements.txt -r requirements-dev.txt + +# Add a new runtime dependency +pip install package-name +pip freeze | grep package-name >> requirements.txt + +# Add a new dev dependency +pip install package-name +pip freeze | grep package-name >> requirements-dev.txt +``` + +### Python Version Management + +The project is compatible with Python 3.9+. If you need to switch Python versions: + +```bash +# Check available Python versions +python3 --version + +# Create venv with specific Python version +python3.11 -m venv venv +source venv/bin/activate +``` + +## Project Structure + +``` +ansible-inventory-github/ +├── plugins/ +│ └── inventory/ +│ └── github_repositories_inventory.py # Main plugin +├── tests/ +│ ├── unit/ +│ │ └── plugins/inventory/ +│ │ └── test_github_repositories_inventory.py +│ ├── integration/ +│ │ └── inventory/ +│ │ └── github_repositories.yml +│ ├── conftest.py +│ └── README.md +├── galaxy.yml # Ansible Collection metadata +├── pyproject.toml # Minimal project config (pytest only) +├── tox.ini # Multi-version testing +├── requirements.txt # Runtime dependencies +├── requirements-dev.txt # Dev/test dependencies +├── Makefile # Development targets +└── README.md +``` + +## Key Components + +### 1. GitHubRepositoryFetcher + +Handles GitHub API interactions: + +```python +from plugins.inventory.github_repositories_inventory import GitHubRepositoryFetcher + +fetcher = GitHubRepositoryFetcher('token') +repos = fetcher.fetch_repositories('filter', 'org') +``` + +### 2. InventoryModule + +Main Ansible inventory plugin. Supports dependency injection for testing: + +```python +from plugins.inventory.github_repositories_inventory import InventoryModule + +# Production usage +inventory = InventoryModule() + +# Testing with mocks +inventory = InventoryModule(logger=mock_logger, fetcher=mock_fetcher) +``` + +## Configuration Files + +### galaxy.yml + +Ansible Collection metadata - defines collection name, version, and dependencies. + +### pyproject.toml + +Minimal configuration for pytest (Ansible Collections standard). + +### tox.ini + +Automates testing across multiple Python and Ansible versions: + +```bash +# Run tests on all versions +tox + +# Run specific environment +tox -e py311-ansible-11 + +# Run linting +tox -e lint +``` + +### Makefile + +Convenient development targets: + +```bash +make help # Show all available targets +make test # Run tests +make lint # Check code quality +make type # Type checking +make test-all # Test with tox +``` + +## Ansible Best Practices Compliance + +This project follows Ansible Collection best practices: + +1. **Collection Structure**: Per [Ansible Collection docs](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html) +2. **Dependency Management**: Standard `requirements.txt` files +3. **Testing**: Comprehensive unit and integration tests +4. **Documentation**: Docstrings, README, and guides +5. **Type Hints**: Type annotations where appropriate +6. **Version Compatibility**: Python 3.9+, Ansible 2.10+ + +## Contributing + +1. Create a feature branch: `git checkout -b feature/xyz` +2. Make changes and install dev deps: `make install-dev` +3. Run tests: `make test` +4. Check code quality: `make lint` +5. Run across versions: `make test-all` +6. Commit with descriptive message: `git commit -am 'Add feature xyz'` +7. Push and create a pull request + +## Common Tasks + +### Run a Single Test + +```bash +pytest tests/unit/plugins/inventory/test_github_repositories_inventory.py::TestGitHubRepositoryFetcher::test_initialization +``` + +### Run Tests Matching a Pattern + +```bash +pytest -k "test_fetch_repositories" +``` + +### Generate Coverage Report + +```bash +make test-coverage +# Open htmlcov/index.html to view report +``` + +### Test with Specific Python Version + +```bash +# Using tox +tox -e py310 + +# Or manually with venv +python3.10 -m venv venv-py310 +source venv-py310/bin/activate +pip install -r requirements.txt -r requirements-dev.txt +pytest +``` + +### Test with Specific Ansible Version + +```bash +# Using tox +tox -e ansible-11 + +# Or manually +pip install 'ansible>=11,<12' +pytest +``` + +### Create a Distribution Package + +```bash +# Build the Ansible Collection +ansible-galaxy collection build +``` + +## Troubleshooting + +### Virtual Environment Issues + +```bash +# Remove and recreate venv +rm -rf venv/ +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt -r requirements-dev.txt +``` + +### Python Version Not Found + +```bash +# List available Python versions +python3 --version +which python3.11 + +# Install specific Python version (macOS with Homebrew) +brew install python@3.11 +``` + +### Pytest Import Errors + +```bash +# Reinstall with clean environment +pip uninstall -y ansible PyGithub pytest +pip install -r requirements.txt -r requirements-dev.txt +``` + +### Tox Configuration Issues + +```bash +# Clear tox cache +rm -rf .tox/ + +# Run with verbose output +tox -vv +``` + +## Resources + +- [Ansible Collections Documentation](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html) +- [Ansible Plugin Development](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html) +- [Pytest Documentation](https://docs.pytest.org/) +- [Tox Documentation](https://tox.readthedocs.io/) + + +## Project Structure + +``` +ansible-inventory-github/ +├── plugins/ +│ └── inventory/ +│ └── github_repositories_inventory.py # Main plugin +├── tests/ +│ ├── unit/ +│ │ └── plugins/inventory/ +│ │ └── test_github_repositories_inventory.py +│ ├── integration/ +│ │ └── inventory/ +│ │ └── github_repositories.yml +│ ├── conftest.py +│ └── README.md +├── gallery.yml # Ansible Collection metadata +├── pyproject.toml # Poetry configuration +├── Makefile # Development targets +└── README.md +``` + +## Key Components + +### 1. GitHubRepositoryFetcher + +Handles GitHub API interactions: + +```python +from plugins.inventory.github_repositories_inventory import GitHubRepositoryFetcher + +fetcher = GitHubRepositoryFetcher('token') +repos = fetcher.fetch_repositories('filter', 'org') +``` + +### 2. InventoryModule + +Main Ansible inventory plugin. Supports dependency injection for testing: + +```python +from plugins.inventory.github_repositories_inventory import InventoryModule + +# Production usage +inventory = InventoryModule() + +# Testing with mocks +inventory = InventoryModule(logger=mock_logger, fetcher=mock_fetcher) +``` + +## Configuration Files + +### pyproject.toml + +Contains all project metadata and dependencies: + +- **[tool.poetry]**: Project definition +- **[tool.poetry.dependencies]**: Runtime dependencies +- **[tool.poetry.group.dev.dependencies]**: Development dependencies +- **[tool.pytest.ini_options]**: Pytest configuration +- **[tool.ruff]**: Ruff linter configuration +- **[tool.black]**: Black formatter configuration +- **[tool.mypy]**: Type checker configuration + +### Makefile + +Convenient development targets: + +```bash +make help # Show all available targets +make test # Run tests +make lint # Check code quality +make format # Format code +``` + +## Ansible Best Practices Compliance + +This project follows Ansible best practices: + +1. **Collection Structure**: Organized as Per Ansible documentation +2. **Dependency Management**: Uses Python industry standard (Poetry) +3. **Testing**: Comprehensive unit and integration tests +4. **Documentation**: Docstrings, README, and guides +5. **Type Hints**: Type annotations where appropriate +6. **Code Quality**: Automated linting and formatting + +## Contributing + +1. Create a feature branch: `git checkout -b feature/xyz` +2. Make changes and run tests: `make test` +3. Check code quality: `make lint` +4. Format code: `make format` +5. Commit with descriptive message: `git commit -am 'Add feature xyz'` +6. Push and create a pull request + +## Common Tasks + +### Run a Single Test + +```bash +poetry run pytest tests/unit/plugins/inventory/test_github_repositories_inventory.py::TestGitHubRepositoryFetcher::test_initialization +``` + +### Run Tests Matching a Pattern + +```bash +poetry run pytest -k "test_fetch_repositories" +``` + +### Generate Coverage Report + +```bash +make test-coverage +# Open htmlcov/index.html to view report +``` + +### Update Dependencies to Latest + +```bash +# Update poetry.lock while respecting version constraints +poetry update + +# Show outdated packages +poetry show --outdated +``` + +### Create a Distribution Package + +```bash +# Build the Ansible Collection +ansible-galaxy collection build +``` + +## Troubleshooting + +### Poetry Lock Issues + +If you encounter lock file conflicts: + +```bash +# Remove and regenerate lock file +rm poetry.lock +poetry install --with dev +``` + +### Python Version Mismatch + +```bash +# Set Poetry to use your Python 3.11+ +poetry env use python3.11 +poetry install --with dev +``` + +### Import Errors + +```bash +# Reinstall with clean environment +poetry cache clear . --all +poetry install --with dev --no-cache +``` + +## Resources + +- [Ansible Collections Documentation](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) +- [Ansible Plugin Development](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html) +- [Poetry Documentation](https://python-poetry.org/docs/) +- [Pytest Documentation](https://docs.pytest.org/) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c1c813 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +.PHONY: help install install-dev test test-unit test-integration test-coverage test-verbose lint type clean + +help: + @echo "Development targets for ansible-inventory-github" + @echo "" + @echo "Installation:" + @echo " make install - Install runtime dependencies" + @echo " make install-dev - Install with dev/test dependencies" + @echo "" + @echo "Testing:" + @echo " make test - Run unit tests" + @echo " make test-unit - Run unit tests only" + @echo " make test-integration- Run integration tests only" + @echo " make test-coverage - Run tests with coverage report" + @echo " make test-verbose - Run tests with verbose output" + @echo " make test-all - Run tests on all Python/Ansible versions (tox)" + @echo "" + @echo "Code Quality:" + @echo " make lint - Run linting (flake8, ansible-lint)" + @echo " make type - Run type checking (mypy)" + @echo "" + @echo "Cleanup:" + @echo " make clean - Remove test artifacts and cache" + @echo "" + +# Installation +install: + pip install -r requirements.txt + +install-dev: + pip install -r requirements.txt -r requirements-dev.txt + +# Testing +test: ## Run unit tests with pytest + pytest tests/unit/ + +test-unit: ## Run unit tests only + pytest tests/unit/ -v + +test-integration: ## Run integration tests only + pytest tests/integration/ -v + +test-coverage: ## Run tests with coverage report + pytest --cov=plugins --cov-report=html --cov-report=term-missing tests/unit/ + +test-verbose: ## Run tests with verbose output + pytest -vv tests/ + +test-all: ## Run tests on all Python/Ansible versions with tox + tox + +# Code Quality +lint: ## Run linters (flake8, ansible-lint) + flake8 plugins/ tests/ --max-line-length=120 --exclude=__pycache__ + ansible-lint plugins/ || true + +type: ## Run type checking (mypy) + mypy plugins/ --ignore-missing-imports + +# Cleanup +clean: ## Remove test artifacts and cache + rm -rf .pytest_cache + rm -rf .coverage + rm -rf htmlcov + rm -rf .mypy_cache + rm -rf .tox + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + diff --git a/plugins/inventory/github_repositories_inventory.py b/plugins/inventory/github_repositories_inventory.py index 657f7de..735e585 100755 --- a/plugins/inventory/github_repositories_inventory.py +++ b/plugins/inventory/github_repositories_inventory.py @@ -70,22 +70,122 @@ repository_filter: *-deployment ''' + +def get_logger(name, logger=None): + """Get or create a logger instance. + + Args: + name: Logger name + logger: Optional pre-configured logger instance + + Returns: + logger instance + """ + if logger is not None: + return logger + return logging.getLogger(name) + + +class GitHubRepositoryFetcher: + """Fetches repositories from GitHub API.""" + + def __init__(self, access_token, logger=None, per_page=100): + """Initialize the GitHub client. + + Args: + access_token: GitHub API token + logger: Optional logger instance + """ + self.access_token = access_token + self.logger = get_logger('GitHubRepositoryFetcher', logger) + self._github_client = None + self.per_page = per_page + + @property + def github_client(self): + """Lazy-load GitHub client.""" + if self._github_client is None: + self._github_client = Github(self.access_token, per_page=self.per_page) + return self._github_client + + def set_github_client(self, client): + """Set a custom GitHub client (useful for testing).""" + self._github_client = client + + def fetch_repositories( + self, + repository_filter, + org, + archived=False, + group_by_languages=False + ): + """Fetch repositories from GitHub. + + Args: + repository_filter: Search filter string + org: GitHub organization + archived: Include archived repositories + group_by_languages: Include language information + + Returns: + List of repository data dictionaries + """ + repos = [] + try: + search_result = self.github_client.search_repositories( + query=repository_filter, + owner=org, + sort="updated", + archived=archived + ) + except Exception as e: + self.logger.error(f'Exception while searching repositories: {e}') + raise + + try: + for count, repository in enumerate(search_result, 1): + repo_raw_data = dict(getattr(repository, '_rawData', {})) + self.logger.debug(f"Group by Language is: {group_by_languages}") + + topics = repo_raw_data.get('topics') + if topics is None: + try: + topics = repository.get_topics() + except Exception: + topics = [] + repo_raw_data['topics'] = topics + + if group_by_languages: + repo_raw_data['languages'] = repository.get_languages() + else: + repo_raw_data['languages'] = None + + self.logger.debug(f'Counter: {count} - {repository.name}') + repos.append(repo_raw_data) + + return repos + except Exception as e: + self.logger.error(f'Exception while iterating repositories: {e}') + raise + + class InventoryModule(BaseInventoryPlugin, Cacheable): ''' Host inventory parser for ansible using GitHub as source. ''' NAME = 'github_repositories_inventory' - def __init__(self): + def __init__(self, logger=None, fetcher=None): + """Initialize the inventory plugin. + + Args: + logger: Optional logger instance for testing + fetcher: Optional GitHubRepositoryFetcher instance for testing + """ super(InventoryModule, self).__init__() self.cache_key = None self.connection = None - logging.basicConfig(filename="dynamic_inventory.log", - filemode='a', - format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', - datefmt='%H:%M:%S', - level=logging.INFO) - - self.logger = logging.getLogger('DynamicInventory') + self.logger = get_logger('DynamicInventory', logger) + self.fetcher = fetcher def verify_file(self, path): valid = False @@ -141,6 +241,8 @@ def parse(self, inventory, loader, path, cache=True): self.archived = bool(self.get_option('show_archived_repos')) self.group_by_languages = bool(self.get_option('group_by_languages')) + results = None + if attempt_to_read_cache: self.logger.debug("Attempting to read cache") try: @@ -162,34 +264,28 @@ def parse(self, inventory, loader, path, cache=True): except Exception as e: self.logger.error(f'Exception on Cache Update: {e}') - self.populate(results) + self.populate(results or []) def get_repositories(self): - count = 1 - g = Github(self.access_token) - repos = [] + """Fetch repositories using GitHubRepositoryFetcher. + + Returns: + List of repository data dictionaries + """ + if self.fetcher is None: + self.fetcher = GitHubRepositoryFetcher(self.access_token, self.logger) + try: - r = g.search_repositories(query=self.repository_filter, owner=self.org, sort="updated", archived=self.archived) - except Exception as e: - self.logger.error(f'Caught an Exception while searching: {e}') - print( - f"Error: {e}", + return self.fetcher.fetch_repositories( + self.repository_filter, + self.org, + archived=self.archived, + group_by_languages=self.group_by_languages ) - return - try: - for repository in r: - repo_raw_data = repository._rawData - self.logger.debug(f"Group by Language is: {self.group_by_languages}") - if self.group_by_languages: - repo_raw_data['languages'] = repository.get_languages() - else: - repo_raw_data['languages'] = None - self.logger.debug(f'Counter: {count} - {repository.name}') - repos.append(repo_raw_data) - count += 1 - return repos except Exception as e: - self.logger.error(f'Caught an Exception while iterating Repositories: {e}') + self.logger.error(f'Error fetching repositories: {e}') + print(f"Error: {e}") + return None def populate(self, r): try: @@ -198,7 +294,7 @@ def populate(self, r): for project in r: groupnames = [] - topics = project['topics'] + topics = project.get('topics', []) team = next((topic for topic in topics if topic.startswith("team-")), None) if team is not None: groupnames.append(team) @@ -216,7 +312,7 @@ def populate(self, r): groupnames.append("unassigned") self.logger.debug(f'Name: {project["name"]}') if self.group_by_languages: - for key, value in project['languages'].items(): + for key, value in (project.get('languages') or {}).items(): group = self.inventory.add_group(to_safe_group_name(f'{key.lower().replace(" ", "")}', force=True, silent=True)) hostname = self.inventory.add_host(str(project['name']), group) for groupentry in groupnames: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f7527f1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +# Minimal pyproject.toml for pytest configuration +# This Ansible Collection uses requirements.txt for dependency management +# See: https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html + +[build-system] +requires = ["setuptools>=40.8.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ansible-inventory-github" +version = "1.0.0" +description = "Ansible inventory plugin to manage GitHub repositories as inventory sources" +readme = "README.md" +license = {text = "GPL-3.0-or-later"} +authors = [ + {name = "Volker Schmitz", email = "saltyblu@github.com"}, + {name = "Martin Soentgenrath", email = "merin80@github.com"}, +] +requires-python = ">=3.9" +keywords = ["ansible", "github", "inventory", "plugin"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: No Input/Output (Daemon)", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: System :: Systems Administration", +] + +[project.urls] +Repository = "https://github.com/saltyblu/ansible-inventory-github.git" +Homepage = "https://github.com/saltyblu/ansible-inventory-github" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +python_classes = "Test*" +python_functions = "test_*" +addopts = "-v --tb=short --strict-markers" +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] +filterwarnings = [ + "ignore::DeprecationWarning", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ebf94d3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +# Pytest configuration is now in pyproject.toml +# This file is kept for compatibility with some tools +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..8758465 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +# Development and testing dependencies for ansible-inventory-github collection +# This is an alias for requirements-test.txt +# Install with: pip install -r requirements.txt -r requirements-dev.txt + +-r requirements-test.txt diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..686cdad --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,22 @@ +# Development and testing dependencies for ansible-inventory-github collection +# See: https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html +# +# Install with: pip install -r requirements.txt -r requirements-test.txt +# Or with tox: tox + +-r requirements.txt + +# Testing +pytest>=7.0 +pytest-cov>=3.0 +pytest-mock>=3.6 + +# Code Quality +ansible-lint>=6.0 +pylint>=2.15 +flake8>=4.0 + +# Type Checking +mypy>=1.0 +types-PyYAML + diff --git a/requirements.txt b/requirements.txt index 19aee71..69c822c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,8 @@ -ansibleg -ansible-compatg -ansible-coreg -ansible-lintg -attrsg -blackg -bracexg -certifig -cffig -charset-normalizerg -clickg -cryptographyg -Deprecatedg -filelockg -idnag -Jinja2g -jmespathg -jsonschemag -jsonschema-specificationsg -markdown-it-pyg -MarkupSafeg -mdurlg -mypy-extensionsg -packagingg -pathspecg -platformdirsg -pycparserg -PyGithubg -Pygmentsg -PyJWTg -PyNaClg -python-dateutilg -PyYAMLg -referencingg -requestsg -resolvelibg -richg -rpds-pyg -ruamel.yamlg -ruamel.yaml.clibg -sixg -subprocess-teeg -urllib3g -wcmatchg -wraptg -yamllintg +# Runtime dependencies for ansible-inventory-github collection +# See: https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html +# +# Install with: pip install -r requirements.txt + +ansible>=2.10,<3 +PyGithub>=1.55 + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..8fc78c2 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,308 @@ +# Testing Guide for ansible-inventory-github + +This guide explains how to test the ansible-inventory-github collection's GitHub repositories inventory plugin. + +Following Ansible Collection best practices: https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html + +## Setup + +### Install Test Dependencies + +This project uses standard Python package management with `pip`: + +```bash +# Create a virtual environment (recommended) +python3 -m venv venv +source venv/bin/activate + +# Install dependencies +pip install -r requirements.txt -r requirements-dev.txt + +# Or use the Make target +make install-dev +``` + +### Project Structure + +``` +tests/ +├── unit/ +│ └── plugins/inventory/ +│ └── test_github_repositories_inventory.py +├── integration/ +│ └── inventory/ +│ └── github_repositories.yml +├── conftest.py # Pytest configuration and fixtures +└── README.md # This file +``` + +## Running Tests + +All test commands use standard `pytest`. Use `make` targets for convenience. + +### Run All Tests + +```bash +make test +# or +pytest tests/unit/ +``` + +### Run Only Unit Tests + +```bash +make test-unit +# or +pytest tests/unit/ -v +``` + +### Run Integration Tests + +```bash +make test-integration +# or +pytest tests/integration/ -v +``` + +For live GitHub API integration tests, set environment variables first: + +```bash +export GITHUB_TOKEN= +export GITHUB_TEST_ORG=saltyblu +export GITHUB_TEST_FILTER='*-deployment' +pytest tests/integration/test_github_repositories_integration.py -v +``` + +### Run with Coverage Report + +```bash +make test-coverage +# or +pytest --cov=plugins --cov-report=html --cov-report=term-missing tests/unit/ +``` + +### Run Specific Test + +```bash +pytest tests/unit/plugins/inventory/test_github_repositories_inventory.py::TestGitHubRepositoryFetcher::test_initialization +``` + +### Run Tests Matching a Pattern + +```bash +pytest -k "test_fetch_repositories" +``` + +### Run with Verbose Output + +```bash +make test-verbose +# or +pytest -vv tests/ +``` + +## Multi-Version Testing with Tox + +Test across multiple Python and Ansible versions: + +```bash +# Test all matrix combinations +make test-all +# or +tox + +# Test specific Python version +tox -e py311 + +# Test specific Ansible version +tox -e ansible-11 + +# List all environments +tox -l +``` + +## Code Quality & Linting + +```bash +# Run linters (flake8, ansible-lint) +make lint + +# Run type checking (mypy) +make type +``` + +## Test Organization + +### Unit Tests (`tests/unit/`) + +Unit tests focus on testing individual components in isolation: + +- **`TestGetLogger`**: Tests for the `get_logger` helper function +- **`TestGitHubRepositoryFetcher`**: Tests for the `GitHubRepositoryFetcher` class + - Initialization with and without custom logger + - Setting custom GitHub client (for mocking) + - Fetching repositories with various options + - Error handling +- **`TestInventoryModuleParseMethods`**: Tests for inventory parsing logic + - Regex-based grouping + - Error handling +- **`TestInventoryModuleIntegration`**: Integration tests with mocked components + +### Integration Tests (`tests/integration/`) + +Integration tests test the full inventory plugin workflow (to be added): + +- Full inventory parsing with YAML configuration +- GitHub API integration (with mocked responses) +- Inventory group and host management + +## Mocking Strategy + +The refactored code makes mocking easier through **Dependency Injection**: + +```python +# Create mocks +mock_logger = Mock(spec=logging.Logger) +mock_fetcher = Mock(spec=GitHubRepositoryFetcher) + +# Inject them +inventory = InventoryModule(logger=mock_logger, fetcher=mock_fetcher) +``` + +### Key Mocking Points + +1. **GitHub Client**: Use `fetcher.set_github_client(mock_client)` to mock GitHub API calls +2. **Logger**: Pass a `Mock` logger to avoid logging overhead in tests +3. **Fetcher**: Inject a mocked `GitHubRepositoryFetcher` to test inventory parsing logic independently + +## Example Test + +```python +def test_fetch_repositories_returns_list(): + """Test that fetch_repositories returns a list of repositories.""" + fetcher = GitHubRepositoryFetcher('test_token', Mock()) + + # Mock GitHub client + mock_github = Mock() + mock_repo = Mock() + mock_repo._rawData = {'name': 'repo1', 'id': 1} + mock_search_result = [mock_repo] + mock_github.search_repositories.return_value = mock_search_result + + fetcher.set_github_client(mock_github) + + result = fetcher.fetch_repositories( + repository_filter='*', + org='test-org' + ) + + assert len(result) == 1 +``` + +## Writing New Tests + +When adding new tests: + +1. **Use descriptive test names** that explain what is being tested +2. **Separate setup, action, and assertion** phases +3. **Use docstrings** to explain the test's purpose +4. **Mock external dependencies** (GitHub API, file I/O, etc.) +5. **Test edge cases** (empty results, errors, invalid input) + +### Test Template + +```python +def test_feature_description(self): + """Test that [feature] does [expected behavior].""" + # Setup + fetcher = GitHubRepositoryFetcher('token', Mock()) + + # Action + result = fetcher.some_method() + + # Assertion + assert result is not None +``` + +## Troubleshooting + +### Virtual Environment Not Activated + +Make sure your virtual environment is activated: + +```bash +# Create venv if needed +python3 -m venv venv + +# Activate venv +source venv/bin/activate # Linux/macOS +# or +venv\Scripts\activate # Windows +``` + +### Import Errors for `ansible` or `github` + +Ensure dependencies are installed: + +```bash +# Reinstall dependencies +pip install -r requirements.txt -r requirements-dev.txt +``` + +### Pytest Not Found + +```bash +pip install pytest pytest-cov pytest-mock +``` + +### Python Version Mismatch + +The project requires Python 3.9+. Check your version: + +```bash +python3 --version + +# If needed, create venv with specific Python version +python3.11 -m venv venv +source venv/bin/activate +pip install -r requirements.txt -r requirements-dev.txt +``` + +### Mock Not Matching Spec + +Ensure the mock spec matches the actual class: + +```python +from unittest.mock import Mock +from plugins.inventory.github_repositories_inventory import GitHubRepositoryFetcher + +mock_fetcher = Mock(spec=GitHubRepositoryFetcher) +``` + +## Continuous Integration + +CI/CD can run tests using: + +```bash +# Run tests with coverage +pytest --cov=plugins --cov-report=xml --junitxml=test-results.xml + +# Run on all versions with tox +tox + +# Run specific Ansible version +tox -e ansible-11 +``` + +This generates: +- Coverage reports (in `coverage.xml`) +- Test results (in `test-results.xml`) + +## Resources + +- [Ansible Collections Documentation](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html) +- [Pytest Documentation](https://docs.pytest.org/) +- [unittest.mock Documentation](https://docs.python.org/3/library/unittest.mock.html) +- [Tox Documentation](https://tox.readthedocs.io/) + + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..ee3f60d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b9511a0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +""" +Pytest configuration for ansible-inventory-github tests. +""" + +import sys +import os +import types +from unittest.mock import MagicMock + +# Add the project root to the path so we can import the inventory plugin +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.insert(0, project_root) + +# Mock Ansible modules if not available +try: + from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable # noqa: F401 + from ansible.inventory.group import to_safe_group_name # noqa: F401 +except ModuleNotFoundError: + ansible_module = types.ModuleType('ansible') + ansible_plugins_module = types.ModuleType('ansible.plugins') + ansible_plugins_inventory_module = types.ModuleType('ansible.plugins.inventory') + ansible_inventory_module = types.ModuleType('ansible.inventory') + ansible_inventory_group_module = types.ModuleType('ansible.inventory.group') + + class BaseInventoryPlugin: + pass + + class Cacheable: + pass + + def to_safe_group_name(name, force=False, silent=False): + return name + + ansible_plugins_inventory_module.BaseInventoryPlugin = BaseInventoryPlugin + ansible_plugins_inventory_module.Cacheable = Cacheable + ansible_inventory_group_module.to_safe_group_name = to_safe_group_name + + sys.modules['ansible'] = ansible_module + sys.modules['ansible.plugins'] = ansible_plugins_module + sys.modules['ansible.plugins.inventory'] = ansible_plugins_inventory_module + sys.modules['ansible.inventory'] = ansible_inventory_module + sys.modules['ansible.inventory.group'] = ansible_inventory_group_module + +# Mock github module only if PyGithub is not installed +try: + from github import Github # noqa: F401 +except ModuleNotFoundError: + github_module = MagicMock() + github_module.Github = MagicMock() + sys.modules['github'] = github_module diff --git a/tests/integration/inventory/github_repositories.yml b/tests/integration/inventory/github_repositories.yml new file mode 100644 index 0000000..e38097b --- /dev/null +++ b/tests/integration/inventory/github_repositories.yml @@ -0,0 +1,14 @@ +# Example inventory configuration for testing +# This can be used to test the inventory plugin with mock GitHub repositories + +plugin: github_repositories_inventory +access_token: !vault | + $ANSIBLE_VAULT;1.1;AES256 + # Note: In real usage, use actual vault or environment variables +url: https://github.com/ +org: test-org +repository_filter: test-* +group_by_languages: true +regex_filter: "(prod|staging)-(.*)" +show_archived_repos: false +cache: true diff --git a/tests/integration/test_github_repositories_integration.py b/tests/integration/test_github_repositories_integration.py new file mode 100644 index 0000000..23c7f50 --- /dev/null +++ b/tests/integration/test_github_repositories_integration.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +import os + +import pytest + +from plugins.inventory.github_repositories_inventory import GitHubRepositoryFetcher + + +pytestmark = pytest.mark.integration + + +def test_fetch_repositories_live_github_api(): + """Integration test against the real GitHub API. + + Required environment variables: + - GITHUB_TOKEN + Optional: + - GITHUB_TEST_ORG (default: "saltyblu") + - GITHUB_TEST_FILTER (default: "-deployment") + """ + token = os.getenv("GITHUB_TOKEN") + if not token: + pytest.skip("GITHUB_TOKEN not set") + + org = os.getenv("GITHUB_TEST_ORG", "saltyblu") + repo_filter = os.getenv("GITHUB_TEST_FILTER", "*-deployment") + + fetcher = GitHubRepositoryFetcher(token) + repos = fetcher.fetch_repositories( + repository_filter=repo_filter, + org=org, + archived=False, + group_by_languages=False, + ) + + assert isinstance(repos, list) + # A valid call should return a list; it can be empty depending on filter/org visibility. + if repos: + assert "name" in repos[0] + assert "languages" in repos[0] + assert repos[0]["languages"] is None diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..ee3f60d --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/unit/plugins/__init__.py b/tests/unit/plugins/__init__.py new file mode 100644 index 0000000..ee3f60d --- /dev/null +++ b/tests/unit/plugins/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/unit/plugins/inventory/__init__.py b/tests/unit/plugins/inventory/__init__.py new file mode 100644 index 0000000..ee3f60d --- /dev/null +++ b/tests/unit/plugins/inventory/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/unit/plugins/inventory/test_github_repositories_inventory.py b/tests/unit/plugins/inventory/test_github_repositories_inventory.py new file mode 100644 index 0000000..9ac6b0a --- /dev/null +++ b/tests/unit/plugins/inventory/test_github_repositories_inventory.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +import pytest +import logging +from unittest.mock import Mock + +from plugins.inventory.github_repositories_inventory import ( + GitHubRepositoryFetcher, + get_logger, +) + + +class TestGetLogger: + """Test the get_logger helper function.""" + + def test_get_logger_with_none_creates_new_logger(self): + """Test that get_logger creates a new logger when passed None.""" + logger = get_logger('test_logger', None) + assert isinstance(logger, logging.Logger) + assert logger.name == 'test_logger' + + def test_get_logger_returns_provided_logger(self): + """Test that get_logger returns the provided logger.""" + mock_logger = Mock() + result = get_logger('test_logger', mock_logger) + assert result is mock_logger + + +class TestGitHubRepositoryFetcher: + """Test the GitHubRepositoryFetcher class.""" + + def setup_method(self): + """Set up test fixtures.""" + self.access_token = 'test_token_123' + self.mock_logger = Mock(spec=logging.Logger) + + def test_initialization(self): + """Test GitHubRepositoryFetcher initialization.""" + fetcher = GitHubRepositoryFetcher(self.access_token, self.mock_logger) + assert fetcher.access_token == self.access_token + assert fetcher.logger is self.mock_logger + assert fetcher._github_client is None + + def test_initialization_without_logger(self): + """Test GitHubRepositoryFetcher initialization without logger.""" + fetcher = GitHubRepositoryFetcher(self.access_token) + assert fetcher.access_token == self.access_token + assert isinstance(fetcher.logger, logging.Logger) + + def test_set_github_client(self): + """Test setting a custom GitHub client.""" + fetcher = GitHubRepositoryFetcher(self.access_token, self.mock_logger) + mock_client = Mock() + fetcher.set_github_client(mock_client) + assert fetcher._github_client is mock_client + + def test_fetch_repositories_returns_list(self): + """Test that fetch_repositories returns a list of repositories.""" + fetcher = GitHubRepositoryFetcher(self.access_token, self.mock_logger) + + # Mock GitHub client and search result + mock_github = Mock() + mock_repo1 = Mock() + mock_repo1._rawData = {'name': 'repo1', 'id': 1} + mock_repo1.get_languages.return_value = {'Python': 100} + + mock_repo2 = Mock() + mock_repo2._rawData = {'name': 'repo2', 'id': 2} + mock_repo2.get_languages.return_value = {'Go': 100} + + mock_search_result = [mock_repo1, mock_repo2] + mock_github.search_repositories.return_value = mock_search_result + + fetcher.set_github_client(mock_github) + + result = fetcher.fetch_repositories( + repository_filter='test-*', + org='test-org', + archived=False, + group_by_languages=True + ) + + assert isinstance(result, list) + assert len(result) == 2 + assert result[0]['name'] == 'repo1' + assert result[0]['languages'] == {'Python': 100} + assert result[1]['name'] == 'repo2' + assert result[1]['languages'] == {'Go': 100} + + def test_fetch_repositories_without_languages(self): + """Test fetch_repositories without language information.""" + fetcher = GitHubRepositoryFetcher(self.access_token, self.mock_logger) + + mock_github = Mock() + mock_repo = Mock() + mock_repo._rawData = {'name': 'repo1', 'id': 1} + + mock_search_result = [mock_repo] + mock_github.search_repositories.return_value = mock_search_result + + fetcher.set_github_client(mock_github) + + result = fetcher.fetch_repositories( + repository_filter='test-*', + org='test-org', + group_by_languages=False + ) + + assert len(result) == 1 + assert result[0]['languages'] is None + + def test_fetch_repositories_search_error(self): + """Test fetch_repositories handles search errors.""" + fetcher = GitHubRepositoryFetcher(self.access_token, self.mock_logger) + + mock_github = Mock() + mock_github.search_repositories.side_effect = Exception('API Error') + + fetcher.set_github_client(mock_github) + + with pytest.raises(Exception) as exc_info: + fetcher.fetch_repositories( + repository_filter='test-*', + org='test-org' + ) + + assert 'API Error' in str(exc_info.value) + self.mock_logger.error.assert_called() + + +class TestInventoryModuleParseMethods: + """Test parse_groupnames and other inventory logic.""" + + def test_parse_groupnames_with_match(self): + """Test parse_groupnames with regex matches.""" + from plugins.inventory.github_repositories_inventory import InventoryModule + + inventory = InventoryModule(logger=Mock()) + repository = {'name': 'main-prod-api'} + + # Test regex that captures environment and service type + regex_filter = r'(prod|staging)-(.*)' + result = inventory.parse_groupnames(repository, regex_filter) + + assert result is not False + assert isinstance(result, list) + assert 'main-prod' in result + + def test_parse_groupnames_no_match(self): + """Test parse_groupnames with no matches.""" + from plugins.inventory.github_repositories_inventory import InventoryModule + + inventory = InventoryModule(logger=Mock()) + repository = {'name': 'random-repo'} + + regex_filter = r'(prod|staging)-(.*)' + result = inventory.parse_groupnames(repository, regex_filter) + + assert result is False + + def test_parse_groupnames_invalid_regex(self): + """Test parse_groupnames handles invalid regex gracefully.""" + from plugins.inventory.github_repositories_inventory import InventoryModule + + mock_logger = Mock() + inventory = InventoryModule(logger=mock_logger) + repository = {'name': 'test-repo'} + + # Invalid regex pattern + regex_filter = r'(invalid[' + result = inventory.parse_groupnames(repository, regex_filter) + + assert result is False + mock_logger.debug.assert_called() + + +class TestInventoryModuleIntegration: + """Integration tests for InventoryModule with mocked fetcher.""" + + def test_inventory_module_with_custom_fetcher(self): + """Test InventoryModule using custom fetcher.""" + from plugins.inventory.github_repositories_inventory import ( + InventoryModule, + GitHubRepositoryFetcher, + ) + + mock_logger = Mock() + mock_fetcher = Mock(spec=GitHubRepositoryFetcher) + mock_fetcher.fetch_repositories.return_value = [ + {'name': 'repo-1', 'topics': ['team-backend']}, + {'name': 'repo-2', 'topics': []}, + ] + + inventory = InventoryModule(logger=mock_logger, fetcher=mock_fetcher) + + # Verify that custom fetcher is stored + assert inventory.fetcher is mock_fetcher + assert inventory.logger is mock_logger diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..347916a --- /dev/null +++ b/tox.ini @@ -0,0 +1,134 @@ +[tox] +envlist = + py{39,310,311,312} + ansible-{2.10,2.11,2.12,2.13,2.14,2.15,8,9,10,11} + lint + type +skip_missing_interpreters = true + +[testenv] +description = Run unit tests with pytest +deps = + -r{toxinidir}/requirements-dev.txt +commands = + pytest {posargs:tests/unit/} + +[testenv:ansible-2.10] +description = Test with Ansible 2.10 +deps = + ansible>=2.10,<2.11 + -r{toxinidir}/requirements-dev.txt +commands = + pytest {posargs:tests/} + +[testenv:ansible-2.11] +description = Test with Ansible 2.11 +deps = + ansible>=2.11,<2.12 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-2.12] +description = Test with Ansible 2.12 +deps = + ansible>=2.12,<2.13 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-2.13] +description = Test with Ansible 2.13 +deps = + ansible>=2.13,<2.14 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-2.14] +description = Test with Ansible 2.14 +deps = + ansible>=2.14,<2.15 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-2.15] +description = Test with Ansible 2.15 +deps = + ansible>=2.15,<3.0 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-8] +description = Test with Ansible 8.x +deps = + ansible>=8,<9 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-9] +description = Test with Ansible 9.x +deps = + ansible>=9,<10 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-10] +description = Test with Ansible 10.x +deps = + ansible>=10,<11 + -r{toxinidir}/requirements-dev.txt + +[testenv:ansible-11] +description = Test with Ansible 11.x +deps = + ansible>=11,<12 + -r{toxinidir}/requirements-dev.txt + +[testenv:lint] +description = Run linters (pylint, flake8, ansible-lint) +skip_install = true +deps = + -r{toxinidir}/requirements-dev.txt + pylint + flake8 + ansible-lint>=6.0 +commands = + flake8 plugins/ tests/ + python -m pylint plugins/ --disable=all --enable=E,F + ansible-lint plugins/ || true + +[testenv:type] +description = Run type checking with mypy +skip_install = true +deps = + -r{toxinidir}/requirements-dev.txt + mypy +commands = + mypy plugins/ + +[testenv:coverage] +description = Run tests with coverage reporting +deps = + -r{toxinidir}/requirements-dev.txt + pytest-cov +commands = + pytest --cov=plugins --cov-report=html --cov-report=term-missing {posargs:tests/unit/} + +[testenv:docs] +description = Build documentation (if applicable) +skip_install = true +deps = + sphinx>=4.0 +commands = + echo "No documentation build configured yet" + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[gh-actions:env] +ANSIBLE_VERSION = + 2.10: ansible-2.10 + 2.11: ansible-2.11 + 2.12: ansible-2.12 + 2.13: ansible-2.13 + 2.14: ansible-2.14 + 2.15: ansible-2.15 + 8: ansible-8 + 9: ansible-9 + 10: ansible-10 + 11: ansible-11