From 8955e27c715a2357e7b0f6ca7a02f0ae07d96426 Mon Sep 17 00:00:00 2001 From: David Ormrod Morley Date: Mon, 12 Aug 2024 14:13:38 +0200 Subject: [PATCH] Add ci workflow to run unit tests on pr or merge into main branches SCMSUITE-9942 SO107 --- .github/workflows/ci.yml | 126 +++++++++++++++++++++++++++++++++ doc/build_plams_doc | 47 +++++++----- doc/requirements.txt | 4 ++ doc/source/conf.py | 10 ++- requirements.txt | 6 ++ setup.py | 3 +- unit_tests/test_helpers.py | 12 ++++ unit_tests/test_inputparser.py | 8 ++- unit_tests/test_interfaces.py | 3 + 9 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 doc/requirements.txt create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a38129ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +name: CI + +on: +# Run on creating or updating a PR + pull_request: + types: [opened, synchronize, reopened] + +# And pushing to the trunk or fix branches + push: + branches: + - trunk + - 'fix*' + +jobs: + + build_and_test: +# Run on ubuntu, mac and windows for python 3.8 (in AMS python stack) and 3.11 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.8", "3.11"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Move Repo To SCM Namespace and Set PYTHONPATH + if: runner.os != 'Windows' + run: | + mkdir -p scm/plams + find . -mindepth 1 -maxdepth 1 -not -name 'scm' -not -name '.*' -exec mv {} scm/plams/ \; + echo "PYTHONPATH=$(pwd):$PYTHONPATH" >> $GITHUB_ENV + + - name: Move Repo To SCM Namespace and Set PYTHONPATH (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + New-Item -Path scm\plams -ItemType Directory -Force + + Get-ChildItem -Directory | Where-Object { $_.Name -ne 'scm' } | ForEach-Object { + Move-Item -Path $_.FullName -Destination scm\plams + } + + Get-ChildItem -File | ForEach-Object { + Move-Item -Path $_.FullName -Destination scm\plams + } + + echo "PYTHONPATH=$(pwd);$env:PYTHONPATH" >> $env:GITHUB_ENV + + - name: Set Up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + working-directory: scm/plams + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + pip install coverage + pip install black + + - name: Run Unit Tests + working-directory: scm/plams + run: | + pwd + coverage run -m pytest unit_tests + - name: Evaluate Coverage + working-directory: scm/plams + run: coverage report --omit="unit_tests/*" -i --fail-under=30 + + + formatting-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install black + pip install "black[jupyter]" + + - name: Run Black Formatting Check + run: | + black --check -t py38 -l 120 . + + build-docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Move Repo To SCM Namespace and Set PYTHONPATH + if: runner.os != 'Windows' + run: | + mkdir -p scm/plams + find . -mindepth 1 -maxdepth 1 -not -name 'scm' -not -name '.*' -exec mv {} scm/plams/ \; + echo "PYTHONPATH=$(pwd):$PYTHONPATH" >> $GITHUB_ENV + + - name: Set Up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install Dependencies + working-directory: scm/plams + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + cd doc + pip install -r requirements.txt + + - name: Build Sphinx Docs + working-directory: scm/plams/doc + run: | + python build_plams_doc diff --git a/doc/build_plams_doc b/doc/build_plams_doc index a83d8099..2c6d28c9 100755 --- a/doc/build_plams_doc +++ b/doc/build_plams_doc @@ -6,45 +6,56 @@ import sys from os.path import join as opj command = "" -#Try to locate sphinx-build executable -if 'AMSBIN' in os.environ: - command = opj(os.path.expandvars('$AMSBIN'), 'python3.5', 'bin', 'sphinx-build') +# Try to locate sphinx-build executable +if "AMSBIN" in os.environ: + command = opj(os.path.expandvars("$AMSBIN"), "python3.5", "bin", "sphinx-build") if not os.path.exists(command): # This might be a windows machine - command = opj(os.path.expandvars('$AMSBIN'), 'python3.5', 'Scripts', 'sphinx-build.exe') + command = opj( + os.path.expandvars("$AMSBIN"), "python3.5", "Scripts", "sphinx-build.exe" + ) if os.path.exists(command): # starting sphinx-build on windows is tricky... - command = ['sh', opj(os.path.expandvars('$AMSBIN'), 'amspython'), '-m', 'sphinx'] + command = [ + "sh", + opj(os.path.expandvars("$AMSBIN"), "amspython"), + "-m", + "sphinx", + ] else: - print('Warning: AMSBIN found in environment, but failed to locate sphinx-build') + print( + "Warning: AMSBIN found in environment, but failed to locate sphinx-build" + ) command = "" if command == "": - null = open(os.devnull, 'wb') + null = open(os.devnull, "wb") try: - subprocess.call(['sphinx-build','--version'], stdout=null, stderr=null) - command = 'sphinx-build' + subprocess.call(["sphinx-build", "--version"], stdout=null, stderr=null) + command = "sphinx-build" except OSError: try: - subprocess.call(['sphinx-build2','--version'], stdout=null, stderr=null) - command = 'sphinx-build2' + subprocess.call(["sphinx-build2", "--version"], stdout=null, stderr=null) + command = "sphinx-build2" except OSError: - print('Error: Sphinx executable not found!') + print("Error: Sphinx executable not found!") null.close() sys.exit(0) null.close() location = os.path.dirname(os.path.realpath(__file__)) -#Source of documentation should be located in "source" subfolder next to this script -source = opj(location, 'source') +# Source of documentation should be located in "source" subfolder next to this script +source = opj(location, "source") -#Target can be given as command line argument, if not the "build" subfolder is used +# Target can be given as command line argument, if not the "build" subfolder is used if len(sys.argv) > 1: target = sys.argv[1] else: - target = opj(location, 'build') + target = opj(location, "build") if isinstance(command, list): # on windows command is a list of multiple items - subprocess.call(command + [source, target]) + return_code = subprocess.call(command + [source, target]) else: - subprocess.call([command, source, target]) + return_code = subprocess.call([command, source, target]) + +sys.exit(return_code) diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..35a0f19d --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,4 @@ +sphinx +sphinx_copybutton +sphinx_tabs +ipython \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index 7f3888bc..743eaf15 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -21,7 +21,7 @@ def modify_signature(app, what, name, obj, options, signature, return_annotation def setup(app): if not tags.has("scm_theme"): - app.add_stylesheet("boxes.css") + app.add_css_file("boxes.css") app.add_directive("warning", Danger) app.add_directive("technical", Important) app.connect("autodoc-process-signature", modify_signature) @@ -41,7 +41,7 @@ def setup(app): else: - extensions = [] + extensions = ["sphinx_tabs.tabs"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -200,7 +200,11 @@ def setup(app): # configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"python3": ("http://docs.python.org/3.8", None)} -autodoc_default_options = {"members": True, "private-members": True, "special-members": True} +autodoc_default_options = { + "members": True, + "private-members": True, + "special-members": True, +} autodoc_member_order = "bysource" autodoc_typehints = "none" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..b442c7fb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +dill==0.3.6 +numpy==1.23.4 +scipy==1.9.3 +natsort==8.1.0 +ase==3.22.1 +rdkit==2024.03.1 \ No newline at end of file diff --git a/setup.py b/setup.py index eacc788f..9e357740 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,8 @@ ], keywords=["molecular modeling", "computational chemistry", "workflow", "python interface"], python_requires=">=3.6", - install_requires=["dill>=0.2.4", "numpy", "scipy", "natsort"], + install_requires=["dill>=0.2.4", "numpy<2", "scipy", "natsort"], + extras_require={"chem_tools": ["ase", "rdkit"]}, packages=packages, package_dir={"scm.plams": "."}, package_data={ diff --git a/unit_tests/test_helpers.py b/unit_tests/test_helpers.py index 9213c57f..c1b46636 100644 --- a/unit_tests/test_helpers.py +++ b/unit_tests/test_helpers.py @@ -1,4 +1,7 @@ import builtins +import pytest +import os + from scm.plams.core.settings import ( SafeRunSettings, LogSettings, @@ -62,3 +65,12 @@ def assert_config_as_expected( assert isinstance(config.job.runscript, RunScriptSettings) assert isinstance(config.jobmanager, JobManagerSettings) assert isinstance(config, ConfigSettings) + + +def skip_if_no_ams_installation(): + """ + Check whether the AMSBIN environment variable is set, and therefore if there is an AMS installation present. + If there is no installation, skip the test with a warning. + """ + if os.getenv("AMSBIN") is None: + pytest.skip("Skipping test as cannot find AMS installation. '$AMSBIN' environment variable is not set.") diff --git a/unit_tests/test_inputparser.py b/unit_tests/test_inputparser.py index 987e3d5a..c79c5c4b 100644 --- a/unit_tests/test_inputparser.py +++ b/unit_tests/test_inputparser.py @@ -3,7 +3,7 @@ from importlib import reload from scm.plams.core.settings import Settings -from scm.plams.unit_tests.test_helpers import get_mock_import_function +from scm.plams.unit_tests.test_helpers import get_mock_import_function, skip_if_no_ams_installation @pytest.fixture @@ -96,6 +96,9 @@ def test_to_dict_with_scmlibbase_succeeds(system_text_inputs): def get_monkeypatched_input_parser(monkeypatch): + # If there is no AMS installation the input parser will not run so skip test with a warning + skip_if_no_ams_installation() + # Mock scm.libbase import failing (even when present in the env) mock_import_function = get_mock_import_function("scm.libbase") monkeypatch.setattr(builtins, "__import__", mock_import_function) @@ -114,6 +117,9 @@ def get_monkeypatched_input_parser(monkeypatch): def get_input_parser_or_skip(): + # If there is no AMS installation the input parser will not run so skip test with a warning + skip_if_no_ams_installation() + from scm.plams.interfaces.adfsuite.inputparser import InputParserFacade, InputParser # Get an instance of the input parser facade using the scm.libbase parser diff --git a/unit_tests/test_interfaces.py b/unit_tests/test_interfaces.py index 76dd3ab8..430a8ad1 100644 --- a/unit_tests/test_interfaces.py +++ b/unit_tests/test_interfaces.py @@ -4,6 +4,7 @@ pass from scm.plams import AMSJob, Settings +from scm.plams.unit_tests.test_helpers import skip_if_no_ams_installation def test_hybrid_engine_input(): @@ -53,6 +54,7 @@ def test_hybrid_engine_input(): EndEngine """ + skip_if_no_ams_installation() job = AMSJob.from_input(AMSinput) assert job.get_input() == AMSinput @@ -105,6 +107,7 @@ def test_list_block_input(): EndEngine """ + skip_if_no_ams_installation() job = AMSJob.from_input(AMSinput) assert job.get_input() == AMSinput