diff --git a/.github/workflows/coveralls-main.yaml b/.github/workflows/coveralls-main.yaml new file mode 100644 index 0000000..e3bf3d2 --- /dev/null +++ b/.github/workflows/coveralls-main.yaml @@ -0,0 +1,145 @@ +name: Coveralls Main + +on: + push: + branches: [main] + + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: coveralls-main-${{ github.ref }} + cancel-in-progress: true + +jobs: + python-coverage: + name: Build main coverage baseline (Python) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + permissions: + contents: read + checks: write + statuses: write + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python 3.14 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: "3.14" + + - name: Install testing dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e .[testing] + + - name: Run coverage + run: | + python -m pytest tests/unit \ + --cov sist \ + --cov-report term-missing \ + --cov-report xml \ + -q + + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: coverage.xml + flag-name: python + parallel: true + fail-on-error: false + + cpp-coverage: + name: Build main coverage baseline (C++) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + permissions: + contents: read + checks: write + statuses: write + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python 3.14 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: "3.14" + + - name: Setup Conda (for irf only) + uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 + with: + miniforge-variant: Miniforge3 + auto-activate: true + activate-environment: base + conda-remove-defaults: true + channels: conda-forge,bioconda + channel-priority: strict + + - name: Install irf + shell: bash -l {0} + run: | + conda install -y \ + --override-channels \ + -c bioconda \ + -c conda-forge \ + "irf>=3.09,<3.10" + + - name: Install sist and coverage tooling + shell: bash -l {0} + run: | + python -m pip install --upgrade pip + python -m pip install -e .[testing] + python -m pip install gcovr + + - name: Build qsidd with coverage instrumentation + run: | + make -C src/trans_three clean + make -C src/trans_three coverage + make -C src/trans_compete clean + make -C src/trans_compete coverage + + - name: Run regression tests against instrumented binaries + shell: bash -l {0} + env: + CONDA_BUILD_STATE: TEST + SIST_TRANS_THREE_BIN: ${{ github.workspace }}/src/trans_three/qsidd + SIST_TRANS_COMPETE_BIN: ${{ github.workspace }}/src/trans_compete/qsidd + run: python -m pytest tests/regression -vv + + - name: Generate lcov report + run: | + gcovr --root . \ + --filter 'src/trans_three/.*' \ + --filter 'src/trans_compete/.*' \ + --gcov-ignore-parse-errors=suspicious_hits.warn \ + --lcov coverage.info + + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: coverage.info + flag-name: cpp + parallel: true + fail-on-error: false + + finish: + name: Close Coveralls build + needs: [python-coverage, cpp-coverage] + runs-on: ubuntu-24.04 + + steps: + - name: Close parallel build + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml new file mode 100644 index 0000000..9e8458b --- /dev/null +++ b/.github/workflows/daily.yaml @@ -0,0 +1,38 @@ +name: Daily Unit Tests + +on: + schedule: + - cron: '0 8 * * 1-5' + workflow_dispatch: + +concurrency: + group: daily-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Unit (py${{ matrix.python-version }}) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install testing dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e .[testing] + + - name: Run unit tests + run: python -m pytest tests/unit diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 106b2f6..5680da4 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -11,6 +11,33 @@ concurrency: cancel-in-progress: true jobs: + unit: + name: Unit (py${{ matrix.python-version }}) + runs-on: ubuntu-24.04 + timeout-minutes: 15 + + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ matrix.python-version }} + + - name: Install testing dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e .[testing] + + - name: Run unit tests + run: python -m pytest tests/unit + regression: name: Regression runs-on: ubuntu-24.04 @@ -78,4 +105,45 @@ jobs: name: docs-html path: docs/build/html/ if-no-files-found: error - retention-days: 7 \ No newline at end of file + retention-days: 7 + + coverage: + name: Coverage + needs: unit + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + permissions: + contents: read + checks: write + statuses: write + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python 3.14 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: "3.14" + + - name: Install testing dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e .[testing] + + - name: Run coverage + run: | + python -m pytest tests/unit \ + --cov sist \ + --cov-report term-missing \ + --cov-report xml \ + -q + + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: coverage.xml + fail-on-error: false \ No newline at end of file diff --git a/.github/workflows/update-copyright-years-in-license-file.yaml b/.github/workflows/update-copyright-years-in-license-file.yaml new file mode 100644 index 0000000..a252dd5 --- /dev/null +++ b/.github/workflows/update-copyright-years-in-license-file.yaml @@ -0,0 +1,17 @@ +name: Update copyright year(s) in license file + +on: + schedule: + - cron: '0 3 1 1 *' # 03:00 AM on January 1 + workflow_dispatch: + +jobs: + update-license-year: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + - uses: FantasticFiasco/action-update-license-year@f180e962fa988db222d8f03ef4636750312d1b3d # v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/weekly-docs.yaml b/.github/workflows/weekly-docs.yaml new file mode 100644 index 0000000..30d0eda --- /dev/null +++ b/.github/workflows/weekly-docs.yaml @@ -0,0 +1,47 @@ +name: Weekly Docs Build + +on: + schedule: + - cron: '0 8 * * 1' + workflow_dispatch: + +concurrency: + group: weekly-docs-${{ github.ref }} + cancel-in-progress: true + +jobs: + docs: + name: Docs build (python ${{ matrix.python-version }}) + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + timeout-minutes: 30 + + steps: + - name: Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r docs/requirements.txt + + - name: Build docs (warnings as errors) + run: | + make -C docs clean + make -C docs html SPHINXOPTS="-W --keep-going" + + - name: Upload docs artifacts on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: docs-py${{ matrix.python-version }}-failure + path: | + docs/build/** diff --git a/.github/workflows/regression-tests.yaml b/.github/workflows/weekly-regression.yaml similarity index 95% rename from .github/workflows/regression-tests.yaml rename to .github/workflows/weekly-regression.yaml index d046ca2..9d07a6d 100644 --- a/.github/workflows/regression-tests.yaml +++ b/.github/workflows/weekly-regression.yaml @@ -1,9 +1,6 @@ -name: Regression Tests +name: Weekly Regression Tests on: - push: - branches: - - main schedule: - cron: "0 8 * * 1" workflow_dispatch: diff --git a/.gitignore b/.gitignore index b855c85..4753c57 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,11 @@ src/trans_compete/depend # Debug information files *.dwo +# gcov/lcov coverage instrumentation and reports +*.gcno +*.gcda +coverage.info + # Build directories build/ dist/ @@ -75,6 +80,10 @@ __pycache__/ .ruff_cache/ *.egg-info/ +# Coverage +.coverage +coverage.xml + # Regression test artefacts .testdata/ test-results/ \ No newline at end of file diff --git a/README.md b/README.md index 651f0b0..217c1b8 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ SIST: Stress-Induced Structural Transitions in superhelical DNA | Category | Badges | |----------------|--------| -| **Build** | [![PR Checks](https://github.com/CCPBioSim/SIST/actions/workflows/pr.yaml/badge.svg)](https://github.com/CCPBioSim/SIST/actions/workflows/pr.yaml) | -| **Documentation** | [![Docs - Status](https://app.readthedocs.org/projects/sist/badge/?version=latest)](https://sist.readthedocs.io/en/latest/?badge=latest) | +| **Build** | [![PR Checks](https://github.com/CCPBioSim/SIST/actions/workflows/pr.yaml/badge.svg)](https://github.com/CCPBioSim/SIST/actions/workflows/pr.yaml) [![Daily Tests](https://github.com/CCPBioSim/SIST/actions/workflows/daily.yaml/badge.svg)](https://github.com/CCPBioSim/SIST/actions/workflows/daily.yaml) | +| **Regression** | [![Weekly Regression](https://github.com/CCPBioSim/SIST/actions/workflows/weekly-regression.yaml/badge.svg)](https://github.com/CCPBioSim/SIST/actions/workflows/weekly-regression.yaml) | +| **Documentation** | [![Weekly Docs](https://github.com/CCPBioSim/SIST/actions/workflows/weekly-docs.yaml/badge.svg)](https://github.com/CCPBioSim/SIST/actions/workflows/weekly-docs.yaml) [![Docs - Status](https://app.readthedocs.org/projects/sist/badge/?version=latest)](https://sist.readthedocs.io/en/latest/?badge=latest) | | **Citation** | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22753478.svg)](https://doi.org/10.5281/zenodo.22753478) | | **Anaconda** | [![Anaconda.org](https://anaconda.org/CCPBioSim/sist/badges/version.svg)](https://anaconda.org/CCPBioSim/sist/) [![Last Updated](https://anaconda.org/CCPBioSim/sist/badges/latest_release_date.svg)](https://anaconda.org/CCPBioSim/sist) [![Platforms](https://anaconda.org/CCPBioSim/sist/badges/platforms.svg)](https://anaconda.org/CCPBioSim/sist) [![License](https://anaconda.org/CCPBioSim/sist/badges/license.svg)](https://anaconda.org/CCPBioSim/sist) [![Downloads](https://anaconda.org/CCPBioSim/sist/badges/downloads.svg)](https://anaconda.org/CCPBioSim/sist)| | **Quality** | [![Coverage Status](https://coveralls.io/repos/github/CCPBioSim/SIST/badge.svg?branch=main)](https://coveralls.io/github/CCPBioSim/SIST?branch=main) | diff --git a/conda-recipe/run_test.sh b/conda-recipe/run_test.sh index 62d6569..8728dc5 100755 --- a/conda-recipe/run_test.sh +++ b/conda-recipe/run_test.sh @@ -6,4 +6,4 @@ command -v irf command -v python command -v pytest -python -m pytest tests -vv +python -m pytest tests/regression -vv diff --git a/pyproject.toml b/pyproject.toml index f2fab85..579aa2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ sist = "sist.cli:main" [project.optional-dependencies] testing = [ "pytest>=9.0,<10.0", + "pytest-cov>=7.0,<8.0", "mypy>=1.14,<2.0", ] diff --git a/src/trans_compete/Makefile b/src/trans_compete/Makefile index 703b15c..93b2108 100644 --- a/src/trans_compete/Makefile +++ b/src/trans_compete/Makefile @@ -54,6 +54,9 @@ tar: gcc: make $(APP) CC="$(CXX)" CPPFLAGS="-O2 -Wall" +coverage: + make $(APP) CC="$(CXX)" CPPFLAGS="-O0 -Wall --coverage -fprofile-abs-path" LDFLAGS="--coverage" + ################### # Inference Rules # diff --git a/src/trans_three/Makefile b/src/trans_three/Makefile index 06b0afe..1908f1d 100644 --- a/src/trans_three/Makefile +++ b/src/trans_three/Makefile @@ -54,6 +54,9 @@ tar: gcc: make $(APP) CC="$(CXX)" CPPFLAGS="-O2 -Wall " +coverage: + make $(APP) CC="$(CXX)" CPPFLAGS="-O0 -Wall --coverage -fprofile-abs-path" LDFLAGS="--coverage" + ################### # Inference Rules # diff --git a/tests/conftest.py b/tests/regression/conftest.py similarity index 97% rename from tests/conftest.py rename to tests/regression/conftest.py index 09632e9..d5854a7 100644 --- a/tests/conftest.py +++ b/tests/regression/conftest.py @@ -9,7 +9,7 @@ import pytest -REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] SIST_TRANSITIONS = ( pytest.param(("melting", "M"), id="melting"), @@ -172,7 +172,7 @@ def run_sist_calculation( runtime_directory = tmp_path_factory.mktemp(f"sist-{name}") - source_input = REPOSITORY_ROOT / "tests" / "data" / "pbr322.toy.fa" + source_input = Path(__file__).resolve().parent / "data" / "pbr322.toy.fa" runtime_input = runtime_directory / "pbr322.toy.fa" shutil.copy2(source_input, runtime_input) diff --git a/tests/data/pbr322.toy.fa b/tests/regression/data/pbr322.toy.fa similarity index 100% rename from tests/data/pbr322.toy.fa rename to tests/regression/data/pbr322.toy.fa diff --git a/tests/reference/v1.0.0/README.md b/tests/regression/reference/v1.0.0/README.md similarity index 100% rename from tests/reference/v1.0.0/README.md rename to tests/regression/reference/v1.0.0/README.md diff --git a/tests/reference/v1.0.0/competition.inherited.txt b/tests/regression/reference/v1.0.0/competition.inherited.txt similarity index 100% rename from tests/reference/v1.0.0/competition.inherited.txt rename to tests/regression/reference/v1.0.0/competition.inherited.txt diff --git a/tests/reference/v1.0.0/competition.rebuilt.txt b/tests/regression/reference/v1.0.0/competition.rebuilt.txt similarity index 100% rename from tests/reference/v1.0.0/competition.rebuilt.txt rename to tests/regression/reference/v1.0.0/competition.rebuilt.txt diff --git a/tests/reference/v1.0.0/cruciform.txt b/tests/regression/reference/v1.0.0/cruciform.txt similarity index 100% rename from tests/reference/v1.0.0/cruciform.txt rename to tests/regression/reference/v1.0.0/cruciform.txt diff --git a/tests/reference/v1.0.0/melting.txt b/tests/regression/reference/v1.0.0/melting.txt similarity index 100% rename from tests/reference/v1.0.0/melting.txt rename to tests/regression/reference/v1.0.0/melting.txt diff --git a/tests/reference/v1.0.0/z-dna.txt b/tests/regression/reference/v1.0.0/z-dna.txt similarity index 100% rename from tests/reference/v1.0.0/z-dna.txt rename to tests/regression/reference/v1.0.0/z-dna.txt diff --git a/tests/test_cli.py b/tests/regression/test_cli.py similarity index 100% rename from tests/test_cli.py rename to tests/regression/test_cli.py diff --git a/tests/test_legacy_entrypoints.py b/tests/regression/test_legacy_entrypoints.py similarity index 95% rename from tests/test_legacy_entrypoints.py rename to tests/regression/test_legacy_entrypoints.py index 4a9f141..9bbe9c6 100644 --- a/tests/test_legacy_entrypoints.py +++ b/tests/regression/test_legacy_entrypoints.py @@ -12,7 +12,6 @@ from pathlib import Path import pytest -from conftest import REPOSITORY_ROOT pytestmark = pytest.mark.regression @@ -41,7 +40,8 @@ def test_master_pl_alias_matches_sist_output( runtime_directory = tmp_path_factory.mktemp("master-pl-alias") input_path = runtime_directory / "pbr322.toy.fa" - shutil.copy2(REPOSITORY_ROOT / "tests" / "data" / "pbr322.toy.fa", input_path) + data_path = Path(__file__).resolve().parent / "data" / "pbr322.toy.fa" + shutil.copy2(data_path, input_path) result = subprocess.run( ["master.pl", "-f", input_path.name, "-a", "M", "-b", "-p", "-r"], diff --git a/tests/test_regression.py b/tests/regression/test_regression.py similarity index 98% rename from tests/test_regression.py rename to tests/regression/test_regression.py index 8ae18da..29d1e0a 100644 --- a/tests/test_regression.py +++ b/tests/regression/test_regression.py @@ -11,15 +11,9 @@ pytestmark = pytest.mark.regression -REPOSITORY_ROOT = Path(__file__).resolve().parents[1] REFERENCE_VERSION = "v1.0.0" -REFERENCE_DIRECTORY = ( - REPOSITORY_ROOT - / "tests" - / "reference" - / REFERENCE_VERSION -) +REFERENCE_DIRECTORY = Path(__file__).resolve().parent / "reference" / REFERENCE_VERSION COMPETITION_REFERENCE = ( REFERENCE_DIRECTORY diff --git a/tests/test_energetics.py b/tests/unit/test_energetics.py similarity index 100% rename from tests/test_energetics.py rename to tests/unit/test_energetics.py diff --git a/tests/test_ir_finder.py b/tests/unit/test_ir_finder.py similarity index 100% rename from tests/test_ir_finder.py rename to tests/unit/test_ir_finder.py