diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index be2ca27..8c3f04a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,8 @@ on: paths: - 'src/**' - 'tests/**' + release: + types: [published] workflow_dispatch: jobs: @@ -30,5 +32,30 @@ jobs: - name: Install test dependencies run: pip install -e '.[dev]' - - name: Run Tests - run: pytest -v tests + - name: Run unit & integration tests (exclude e2e) + run: pytest -v --ignore=tests/e2e tests + + run-e2e: + needs: run-tests + timeout-minutes: 30 + runs-on: ubuntu-latest + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main') || + (github.event_name == 'release' && github.event.action == 'published') + + steps: + - name: Check out the repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + cache: "pip" + + - name: Install test dependencies + run: pip install -e '.[dev]' + + - name: Run e2e tests + run: pytest -q tests/e2e/test_e2e.py diff --git a/docs/index.md b/docs/index.md index 6f5c6f1..cab67fd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -123,10 +123,20 @@ print(info) For a detailed API reference, see the [API Reference](api.md) section of the documentation. -## Release Workflow - > [!NOTE] > This section is intended for maintainers of the `datacollective` library. +## Tests + +Run the full test suite: +```bash +pytest -v +``` + +Note that the e2e tests require a valid `MDC_TEST_API_KEY` and a `MDC_TEST_API_URL` key set in your environment. Pytest will skip the live e2e tests automatically if either is missing. + + +## Release Workflow + Check out the [Release Workflow](release.md) document for details on how to publish new versions of the library to PyPI using GitHub Actions. \ No newline at end of file diff --git a/src/datacollective/datasets.py b/src/datacollective/datasets.py index b0b95ca..0422bed 100644 --- a/src/datacollective/datasets.py +++ b/src/datacollective/datasets.py @@ -7,6 +7,7 @@ from typing import Any import pandas as pd +from fox_progress_bar import ProgressBar from datacollective.api_utils import ( ENV_DOWNLOAD_PATH, @@ -17,7 +18,6 @@ from datacollective.dataset_loading_scripts.registry import ( load_dataset_from_name_as_dataframe, ) -from fox_progress_bar import ProgressBar def get_dataset_details(dataset_id: str) -> dict[str, Any]: diff --git a/tests/dataset_loading_scripts/__init__.py b/tests/dataset_loading_scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dataset_loading_scripts/test_registry.py b/tests/dataset_loading_scripts/test_registry.py new file mode 100644 index 0000000..db73dcb --- /dev/null +++ b/tests/dataset_loading_scripts/test_registry.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import pytest +from _pytest.monkeypatch import MonkeyPatch + +from datacollective.dataset_loading_scripts import registry + + +def test_load_dataset_routes_to_scripted( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + sentinel = object() + + def fake_loader(extract_dir: Path) -> object: + assert extract_dir == tmp_path + return sentinel + + monkeypatch.setattr(registry, "_load_scripted", fake_loader) + result = registry.load_dataset_from_name_as_dataframe( + "common voice scripted", tmp_path + ) + assert result is sentinel + + +def test_load_dataset_routes_to_spontaneous( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + sentinel = object() + + def fake_loader(extract_dir: Path) -> object: + assert extract_dir == tmp_path + return sentinel + + monkeypatch.setattr(registry, "_load_spontaneous", fake_loader) + result = registry.load_dataset_from_name_as_dataframe( + "common voice spontaneous", tmp_path + ) + assert result is sentinel + + +def test_load_dataset_invalid_name(tmp_path: Path) -> None: + with pytest.raises(ValueError): + registry.load_dataset_from_name_as_dataframe("unknown dataset", tmp_path) diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py new file mode 100644 index 0000000..9d1d4d7 --- /dev/null +++ b/tests/e2e/test_e2e.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path + +import pandas as pd +import pytest +from _pytest.monkeypatch import MonkeyPatch +from requests import HTTPError + +from datacollective import get_dataset_details, load_dataset + +MDC_TEST_API_KEY = os.getenv("MDC_TEST_API_KEY") +MDC_TEST_API_URL = os.getenv("MDC_TEST_API_URL") + +pytestmark = pytest.mark.skipif( + not (MDC_TEST_API_KEY and MDC_TEST_API_URL), + reason="Set MDC_TEST_API_KEY and MDC_TEST_API_URL to run live API tests.", +) + + +def _skip_if_rate_limited(exc: Exception) -> None: + """ + Since our backend implements strict rate limiting + there is a chance that our e2e tests might hit it, + so we skip the tests when backend returns HTTP 429 (rate limit).""" + if isinstance(exc, HTTPError) and getattr(exc, "response", None): + if getattr(exc.response, "status_code", None) == 429: + pytest.skip("Skipped due to API rate limiting (HTTP 429)") + raise exc + + +def test_get_dataset_details_live_api( + monkeypatch: MonkeyPatch, + dataset_id: str = "cmiq2s3q5000fo207k9g6g7ou", +) -> None: + """NOTE: This test calls a live MDC API endpoint (dev).""" + monkeypatch.setenv("MDC_API_KEY", MDC_TEST_API_KEY) + monkeypatch.setenv("MDC_API_URL", MDC_TEST_API_URL) + + try: + details = get_dataset_details(dataset_id) + except Exception as exc: + _skip_if_rate_limited(exc) + + assert isinstance(details, dict) + assert details.get("id") == dataset_id + assert isinstance(details.get("name"), str) and details["name"].strip() + + +def test_load_dataset_live_api( + tmp_path: Path, + monkeypatch: MonkeyPatch, + dataset_id: str = "cmiq2s3q5000fo207k9g6g7ou", +) -> None: + """NOTE: This test calls a live MDC API endpoint (dev).""" + + monkeypatch.setenv("MDC_API_KEY", MDC_TEST_API_KEY) + monkeypatch.setenv("MDC_DOWNLOAD_PATH", str(tmp_path)) + monkeypatch.setenv("MDC_API_URL", MDC_TEST_API_URL) + + try: + df = load_dataset( + dataset_id, + download_directory=str(tmp_path), + show_progress=False, + overwrite_existing=True, + ) + except Exception as exc: # noqa: BLE001 + _skip_if_rate_limited(exc) + + assert isinstance(df, pd.DataFrame) + assert not df.empty + assert len(df.columns) > 0 diff --git a/tests/test_datasets.py b/tests/test_datasets.py new file mode 100644 index 0000000..0dff864 --- /dev/null +++ b/tests/test_datasets.py @@ -0,0 +1,35 @@ +from pathlib import Path + +from _pytest.monkeypatch import MonkeyPatch + +from datacollective.datasets import _resolve_download_dir, _strip_archive_suffix + + +def test_strip_archive_suffix_removes_known_extensions(tmp_path: Path) -> None: + tar_path = tmp_path / "sample.tar.gz" + zip_path = tmp_path / "sample.zip" + + assert _strip_archive_suffix(tar_path).name == "sample" + assert _strip_archive_suffix(zip_path).name == "sample" + + +def test_resolve_download_dir_prefers_argument( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.delenv("MDC_DOWNLOAD_PATH", raising=False) + custom_dir = tmp_path / "custom" + resolved = _resolve_download_dir(str(custom_dir)) + + assert resolved == custom_dir + assert custom_dir.exists() + + +def test_resolve_download_dir_uses_env_default( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: + env_dir = tmp_path / "env" + monkeypatch.setenv("MDC_DOWNLOAD_PATH", str(env_dir)) + resolved = _resolve_download_dir(None) + + assert resolved == env_dir + assert env_dir.exists()