From 1f5a7b44ee6d46a130369cc4c48de77428f661c5 Mon Sep 17 00:00:00 2001
From: Frederik Hennig <frederik.hennig@fau.de>
Date: Mon, 13 Jan 2025 14:10:14 +0100
Subject: [PATCH] introduce Nox and refactor CI

---
 .gitignore     |  3 ++
 .gitlab-ci.yml | 33 +++++++++------------
 noxfile.py     | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++
 pyproject.toml |  7 ++++-
 4 files changed, 101 insertions(+), 21 deletions(-)
 create mode 100644 noxfile.py

diff --git a/.gitignore b/.gitignore
index 992284e7d..2129c6416 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
 __pycache__
 .ipynb_checkpoints
+
 .coverage*
+coverage.xml
+
 *.pyc
 *.vti
 /build
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6c58a26bd..901eee0ec 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -255,30 +255,25 @@ pycodegen-integration:
 
 # -------------------- Code Quality ---------------------------------------------------------------------
 
-
-flake8-lint:
+.qa-base:
   stage: "Code Quality"
+  image: i10git.cs.fau.de:5005/pycodegen/pycodegen/nox:alpine
+  needs: []
   except:
     variables:
       - $ENABLE_NIGHTLY_BUILDS
-  image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full
-  script:
-    - flake8 src/pystencils
   tags:
     - docker
 
-mypy-typecheck:
-  stage: "Code Quality"
-  except:
-    variables:
-      - $ENABLE_NIGHTLY_BUILDS
-  image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full
-  before_script:
-    - pip install -e .[tests]
+lint:
+  extends: .qa-base
   script:
-    - mypy src/pystencils
-  tags:
-    - docker
+    - nox --session lint
+
+typecheck:
+  extends: .qa-base
+  script:
+    - nox --session typecheck
 
 # -------------------- Unit Tests ---------------------------------------------------------------------
 
@@ -286,18 +281,16 @@ mypy-typecheck:
 tests-and-coverage:
   stage: "Unit Tests"
   needs: []
-  image: i10git.cs.fau.de:5005/pycodegen/pycodegen/full:cupy12.3
+  image: i10git.cs.fau.de:5005/pycodegen/pycodegen/nox:ubuntu24.04-cuda12
   before_script:
     - pip install -e .[tests]
   script:
     - env
     - pip list
-    - export NUM_CORES=$(nproc --all)
     - mkdir -p ~/.config/matplotlib
     - echo "backend:template" > ~/.config/matplotlib/matplotlibrc
     - mkdir public
-    - pytest -v -n $NUM_CORES --cov-report html --cov-report xml --cov-report term --cov=. -m "not longrun" --html test-report/index.html --junitxml=report.xml
-    - python -m coverage xml
+    - nox --session "testsuite(cupy12)"
   tags:
     - docker
     - cuda11
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 000000000..876d7a914
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+from typing import Sequence
+
+import os
+import nox
+import subprocess
+
+nox.options.sessions = ["lint", "typecheck", "testsuite"]
+
+
+def get_cuda_version() -> None | tuple[int, ...]:
+    smi_args = ["nvidia-smi", "--version"]
+
+    try:
+        result = subprocess.run(smi_args, capture_output=True)
+    except FileNotFoundError:
+        return None
+    
+    smi_output = str(result.stdout).splitlines()
+    cuda_version = smi_output[-1].split(":")[1].strip()
+    return tuple(int(v) for v in cuda_version.split("."))
+
+
+def editable_install(session: nox.Session, opts: Sequence[str] = ()):
+    if opts:
+        opts_str = "[" + ",".join(opts) + "]"
+    else:
+        opts_str = ""
+    session.install("-e", f".{opts_str}")
+
+
+@nox.session(python="3.10", tags=["qa", "code-quality"])
+def lint(session: nox.Session):
+    """Lint code using flake8"""
+
+    session.install("flake8")
+    session.run("flake8", "src/pystencils")
+
+
+@nox.session(python="3.10", tags=["qa", "code-quality"])
+def typecheck(session: nox.Session):
+    """Run MyPy for static type checking"""
+    editable_install(session)
+    session.install("mypy")
+    session.run("mypy", "src/pystencils")
+
+
+@nox.parametrize(
+    "cupy_version",
+    [None, "12", "13"],
+    ids=["cpu", "cupy12", "cupy13"]
+)
+@nox.session(python="3.10", tags=["test"])
+def testsuite(session: nox.Session, cupy_version: str | None):
+    if cupy_version is not None:
+        cuda_version = get_cuda_version()
+        if cuda_version is None or cuda_version[0] < 11:
+            session.skip("No compatible installation of CUDA found - Need at least CUDA 11")
+        
+        cuda_major = cuda_version[0]
+        cupy_package = f"cupy-cuda{cuda_major}=={cupy_version}"
+        session.install(cupy_package)
+
+    editable_install(session, ["alltrafos", "use_cython", "interactive", "testsuite"])
+
+    num_cores = os.cpu_count()
+
+    session.run(
+        "pytest",
+        "-v",
+        "-n", str(num_cores),
+        "--cov-report=term",
+        "--cov=.",
+        "-m", "not longrun",
+        "--html", "test-report/index.html",
+        "--junitxml=report.xml"
+    )
+    session.run("coverage", "html")
+    session.run("coverage", "xml")
diff --git a/pyproject.toml b/pyproject.toml
index d9a33c9d7..4f4cad8e7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,6 +44,11 @@ interactive = [
 use_cython = [
     'Cython'
 ]
+dev = [
+    "flake8",
+    "mypy",
+    "black",
+]
 doc = [
     'sphinx',
     'pydata-sphinx-theme==0.15.4',
@@ -54,7 +59,7 @@ doc = [
     'sphinx_design',
     'myst-nb'
 ]
-tests = [
+testsuite = [
     'pytest',
     'pytest-cov',
     'pytest-html',
-- 
GitLab