diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a1bd972 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: Run Tests + +on: + - push + - pull_request + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + + steps: + - uses: actions/checkout@v4 + - name: Set up git-annex + run: | + sudo apt-get update + sudo apt-get install -y git-annex + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install tox tox-gh-actions + - name: Configure Git for DataLad + run: | + git config --global user.email "junifer-runner@github.com" + git config --global user.name "GitHub Runner" + + - name: Test with tox + run: | + tox + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + flags: junifer + if: success() && matrix.python-version == 3.12 diff --git a/changelog.d/4.added.md b/changelog.d/4.added.md new file mode 100644 index 0000000..40fd7ac --- /dev/null +++ b/changelog.d/4.added.md @@ -0,0 +1 @@ +Add unit tests and CI workflow diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..92b8a3f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,14 @@ +codecov: + notify: {} + require_ci_to_pass: false + +comment: # this is a top-level key + layout: "reach, diff, flags, files" + behavior: default + require_changes: false # if true: only post the comment if coverage changes + require_base: no # [yes :: must have a base report to post] + +coverage: + status: + project: off + patch: off diff --git a/junifer_data/_utils.py b/junifer_data/_utils.py index f7ae929..c4d27d3 100644 --- a/junifer_data/_utils.py +++ b/junifer_data/_utils.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -def check_dataset( # noqa: C901 +def check_dataset( data_dir: Union[str, Path, None] = None, tag: Optional[str] = None, hexsha: Optional[str] = None, diff --git a/junifer_data/tests/test_utils.py b/junifer_data/tests/test_utils.py new file mode 100644 index 0000000..a570b93 --- /dev/null +++ b/junifer_data/tests/test_utils.py @@ -0,0 +1,92 @@ +"""Tests for junifer_data.utils.""" + +from pathlib import Path + +import pytest + +from junifer_data import check_dataset + + +def test_check_dataset_main(tmp_path: Path) -> None: + """Test check_dataset with main tag. + + Parameters + ---------- + tmp_path : pathlib.Path + Pytest fixture that provides a temporary directory. + + """ + dataset = check_dataset(data_dir=tmp_path) + assert dataset.pathobj.name == "main" + + # Check-out again, should update + check_dataset(data_dir=tmp_path) + + +def test_check_dataset_tag_errors(tmp_path: Path) -> None: + """Test check_dataset hexsha errors. + + Parameters + ---------- + tmp_path : pathlib.Path + Pytest fixture that provides a temporary directory. + + """ + with pytest.raises(RuntimeError, match="Failed to checkout state"): + check_dataset(data_dir=tmp_path, tag="wrong") + + +def test_check_dataset_hexsha_errors(tmp_path: Path) -> None: + """Test check_dataset hexsha errors. + + Parameters + ---------- + tmp_path : pathlib.Path + Pytest fixture that provides a temporary directory. + + """ + with pytest.raises(ValueError, match="Cannot verify hexsha for main tag."): + check_dataset(data_dir=tmp_path, hexsha="wrong") + + # Now clone the dataset without checking + check_dataset(data_dir=tmp_path, tag="1") + + with pytest.raises(ValueError, match="Commit verification failed."): + check_dataset(data_dir=tmp_path, tag="1", hexsha="wrong") + + # Check with the right hexsha + dataset = check_dataset( + data_dir=tmp_path, + tag="1", + hexsha="e9aecf7b5a2fff82de00d265e02afde42a448647", + ) + + ds_path = tmp_path / "v1" + + with open(ds_path / "test.txt", "w") as f: + f.write("test") + + with pytest.raises(RuntimeError, match="dirty junifer-data"): + check_dataset(data_dir=tmp_path, tag="1") + + # We will now update the tag + dataset.repo.add((ds_path / "test.txt").as_posix()) + dataset.repo.commit(msg="update") + + with pytest.raises(ValueError, match="Wrong commit checked out."): + check_dataset(data_dir=tmp_path, tag="1") + + # Update tag + dataset.repo.tag("v1", options=["-d"]) + dataset.repo.tag("v1") + + # Does not fail + check_dataset(data_dir=tmp_path, tag="1") + + # But does not have the right hexsha + with pytest.raises(ValueError, match="Commit verification failed."): + check_dataset( + data_dir=tmp_path, + tag="1", + hexsha="e9aecf7b5a2fff82de00d265e02afde42a448647", + ) diff --git a/pyproject.toml b/pyproject.toml index 1c1e8c8..ab839b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,8 @@ ignore = [ "D107", # use specific rule codes when ignoring type issues "PGH003", + # ignore too complex expressions + "C901", ] [tool.ruff.lint.isort] diff --git a/tox.ini b/tox.ini index 8794b35..c49a440 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,26 @@ requires = tox>=4 env_list = ruff, - changelog + changelog, + coverage, + py3{9,10,11,12,13} + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: coverage + 3.13: py313 + +[testenv] +skip_install = false +passenv = + HOME +deps = + pytest +commands = + pytest [testenv:ruff] description = run ruff @@ -21,3 +40,33 @@ deps = towncrier commands = towncrier build --draft + +[testenv:coverage] +skip_install = false +deps = + pytest-cov +commands = + pytest --cov={envsitepackagesdir}/junifer_data --cov-report=xml --cov-report=term {envsitepackagesdir}/junifer_data + +[coverage:paths] +source = + junifer_data + */site-packages/junifer_data + +[coverage:run] +branch = true +omit = + */setup.py + */_version.py + */tests/* +parallel = false + +[coverage:report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + # Type checking if statements should not be considered + if TYPE_CHECKING: + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: +precision = 2