From 37491e8c9625d90e4752bcc9705abcf7c3a9a990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-No=C3=ABl=20Grad?= Date: Fri, 17 May 2024 09:28:46 +0200 Subject: [PATCH] Add Codecov (#57) * Split workflows * Add Codecov * Add unit tests --- .codecov.yml | 26 +++++++++++++ .github/actions/dependencies/action.yml | 25 ++++++++++++ .github/workflows/samples.yml | 35 +++++++++++++++++ .github/workflows/testsuite.yml | 35 +++++++++++------ Makefile | 26 ++++++++++++- README.md | 7 +++- lib/analysis.py | 3 +- pyMBE.py | 8 ++-- testsuite/serialization_test.py | 52 +++++++++++++++++++++++++ 9 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 .codecov.yml create mode 100644 .github/actions/dependencies/action.yml create mode 100644 .github/workflows/samples.yml create mode 100644 testsuite/serialization_test.py diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..8cc8a80 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,26 @@ +--- +codecov: + branch: main + notify: + require_ci_to_pass: yes + +coverage: + precision: 0 + round: down + range: "70...100" + status: + project: + default: + enabled: yes + threshold: 5 + patch: + default: + enabled: yes + threshold: 5% + changes: no + +comment: false + +ignore: +- "samples" +- "maintainer" diff --git a/.github/actions/dependencies/action.yml b/.github/actions/dependencies/action.yml new file mode 100644 index 0000000..3eac63f --- /dev/null +++ b/.github/actions/dependencies/action.yml @@ -0,0 +1,25 @@ +name: 'dependencies' +description: 'Install pyMBE dependencies' +inputs: + extra-python-packages: + description: Newline-separated list of arguments for pip. + required: false + modules: + description: Newline-separated list of arguments for module load. + required: true +runs: + using: "composite" + steps: + - run: | + module load ${{ inputs.modules }} + module save pymbe + python3 -m venv --system-site-packages venv + source venv/bin/activate + python3 maintainer/configure_venv.py + echo -e "\n" >> requirements.txt + echo "${{ inputs.extra-python-packages }}" >> requirements.txt + python3 -m pip install -r requirements.txt + git checkout requirements.txt + deactivate + module purge + shell: bash diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml new file mode 100644 index 0000000..d53417e --- /dev/null +++ b/.github/workflows/samples.yml @@ -0,0 +1,35 @@ +name: samples + +on: + schedule: + - cron: '20 6 5,20 * *' # biweekly at 06:20 AM UTC+00 (on the 5th and 20th of the month) + workflow_dispatch: # manual trigger + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + samples: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'schedule' && github.repository == 'pyMBE-dev/pyMBE' || github.event_name != 'schedule' }} + env: + FI_PROVIDER: "^psm3,psm3;ofi_rxd" + OMPI_MCA_mtl_ofi_provider_exclude: psm3 + steps: + - name: Setup EESSI + uses: eessi/github-action-eessi@v3 + with: + eessi_stack_version: "2023.06" + - name: Checkout repository + uses: actions/checkout@main + - name: Install dependencies + uses: ./.github/actions/dependencies + with: + modules: |- + ESPResSo/4.2.1-foss-2023a + - name: Run testsuite + run: | + module restore pymbe + source venv/bin/activate + make functional_tests + shell: bash diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index a8bd5ab..b1e5669 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -1,4 +1,4 @@ -name: run tests +name: testsuite on: push: @@ -21,22 +21,22 @@ jobs: - name: Checkout repository uses: actions/checkout@main - name: Install dependencies - run: | - module load ESPResSo/4.2.1-foss-2023a - python3 -m venv --system-site-packages venv - source venv/bin/activate - python3 maintainer/configure_venv.py - python3 -m pip install -r requirements.txt - python3 -m pip install "pdoc==14.3" "pylint==3.0.3" - deactivate + uses: ./.github/actions/dependencies + with: + modules: |- + ESPResSo/4.2.1-foss-2023a + extra-python-packages: |- + pdoc==14.3 + pylint==3.0.3 + coverage==7.4.4 - name: Run testsuite run: | - module load ESPResSo/4.2.1-foss-2023a + module restore pymbe source venv/bin/activate make pylint - make tests + make unit_tests COVERAGE=1 make docs - deactivate + make coverage_xml shell: bash - name: Upload artifact uses: actions/upload-artifact@v4 @@ -45,3 +45,14 @@ jobs: name: documentation retention-days: 2 if-no-files-found: error + - name: Upload coverage to Codecov + if: ${{ github.repository == 'pyMBE-dev/pyMBE' }} + uses: codecov/codecov-action@v4 + with: + file: "./coverage.xml" + disable_search: true + env_vars: OS,PYTHON + fail_ci_if_error: false + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/Makefile b/Makefile index 2ed0794..9a148de 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,24 @@ .PHONY: visual .PHONY: clean +# whether to run unit tests with code coverage +COVERAGE = 0 + +# output directory for the code coverage HTML files +COVERAGE_HTML = coverage + +# Python executable or launcher, possibly with command line arguments PYTHON = python3 +ifeq ($(COVERAGE),1) +PYTHON := ${PYTHON} -m coverage run --parallel-mode --source=$(CURDIR) +endif docs: mkdir -p ./documentation PDOC_ALLOW_EXEC=0 ${PYTHON} -m pdoc ./pyMBE.py -o ./documentation --docformat google -tests: +unit_tests: + ${PYTHON} testsuite/serialization_test.py ${PYTHON} testsuite/lj_tests.py ${PYTHON} testsuite/set_particle_acidity_test.py ${PYTHON} testsuite/bond_tests.py @@ -19,6 +30,8 @@ tests: ${PYTHON} testsuite/read-write-df_test.py ${PYTHON} testsuite/parameter_test.py ${PYTHON} testsuite/henderson_hasselbalch_tests.py + +functional_tests: ${PYTHON} testsuite/cph_ideal_tests.py ${PYTHON} testsuite/grxmc_ideal_tests.py ${PYTHON} testsuite/peptide_tests.py @@ -26,6 +39,17 @@ tests: ${PYTHON} testsuite/weak_polyelectrolyte_dialysis_test.py ${PYTHON} testsuite/globular_protein_tests.py +tests: unit_tests functional_tests + +coverage_xml: + ${PYTHON} -m coverage combine . + ${PYTHON} -m coverage report + ${PYTHON} -m coverage xml + +coverage_html: + ${PYTHON} -m coverage combine . + ${PYTHON} -m coverage html --directory="${COVERAGE_HTML}" + sample: ${PYTHON} samples/peptide.py diff --git a/README.md b/README.md index 6eb2e63..45c9656 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # pyMBE: the Python-based Molecule Builder for ESPResSo +![GitHub Actions](https://github.com/pyMBE-dev/pyMBE/actions/workflows/testsuite.yml/badge.svg) +[![codecov](https://codecov.io/gh/pyMBE-dev/pyMBE/branch/codecov/graph/badge.svg)](https://codecov.io/gh/pyMBE-dev/pyMBE) + pyMBE provides tools to facilitate building up molecules with complex architectures in the Molecular Dynamics software [ESPResSo](https://espressomd.org/wordpress/). Some examples of molecules that can be set up with pyMBE are polyelectrolytes, peptides and proteins. pyMBE bookkeeps all the information about the molecule topology, permitting to link each particle to its corresponding residue and molecule. pyMBE uses the [Pint](https://pint.readthedocs.io/en/stable/) library to enable input parameters in any arbitrary unit system, which is later transformed in the reduced unit system used in ESPResSo. ## Dependencies @@ -47,8 +50,8 @@ git clone git@github.com:pyMBE-dev/pyMBE.git Please, be aware that pyMBE is intended to be a supporting tool to setup simulations with ESPResSo. Thus, for most of its functionalities ESPResSo must also be available. Following the NEP29 guidelines, we recommend the users of pyMBE to use Python3.10+ when using our module. -The pyMBE module uses its own Python virtual enviroment to avoid incompatibility issues when loading its requierements from other libraries. -The Python module (`venv`)[https://docs.python.org/3/library/venv.html#module-venv] from the Python Standard Library (starting with Python 3.3) is needed to set up pyMBE. +The pyMBE module uses its own Python virtual enviroment to avoid incompatibility issues when loading its requirements from other libraries. +The Python module [`venv`](https://docs.python.org/3/library/venv.html) is needed to set up pyMBE. If `venv` is not in the Python distribution of the user, the user will need to first install 'venv' before setting up pyMBE. For Ubuntu users, this can be done as follows: diff --git a/lib/analysis.py b/lib/analysis.py index f82745c..9cd119a 100644 --- a/lib/analysis.py +++ b/lib/analysis.py @@ -136,7 +136,7 @@ def get_params_from_dir_name(name): entries = name.split('_') params = {} for entry in entries: - sp_entry = entry.split('-') + sp_entry = entry.split('-', 1) params[sp_entry[0]] = sp_entry[-1] #float(sp_entry[-1]) # creates a dictionary of parameters and their values. return params @@ -343,7 +343,6 @@ def get_distribution_from_df(df, key): distribution_list (`lst`): list stored under `key` """ - import pandas as pd distribution_list=[] for row in df[key]: if pd.isnull(row): diff --git a/pyMBE.py b/pyMBE.py index eb1e5ba..daa664e 100644 --- a/pyMBE.py +++ b/pyMBE.py @@ -2157,10 +2157,10 @@ def print_reduced_units(self): unit_length=self.units.Quantity(1,'reduced_length') unit_energy=self.units.Quantity(1,'reduced_energy') unit_charge=self.units.Quantity(1,'reduced_charge') - print(unit_length.to('nm'), "=", unit_length) - print(unit_energy.to('J'), "=", unit_energy) - print('Temperature:', (self.kT/self.Kb).to("K")) - print(unit_charge.to('C'), "=", unit_charge) + print(f"{unit_length.to('nm'):.5g} = {unit_length}") + print(f"{unit_energy.to('J'):.5g} = {unit_energy}") + print(f"{unit_charge.to('C'):.5g} = {unit_charge}") + print(f"Temperature: {(self.kT/self.Kb).to('K'):.5g}") print() def propose_unused_type(self): diff --git a/testsuite/serialization_test.py b/testsuite/serialization_test.py new file mode 100644 index 0000000..eea3ad2 --- /dev/null +++ b/testsuite/serialization_test.py @@ -0,0 +1,52 @@ +import io +import json +import contextlib +import unittest as ut +import numpy as np +import pandas as pd +import pyMBE +import lib.analysis + + +class Serialization(ut.TestCase): + + def test_json_encoder(self): + encoder = pyMBE.pymbe_library.NumpyEncoder + # Python types + self.assertEqual(json.dumps(1, cls=encoder), "1") + self.assertEqual(json.dumps([1, 2], cls=encoder), "[1, 2]") + self.assertEqual(json.dumps((1, 2), cls=encoder), "[1, 2]") + self.assertEqual(json.dumps({1: 2}, cls=encoder), """{"1": 2}""") + # NumPy types + self.assertEqual(json.dumps(np.array([1, 2]), cls=encoder), "[1, 2]") + self.assertEqual(json.dumps(np.array(1), cls=encoder), "1") + self.assertEqual(json.dumps(np.int32(1), cls=encoder), "1") + # Pandas types + with self.assertRaisesRegex(TypeError, "Object of type Series is not JSON serializable"): + json.dumps(pd.Series([1, 2]), cls=encoder) + + def test_parameters_to_path(self): + params = {"kT": 2., "phi": -np.pi, "n": 3, "fene": True, "name": "pep"} + name = lib.analysis.built_output_name(params) + self.assertEqual(name, "kT-2_phi--3.14_n-3_fene-True_name-pep") + params_out = lib.analysis.get_params_from_dir_name(name) + params_ref = {"kT": "2", "phi": "-3.14", "n": "3", + "fene": "True", "name": "pep"} + self.assertEqual(params_out, params_ref) + + def test_pint_units(self): + ref_output = [ + "Current set of reduced units:", + "0.355 nanometer = 1 reduced_length", + "4.1164e-21 joule = 1 reduced_energy", + "1.6022e-19 coulomb = 1 reduced_charge", + "Temperature: 298.15 kelvin", + ] + pmb = pyMBE.pymbe_library(SEED=42) + with contextlib.redirect_stdout(io.StringIO()) as f: + pmb.print_reduced_units() + self.assertEqual(f.getvalue().strip("\n").split("\n"), ref_output) + + +if __name__ == "__main__": + ut.main()