Skip to content

Commit

Permalink
Add unit tests for tasks (#45)
Browse files Browse the repository at this point in the history
Add tests for tasks.
Run tests in CI with coverage.

Fix API reference generation project links in markdown files.
  • Loading branch information
CasperWA committed Aug 23, 2022
1 parent 9d48fa5 commit e934b85
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 10 deletions.
51 changes: 47 additions & 4 deletions .github/workflows/_local_ci_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,56 @@ jobs:
name: Call reusable workflow
uses: ./.github/workflows/ci_tests.yml
with:
# general
python_version: "3.9"
install_extras: "[dev,docs]"
skip_pre-commit_hooks: pylint
pylint_options: --rcfile=pyproject.toml
pylint_targets: ci_cd
install_extras: "[dev,docs,testing]"

# pre-commit
skip_pre-commit_hooks: pylint,pylint-tests

# pylint
pylint_runs: |
--rcfile=pyproject.toml ci_cd
--rcfile=pyproject.toml --disable=import-outside-toplevel,redefined-outer-name tests
# build dist
build_libs: flit
build_cmd: flit build

# build docs
update_python_api_ref: false
update_docs_landing_page: false
debug: false

pytest:
name: pytest
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version}}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version}}

- name: Install Python dependencies
run: |
python -m pip install -U pip
pip install -U setuptools wheel flit
pip install -e .[testing]
- name: Test with pytest
run: pytest -vvv --cov=ci_cd --cov-report=xml

- name: Upload coverage to Codecov
if: matrix.python-version == '3.9' && github.repository == 'SINTEF/ci-cd'
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ __pycache__/
*.egg*/
build/

# Documentation
site/

# Coverage
.coverage
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ repos:
hooks:
- id: bandit
args: ["-r"]
exclude: ^tests/.*$

# mypy is a static typing linter
# The main code repository can be found at:
Expand All @@ -62,6 +63,23 @@ repos:
- id: pylint
name: pylint
entry: pylint
args: ["--rcfile=pyproject.toml"]
language: python
types: [python]
require_serial: true
exclude: ^tests/.*$
# pylint is a Python linter
# It is run through the local environment to ensure external packages can be
# imported without issue.
# For more information about pylint see its documentation at:
# https://pylint.pycqa.org/en/latest/
- id: pylint-tests
name: pylint - tests
entry: pylint
args:
- "--rcfile=pyproject.toml"
- "--disable=import-outside-toplevel,redefined-outer-name"
language: python
types: [python]
require_serial: true
files: ^tests/.*$
16 changes: 10 additions & 6 deletions ci_cd/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,8 @@ def update_deps( # pylint: disable=too-many-branches,too-many-locals,too-many-s

# Update pyproject.toml
updated_version = ".".join(latest_version[: len(version.split("."))])
escaped_full_dependency_name = full_dependency_name.replace(
"[", "\[" # pylint: disable=anomalous-backslash-in-string
).replace(
"]", "\]" # pylint: disable=anomalous-backslash-in-string
escaped_full_dependency_name = full_dependency_name.replace("[", r"\[").replace(
"]", r"\]"
)
update_file(
pyproject_path,
Expand Down Expand Up @@ -500,6 +498,8 @@ def create_api_reference_docs( # pylint: disable=too-many-locals,too-many-branc
unwanted_folder: list[str] = ["__pycache__"]
if not unwanted_file:
unwanted_file: list[str] = ["__init__.py"]
if not full_docs_folder:
full_docs_folder: list[str] = []

def write_file(full_path: Path, content: str) -> None:
"""Write file with `content` to `full_path`"""
Expand Down Expand Up @@ -604,9 +604,13 @@ def write_file(full_path: Path, content: str) -> None:

basename = filename[: -len(".py")]
py_path = (
f"{package_dir.name}/{relpath}/{basename}".replace("/", ".")
f"{package_dir.relative_to(root_repo_path)}/{relpath}/{basename}".replace(
"/", "."
)
if str(relpath) != "."
else f"{package_dir.name}/{basename}".replace("/", ".")
else f"{package_dir.relative_to(root_repo_path)}/{basename}".replace(
"/", "."
)
)
md_filename = filename.replace(".py", ".md")
if debug:
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ docs = [
"mkdocs-material ~=8.4",
"mkdocstrings[python] ~=0.19.0",
]
testing = [
"pytest ~=7.1",
"pytest-cov ~=3.0",
]
dev = [
"pre-commit ~=2.20",
"pylint ~=2.13",
Expand Down
200 changes: 200 additions & 0 deletions tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"""Test `ci_cd/tasks.py`."""
# pylint: disable=too-many-locals
import pytest


def test_create_api_reference() -> None:
"""Check create_api_reference_docs runs with defaults."""
import os
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory

from invoke import MockContext

from ci_cd.tasks import create_api_reference_docs

with TemporaryDirectory() as tmpdir:
root_path = Path(tmpdir).resolve()
package_dir = root_path / "ci_cd"
shutil.copytree(
src=Path(__file__).resolve().parent.parent / "ci_cd",
dst=package_dir,
)

docs_folder = root_path / "docs"
docs_folder.mkdir()

create_api_reference_docs(
MockContext(),
str(package_dir),
root_repo_path=str(root_path),
)

api_reference_folder = docs_folder / "api_reference"

assert docs_folder.exists(), f"Parent content: {os.listdir(docs_folder.parent)}"
assert (
api_reference_folder.exists()
), f"Parent content: {os.listdir(api_reference_folder.parent)}"
assert {".pages", "main.md", "tasks.md"} == set(
os.listdir(api_reference_folder)
)

assert (api_reference_folder / ".pages").read_text(
encoding="utf8"
) == 'title: "API Reference"\n'
assert (api_reference_folder / "main.md").read_text(
encoding="utf8"
) == "# main\n\n::: ci_cd.main\n"
assert (api_reference_folder / "tasks.md").read_text(
encoding="utf8"
) == "# tasks\n\n::: ci_cd.tasks\n"


def test_api_reference_nested_package() -> None:
"""Check create_api_reference_docs generates correct link to sub-nested package
directory."""
import os
import shutil
from pathlib import Path
from tempfile import TemporaryDirectory

from invoke import MockContext

from ci_cd.tasks import create_api_reference_docs

with TemporaryDirectory() as tmpdir:
root_path = Path(tmpdir).resolve()
package_dir = root_path / "src" / "ci_cd" / "ci_cd"
shutil.copytree(
src=Path(__file__).resolve().parent.parent / "ci_cd",
dst=package_dir,
)

docs_folder = root_path / "docs"
docs_folder.mkdir()

create_api_reference_docs(
MockContext(),
str(package_dir),
root_repo_path=str(root_path),
)

api_reference_folder = docs_folder / "api_reference"

assert docs_folder.exists(), f"Parent content: {os.listdir(docs_folder.parent)}"
assert (
api_reference_folder.exists()
), f"Parent content: {os.listdir(api_reference_folder.parent)}"
assert {".pages", "main.md", "tasks.md"} == set(
os.listdir(api_reference_folder)
)

assert (api_reference_folder / ".pages").read_text(
encoding="utf8"
) == 'title: "API Reference"\n'
assert (api_reference_folder / "main.md").read_text(
encoding="utf8"
) == "# main\n\n::: src.ci_cd.ci_cd.main\n"
assert (api_reference_folder / "tasks.md").read_text(
encoding="utf8"
) == "# tasks\n\n::: src.ci_cd.ci_cd.tasks\n"


def test_update_deps() -> None:
"""Check update_deps runs with defaults."""
import re
from pathlib import Path
from tempfile import TemporaryDirectory

import tomlkit
from invoke import MockContext

from ci_cd.tasks import update_deps

original_dependencies = {
"invoke": "1.7",
"tomlkit": "0.11.4",
"mike": "1.1",
"pytest": "7.1",
"pytest-cov": "3.0",
"pre-commit": "2.20",
"pylint": "2.13",
}

with TemporaryDirectory() as tmpdir:
root_path = Path(tmpdir).resolve()
pyproject_file = root_path / "pyproject.toml"
pyproject_file.write_text(
data=f"""
[project]
requires-python = "~=3.7"
dependencies = [
"invoke ~={original_dependencies['invoke']}",
"tomlkit[test,docs] ~={original_dependencies['tomlkit']}",
]
[project.optional-dependencies]
docs = [
"mike >={original_dependencies['mike']},<3",
]
testing = [
"pytest ~={original_dependencies['pytest']}",
"pytest-cov ~={original_dependencies['pytest-cov']}",
]
dev = [
"mike >={original_dependencies['mike']},<3",
"pytest ~={original_dependencies['pytest']}",
"pytest-cov ~={original_dependencies['pytest-cov']}",
"pre-commit ~={original_dependencies['pre-commit']}",
"pylint ~={original_dependencies['pylint']}",
]
""",
encoding="utf8",
)

context = MockContext(
run={
re.compile(r".*invoke$"): "invoke (1.7.1)\n",
re.compile(r".*tomlkit$"): "tomlkit (1.0.0)",
re.compile(r".*mike$"): "mike (1.0.1)",
re.compile(r".*pytest$"): "pytest (7.1.0)",
re.compile(r".*pytest-cov$"): "pytest-cov (3.1.0)",
re.compile(r".*pre-commit$"): "pre-commit (2.20.0)",
re.compile(r".*pylint$"): "pylint (2.14.0)",
}
)

update_deps(
context,
root_repo_path=str(root_path),
)

pyproject = tomlkit.loads(pyproject_file.read_bytes())

dependencies: list[str] = pyproject.get("project", {}).get("dependencies", [])
for optional_deps in (
pyproject.get("project", {}).get("optional-dependencies", {}).values()
):
dependencies.extend(optional_deps)

for line in dependencies:
if any(
package_name in line
for package_name in ["invoke ", "pytest ", "pre-commit "]
):
package_name = line.split(maxsplit=1)[0]
assert line == f"{package_name} ~={original_dependencies[package_name]}"
elif "tomlkit" in line:
# Should be three version digits, since the original dependency had three.
assert line == "tomlkit[test,docs] ~=1.0.0"
elif "mike" in line:
assert line == "mike >=1.0,<3"
elif "pytest-cov" in line:
assert line == "pytest-cov ~=3.1"
elif "pylint" in line:
assert line == "pylint ~=2.14"
else:
pytest.fail(f"Unknown package in line: {line}")

0 comments on commit e934b85

Please sign in to comment.