diff --git a/.dockerignore b/.dockerignore index 0367979..bf7552c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,5 +3,4 @@ INCHI-1-TEST/data/ INCHI-1-TEST/docs/ INCHI-1-TEST/libs/ INCHI-1-TEST/exes/ -INCHI-1-TEST/tests/ **/__pycache__/ \ No newline at end of file diff --git a/.github/actions/compile_inchi_exe/action.yml b/.github/actions/compile_inchi_exe/action.yml new file mode 100644 index 0000000..121b17b --- /dev/null +++ b/.github/actions/compile_inchi_exe/action.yml @@ -0,0 +1,13 @@ +name: Compile InChI executable from triggering commit + +runs: + using: "composite" + steps: + - run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" # https://github.com/actions/runner-images/issues/6775 + mkdir "$GITHUB_WORKSPACE/$EXE_DIR" + ./INCHI-1-TEST/compile_inchi.sh $COMMIT "$GITHUB_WORKSPACE/$EXE_DIR" exe + shell: bash + env: + COMMIT: ${{ github.sha }} + EXE_DIR: INCHI-1-TEST/exes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d74c0ad..df0b209 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,11 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e INCHI-1-TEST[invariance-tests] + + - uses: ./.github/actions/compile_inchi_exe + - name: Run executable tests + run: pytest INCHI-1-TEST/tests/test_executable + - uses: ./.github/actions/compile_inchi_lib - uses: ./.github/actions/regression_tests with: @@ -41,7 +46,7 @@ jobs: steps: - name: Install build and test environment run: | - apk add bash git musl-dev gcc make python3 py-pip + apk add bash git musl-dev gcc g++ make python3 py-pip # We need to install git before checking out the repository. # Otherwise, the repository will be downloaded using the GitHub REST API instead of git. - uses: actions/checkout@v4 @@ -59,6 +64,11 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -e INCHI-1-TEST + + - uses: ./.github/actions/compile_inchi_exe + - name: Run executable tests + run: pytest INCHI-1-TEST/tests/test_executable + - uses: ./.github/actions/compile_inchi_lib - uses: ./.github/actions/regression_tests with: diff --git a/INCHI-1-TEST/Dockerfile b/INCHI-1-TEST/Dockerfile index abdd1be..b483f13 100644 --- a/INCHI-1-TEST/Dockerfile +++ b/INCHI-1-TEST/Dockerfile @@ -32,6 +32,13 @@ RUN mkdir $lib_dir && for version in $inchi_versions; do \ /inchi/INCHI-1-TEST/compile_inchi.sh "$version" "$lib_dir" lib || exit 1; \ done +ENV exe_dir="/inchi/INCHI-1-TEST/exes" +RUN for version in $inchi_versions; do \ + version_dir="${exe_dir}/${version}"; \ + mkdir -p $version_dir; \ + /inchi/INCHI-1-TEST/compile_inchi.sh "$version" "$version_dir" exe || exit 1; \ + done + FROM gcc:14-bookworm AS inchi_test WORKDIR /inchi @@ -39,6 +46,8 @@ WORKDIR /inchi # Include only what's necessary for running the tests. COPY --from=inchi_compilation /inchi/INCHI-1-TEST/src /inchi/INCHI-1-TEST/src COPY --from=inchi_compilation /inchi/INCHI-1-TEST/libs /inchi/INCHI-1-TEST/libs +COPY --from=inchi_compilation /inchi/INCHI-1-TEST/exes /inchi/INCHI-1-TEST/exes +COPY --from=inchi_compilation /inchi/INCHI-1-TEST/tests /inchi/INCHI-1-TEST/tests COPY --from=inchi_compilation /inchi/INCHI-1-TEST/pyproject.toml /inchi/INCHI-1-TEST/pyproject.toml COPY --from=inchi_compilation /inchi/INCHI-1-TEST/install.sh /inchi/INCHI-1-TEST/install.sh diff --git a/INCHI-1-TEST/install.sh b/INCHI-1-TEST/install.sh index 8eeb711..3655ed7 100755 --- a/INCHI-1-TEST/install.sh +++ b/INCHI-1-TEST/install.sh @@ -2,6 +2,6 @@ apt update && apt install -y python3-pip cmake python3 -m pip install --upgrade --break-system-packages pip -python3 -m pip install --break-system-packages -e .[invariance-tests,development] +python3 -m pip install --break-system-packages -e .[invariance-tests] # Make `python3` available as `python`. update-alternatives --install /usr/bin/python python /usr/bin/python3 10 diff --git a/INCHI-1-TEST/pyproject.toml b/INCHI-1-TEST/pyproject.toml index cbcf745..c7fd3d3 100644 --- a/INCHI-1-TEST/pyproject.toml +++ b/INCHI-1-TEST/pyproject.toml @@ -2,12 +2,11 @@ name = "inchi_tests" version = "1.0.0" requires-python = ">=3.11" -dependencies = ["pydantic == 2.7.1"] +dependencies = ["pydantic == 2.7.1", "pytest == 8.3.3"] [project.optional-dependencies] # FIXME: We're forcing numpy major version for now until https://github.com/kuelumbus/rdkit-pypi/issues/102 is resolved. invariance-tests = ["rdkit == 2023.9.6", "numpy < 2.0.0"] -development = ["pytest"] [project.scripts] run-tests = "inchi_tests.run_tests:main" diff --git a/INCHI-1-TEST/tests/test_executable/README.md b/INCHI-1-TEST/tests/test_executable/README.md new file mode 100644 index 0000000..52e159b --- /dev/null +++ b/INCHI-1-TEST/tests/test_executable/README.md @@ -0,0 +1,19 @@ +# Executable tests + +Test specific behaviors of the executable. +Ensure that specific input (molfile and arguments) elicits specific output (e.g., error). + +Where possible, stick to the following principles: +- Define input inline rather than using external files (having entire test scenario on screen is nice). +- Keep number of tests per file to a minimum. +- When replicating a bug, reference the (GitHub) issue number in the file name (e.g., `test_github_42.py`). + +Have a look at the existing tests for examples on how to write a test. + +Run with `pytest INCHI-1-TEST/tests/test_executable`. +Note that by default, the tests expect the InChI executable to live at `INCHI-1-TEST/exes/inchi-1`. +You can specify another InChI executable like so: + +```shell +pytest INCHI-1-TEST/tests/test_executable --exe-path=path/to/executable +``` diff --git a/INCHI-1-TEST/tests/test_executable/conftest.py b/INCHI-1-TEST/tests/test_executable/conftest.py new file mode 100644 index 0000000..a643a9c --- /dev/null +++ b/INCHI-1-TEST/tests/test_executable/conftest.py @@ -0,0 +1,31 @@ +import pytest +import subprocess +from typing import Callable +from pathlib import Path + + +def pytest_addoption(parser): + parser.addoption( + "--exe-path", + action="store", + default="INCHI-1-TEST/exes/inchi-1", + help="Absolute path to the InChI executable.", + ) + + +@pytest.fixture +def run_inchi_exe(request) -> Callable: + def _run_inchi_exe( + molfile_path: str, args: str = "" + ) -> subprocess.CompletedProcess: + + exe_path: str = request.config.getoption("--exe-path") + if not Path(exe_path).exists(): + raise FileNotFoundError(f"InChI executable not found at {exe_path}.") + + return subprocess.run( + [exe_path, molfile_path, args], + capture_output=True, + ) + + return _run_inchi_exe diff --git a/INCHI-1-TEST/tests/test_executable/helpers.py b/INCHI-1-TEST/tests/test_executable/helpers.py new file mode 100644 index 0000000..d216852 --- /dev/null +++ b/INCHI-1-TEST/tests/test_executable/helpers.py @@ -0,0 +1,18 @@ +from typing import Callable +from pathlib import Path + + +def tmp_molfile(molfile: Callable) -> Callable: + """ + Creates a temporary molfile and returns its path. + + `molfile` must return a string. + """ + + def get_molfile_path(tmp_path: Path) -> str: + molfile_path = tmp_path.joinpath("tmp.mol") + molfile_path.write_text(molfile()) + + return str(molfile_path) + + return get_molfile_path diff --git a/INCHI-1-TEST/tests/test_executable/test_github_40.py b/INCHI-1-TEST/tests/test_executable/test_github_40.py new file mode 100644 index 0000000..5fca58a --- /dev/null +++ b/INCHI-1-TEST/tests/test_executable/test_github_40.py @@ -0,0 +1,113 @@ +import pytest +from helpers import tmp_molfile + + +@pytest.fixture +@tmp_molfile +def molfile(): + return """(R)-SDP + ChemDraw08122419562D + + 43 50 0 0 1 0 0 0 0 0999 V2000 + -0.7846 -1.0799 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.4991 -1.4924 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.2136 -1.0799 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.2136 -0.2549 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.4991 0.1576 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.7813 0.9328 0.0000 P 0 0 0 0 0 0 0 0 0 0 0 0 + -1.2510 1.5648 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.5331 2.3400 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.0028 2.9720 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.1904 2.8288 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0918 2.0535 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4385 1.4215 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.5937 1.0761 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -3.1240 0.4441 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -3.9365 0.5873 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -4.2187 1.3626 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -3.6884 1.9946 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.8759 1.8513 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.7846 -0.2549 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4849 0.6674 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0000 1.3349 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.7846 1.0799 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.7846 0.2549 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.4991 -0.1576 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.7813 -0.9328 0.0000 P 0 0 0 0 0 0 0 0 0 0 0 0 + 2.5937 -1.0761 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.8759 -1.8513 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.6884 -1.9946 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.2187 -1.3626 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.9365 -0.5873 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.1240 -0.4441 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.2510 -1.5648 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.4385 -1.4215 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0918 -2.0535 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.1904 -2.8288 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.0028 -2.9720 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.5331 -2.3400 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2136 0.2549 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2136 1.0799 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.4991 1.4924 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.4849 -0.6674 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0000 -1.3349 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 2 0 0 + 2 3 1 0 0 + 3 4 2 0 0 + 4 5 1 0 0 + 5 6 1 0 0 + 6 7 1 0 0 + 7 8 2 0 0 + 8 9 1 0 0 + 9 10 2 0 0 + 10 11 1 0 0 + 11 12 2 0 0 + 7 12 1 0 0 + 6 13 1 0 0 + 13 14 2 0 0 + 14 15 1 0 0 + 15 16 2 0 0 + 16 17 1 0 0 + 17 18 2 0 0 + 13 18 1 0 0 + 5 19 2 0 0 + 1 19 1 0 0 + 20 19 1 1 0 + 20 21 1 0 0 + 21 22 1 0 0 + 22 23 1 0 0 + 23 24 2 0 0 + 20 24 1 0 0 + 24 25 1 0 0 + 25 26 1 0 0 + 26 27 1 0 0 + 27 28 2 0 0 + 28 29 1 0 0 + 29 30 2 0 0 + 30 31 1 0 0 + 31 32 2 0 0 + 27 32 1 0 0 + 26 33 1 0 0 + 33 34 2 0 0 + 34 35 1 0 0 + 35 36 2 0 0 + 36 37 1 0 0 + 37 38 2 0 0 + 33 38 1 0 0 + 25 39 2 0 0 + 39 40 1 0 0 + 40 41 2 0 0 + 23 41 1 0 0 + 20 42 1 6 0 + 42 43 1 0 0 + 1 43 1 0 0 +M END +""" + + +@pytest.mark.xfail(strict=True, raises=AssertionError) +def test_spiro_compound_chiral(molfile, run_inchi_exe): + result = run_inchi_exe(molfile) + + assert "Warning (Not chiral) structure #1." not in result.stderr.decode() diff --git a/INCHI-1-TEST/tests/test_executable/test_github_52.py b/INCHI-1-TEST/tests/test_executable/test_github_52.py new file mode 100644 index 0000000..48d3af7 --- /dev/null +++ b/INCHI-1-TEST/tests/test_executable/test_github_52.py @@ -0,0 +1,57 @@ +import pytest +from helpers import tmp_molfile + + +@pytest.fixture +@tmp_molfile +def molfile_empty_bondblock(): + return """ + -INDIGO-08292417452D + + 0 0 0 0 0 0 0 0 0 0 0 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 1 0 0 0 0 +M V30 BEGIN ATOM +M V30 1 C 9.35 -4.8 0.0 0 +M V30 END ATOM +M V30 BEGIN BOND +M V30 END BOND +M V30 END CTAB +M END +""" + + +@pytest.fixture +@tmp_molfile +def molfile_no_bondblock(): + return """ + -INDIGO-08292417452D + + 0 0 0 0 0 0 0 0 0 0 0 V3000 +M V30 BEGIN CTAB +M V30 COUNTS 1 0 0 0 0 +M V30 BEGIN ATOM +M V30 1 C 9.35 -4.8 0.0 0 +M V30 END ATOM +M V30 END CTAB +M END +""" + + +@pytest.mark.xfail(strict=True, raises=AssertionError) +def test_empty_bondblock(molfile_empty_bondblock, run_inchi_exe): + result = run_inchi_exe(molfile_empty_bondblock) + + assert ( + "Error 71 (no InChI; Error: No V3000 CTAB end marker) inp structure #1." + not in result.stderr.decode() + ) + + +def test_no_bondblock(molfile_no_bondblock, run_inchi_exe): + result = run_inchi_exe(molfile_no_bondblock) + + assert ( + "Error 71 (no InChI; Error: No V3000 CTAB end marker) inp structure #1." + not in result.stderr.decode() + )