diff --git a/.github/workflows/test-terra-base.yml b/.github/workflows/test-terra-base.yml new file mode 100644 index 00000000..7705aa49 --- /dev/null +++ b/.github/workflows/test-terra-base.yml @@ -0,0 +1,43 @@ +name: Test terra-base +# Smoke-test the terra-base image on pull requests that touch it. + +on: + pull_request: + branches: [ master ] + + workflow_dispatch: + # Allows manually triggering the workflow on a selected branch via the GitHub Actions tab. + +jobs: + + test_docker_image: + runs-on: self-hosted + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Build the terra-base image locally + run: | + docker build --pull ./terra-base --tag terra-base:smoke-test + + - name: Test Python version + run: | + version=$(docker run --rm --entrypoint="" terra-base:smoke-test python --version) + echo "$version" + [[ "$version" == "Python 3.10"* ]] + + - name: Test OS version + run: | + version=$(docker run --rm --entrypoint="" terra-base:smoke-test \ + bash -c 'source /etc/os-release && echo "$VERSION"') + echo "$version" + [[ "$version" == "22.04"* ]] + + - name: Smoke test + run: | + docker run --rm \ + --volume "$GITHUB_WORKSPACE/terra-base/tests:/tests:ro" \ + --entrypoint="" \ + terra-base:smoke-test \ + /etc/jupyter/bin/python /tests/smoke_test.py diff --git a/build_all.sh b/build_all.sh index 04912cc2..cc1f55cb 100755 --- a/build_all.sh +++ b/build_all.sh @@ -13,7 +13,7 @@ # 8- terra-jupyter-bioconductor # 9- terra-rstudio-aou # 10- wondershaper -images=("terra-base", "terra-jupyter-base" "terra-jupyter-python" "terra-jupyter-r" "terra-jupyter-gatk" "terra-jupyter-hail" "terra-jupyter-aou" "terra-jupyter-bioconductor" "terra-rstudio-aou" "wondershaper") +images=("terra-base" "terra-jupyter-base" "terra-jupyter-python" "terra-jupyter-r" "terra-jupyter-gatk" "terra-jupyter-hail" "terra-jupyter-aou" "terra-jupyter-bioconductor" "terra-rstudio-aou" "wondershaper") # Loop over each image to build in the correct order for image in "${images[@]}"; do diff --git a/terra-base/tests/smoke_test.py b/terra-base/tests/smoke_test.py new file mode 100644 index 00000000..9f3c14fc --- /dev/null +++ b/terra-base/tests/smoke_test.py @@ -0,0 +1,87 @@ +"""Smoke tests for the terra-base image. + +Verifies that both the user-facing conda kernel env and the Jupyter server venv run the +image's target Python (the PYTHON_VERSION set in the Dockerfile), and that the Jupyter +stack and the Terra (Leonardo) notebook extensions import on that Python. + +Has no third-party dependencies, so CI can run it without installing anything: + + docker run --rm --entrypoint="" terra-base:smoke-test \ + /etc/jupyter/bin/python /tests/smoke_test.py + +Run it with the Jupyter *server* venv python (it shells out to check the kernel env too). +The functions are also pytest-discoverable if pytest is available in that interpreter. +""" +import os +import subprocess +import sys + +# The Jupyter server runs in this venv; user notebook code runs in the conda kernel env. +SERVER_PY = "/etc/jupyter/bin/python" +KERNEL_PY = "/home/jupyter/.envs/python3/bin/python" +# Where the Dockerfile copies the Leonardo extensions (loaded via run-jupyter.sh). +CUSTOM_DIR = "/etc/jupyter/custom" + + +def _target_version(): + """The image's target Python as 'major.minor' (from ENV PYTHON_VERSION), or None.""" + return os.environ.get("PYTHON_VERSION", "").strip() or None + + +def _minor(python_path): + out = subprocess.check_output( + [python_path, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"] + ) + return out.decode().strip() + + +def test_kernel_env_matches_target(): + target = _target_version() + got = _minor(KERNEL_PY) + if target: + assert got == target, f"conda kernel env is Python {got}, expected {target}" + + +def test_server_venv_matches_target(): + target = _target_version() + got = _minor(SERVER_PY) + if target: + assert got == target, f"jupyter server venv is Python {got}, expected {target}" + + +def test_server_and_kernel_agree(): + assert _minor(SERVER_PY) == _minor(KERNEL_PY), "server venv and kernel env Python differ" + + +def test_jupyter_stack_imports(): + # Runs under the server venv python, where the Jupyter stack lives. + import notebook # noqa: F401 + import jupyterlab # noqa: F401 + import jupyter_server # noqa: F401 + import nbclassic # noqa: F401 + import jupyter_core # noqa: F401 + import traitlets # noqa: F401 + import notebook.utils # noqa: F401 exercises the distutils->setuptools shim on 3.12+ + + +def test_terra_extensions_import(): + sys.path.insert(0, CUSTOM_DIR) + import jupyter_localize_extension # noqa: F401 + import jupyter_delocalize # noqa: F401 + + +if __name__ == "__main__": + import traceback + + tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] + failed = 0 + for test in tests: + try: + test() + print(f"PASS {test.__name__}") + except Exception: + failed += 1 + print(f"FAIL {test.__name__}") + traceback.print_exc() + print(f"\n{len(tests) - failed}/{len(tests)} passed") + sys.exit(1 if failed else 0)