diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fffc758..d1e8118 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,11 +23,10 @@ jobs: - shell: bash -l {0} id: fetch-release run: | - python -m pip install packaging python fetch_release.py ${{ github.event.inputs.version }} - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2.2.0 if: steps.fetch-release.outputs.MICROMAMBA_NEW_VERSION == 'true' with: name: "micromamba ${{ steps.fetch-release.outputs.MICROMAMBA_VERSION }}" diff --git a/.github/workflows/test_fetch_release.yml b/.github/workflows/test_fetch_release.yml new file mode 100644 index 0000000..c102fce --- /dev/null +++ b/.github/workflows/test_fetch_release.yml @@ -0,0 +1,23 @@ +name: "Test fetching releases from Anaconda" + +on: + pull_request: + branches: + - main + +jobs: + test_fetch_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: mamba-org/setup-micromamba@v1 + with: + environment-file: environment.yml + create-args: pytest + - name: Add micromamba to GITHUB_PATH + run: echo "${HOME}/micromamba-bin" >> "$GITHUB_PATH" + + - shell: bash -l {0} + id: fetch-release + run: | + python -m pytest tests/test_fetch_release.py -v --exitfirst diff --git a/.github/workflows/test.yml b/.github/workflows/test_install.yml similarity index 100% rename from .github/workflows/test.yml rename to .github/workflows/test_install.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05bfce0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Python caches +*.pyc +.pytest_cache/ +__pycache__ + +# Generated files from tests +*.tar.bz2 diff --git a/environment.yml b/environment.yml index afedb0b..e7679e6 100644 --- a/environment.yml +++ b/environment.yml @@ -2,6 +2,7 @@ name: fetch-release-env channels: - conda-forge dependencies: + - packaging - python - requests - rich diff --git a/fetch_release.py b/fetch_release.py index 100efe2..0511d7f 100644 --- a/fetch_release.py +++ b/fetch_release.py @@ -72,6 +72,7 @@ def get_micromamba(version, use_default_version): build = max(all_build) print(f"Existing versions: {existing_tags}") + print(f"Checking {version}-{build}") if f"{version}-{build}" in existing_tags: print("Tag already exists, skipping") set_output("MICROMAMBA_NEW_VERSION", "false") @@ -150,7 +151,7 @@ def get_micromamba(version, use_default_version): else: set_output("MICROMAMBA_NEW_PRERELEASE", "true") - if is_stable and v.major == 2: + if is_stable and v.major >= 2: set_output("MICROMAMBA_LATEST", "true") else: set_output("MICROMAMBA_LATEST", "false") diff --git a/tests/test_fetch_release.py b/tests/test_fetch_release.py new file mode 100644 index 0000000..26e4ce1 --- /dev/null +++ b/tests/test_fetch_release.py @@ -0,0 +1,377 @@ +import hashlib +import pytest +import requests +import time + +from unittest.mock import patch, MagicMock + + +# TODO do this more elegantly? +# Put fetch_release in a py folder for example? +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import fetch_release + +@pytest.fixture(scope="module") +def retry_config(): + """ + Fixture to define retry configuration. + """ + return { + 'max_retries': 1, # TODO set to 3 later? + 'retry_delay': 5 # Delay between retries in seconds + } + +def get_output_value(name): + """ + Retrieve the value of a variable from the GITHUB_OUTPUT file. + """ + github_output_path = os.environ.get("GITHUB_OUTPUT") + + if github_output_path and os.path.exists(github_output_path): + with open(github_output_path, "r") as f: + for line in f: + # Each line is in the form 'name=value' + if line.startswith(name + "="): + return line.split("=", 1)[1].strip() # Return the value after '=' + return None + +def test_get_all_tags_github(retry_config): + """ + Test getting all GitHub tags using the GitHub API. + """ + max_retries = retry_config['max_retries'] + retry_delay = retry_config['retry_delay'] + + for _ in range(max_retries): + try: + tags = fetch_release.get_all_tags_github() + # Tags list for a minimal tags check + tags_to_check = ["2.0.5-0", "2.0.5.rc0-0", "1.5.12-0"] + assert isinstance(tags, set) + assert len(tags) >= 30 + assert set(tags_to_check).issubset(tags), f"Not all tags are in the set: {tags_to_check}" + print("Fetched GitHub tags:", tags) + return + except requests.exceptions.RequestException as e: + print(f"Error fetching tags, retrying... {e}") + time.sleep(retry_delay) + + pytest.fail("Failed to fetch GitHub tags after multiple retries.") + + +@pytest.mark.parametrize("version", ("latest", "2.0.5", "1.5.9", "2.0.5.rc0", "2.0.4alpha1")) +@pytest.mark.parametrize("use_default_version", (False, True)) +def test_get_micromamba_existing_version(retry_config, version, use_default_version): + """ + Test fetching existing micromamba stable version. + """ + max_retries = retry_config['max_retries'] + retry_delay = retry_config['retry_delay'] + + for _ in range(max_retries): + try: + fetch_release.get_micromamba(version, use_default_version) + assert get_output_value("MICROMAMBA_NEW_VERSION") == "false" + assert get_output_value("MICROMAMBA_NEW_PRERELEASE") == None + assert get_output_value("MICROMAMBA_LATEST") == None + assert get_output_value("MICROMAMBA_VERSION") == None + print(f"Fetched micromamba release {version} successfully.") + return + except requests.exceptions.RequestException as e: + print(f"Error fetching micromamba release, retrying... {e}") + time.sleep(retry_delay) + + pytest.fail(f"Failed to fetch micromamba release info after multiple retries.") + +@pytest.mark.parametrize("use_default_version", (False, True)) +def test_get_micromamba_non_existing_version(use_default_version): + """ + Test fetching non existing micromamba version. + """ + + with pytest.raises(requests.exceptions.HTTPError): + fetch_release.get_micromamba("9.10.5", use_default_version) + +#TODO mock test for non existing versions => new_version_1_x, new_version_2_x, new_prerelease + +@pytest.fixture +def mock_github_tags(): + """Mock GitHub tags to simulate existing tags.""" + with patch('fetch_release.get_all_tags_github') as mock: + mock.return_value = {"2.0.5-0"} + yield mock + +@pytest.fixture +def mock_anaconda_api(): + """Mock Anaconda.org API response.""" + with patch('fetch_release.requests.get') as mock: + # Simulate a new version available + mock.status_code = 200 + #mock.raise_for_status = MagicMock() + + mocked_content = b"some random binary data representing a tar.bz2 file" + mock.content = mocked_content + + sha256 = hashlib.sha256() + sha256.update(mocked_content) + computed_checksum = sha256.hexdigest() + + mock.json.return_value = { + "distributions": [ + { + "attrs": { + "subdir": "linux-64", + "build_number": 1 + }, + "download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/linux-64/micromamba-10.11.12-1.tar.bz2", + "sha256": computed_checksum, + "basename": "linux-64/micromamba-10.11.12-linux-64.tar.bz2", + "version": "10.11.12" + }, + { + "attrs": { + "subdir": "linux-aarch64", + "build_number": 1 + }, + "download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/linux-aarch64/micromamba-10.11.12-1.tar.bz2", + "sha256": computed_checksum, + "basename": "linux-aarch64/micromamba-10.11.12-linux-aarch64.tar.bz2", + "version": "10.11.12" + }, + { + "attrs": { + "subdir": "linux-ppc64le", + "build_number": 1 + }, + "download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/linux-ppc64le/micromamba-10.11.12-1.tar.bz2", + "sha256": computed_checksum, + "basename": "linux-ppc64le/micromamba-10.11.12-linux-ppc64le.tar.bz2", + "version": "10.11.12" + }, + { + "attrs": { + "subdir": "win-64", + "build_number": 1 + }, + "download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/win-64/micromamba-10.11.12-1.tar.bz2", + "sha256": computed_checksum, + "basename": "win-64/micromamba-10.11.12-win-64.tar.bz2", + "version": "10.11.12" + }, + { + "attrs": { + "subdir": "osx-64", + "build_number": 1 + }, + "download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/osx-64/micromamba-10.11.12-1.tar.bz2", + "sha256": computed_checksum, + "basename": "osx-64/micromamba-10.11.12-osx-64.tar.bz2", + "version": "10.11.12" + }, + { + "attrs": { + "subdir": "osx-arm64", + "build_number": 1 + }, + "download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/osx-arm64/micromamba-10.11.12-1.tar.bz2", + "sha256": computed_checksum, + "basename": "osx-arm64/micromamba-10.11.12-osx-arm64.tar.bz2", + "version": "10.11.12" + } + ] + } + + yield mock + + +@pytest.fixture +def mock_check_call(): + """Mock subprocess.check_call""" + with patch('fetch_release.subprocess.check_call') as mock: + mock.return_value = None + yield mock + +@pytest.fixture +def mock_copyfile(): + """Mock subprocess.check_call""" + with patch('fetch_release.shutil.copyfile') as mock: + #mock.return_value = None + def mock_copyfile_side_effect(*args, **kwargs): #src=None, dst=None, follow_symlinks=True): + # Simulate that the source file exists + # TODO use different versions and subdirs + #if src == 'micromamba-10.11.12-1-linux-64/bin/micromamba': + ## Simulate successful copy operation + #assert dst == 'releases/micromamba-linux-64' + #else: + if not args and not kwargs: + raise FileNotFoundError(f"File {src} not found") + + mock.side_effect = mock_copyfile_side_effect + + yield mock + +#@patch('requests.get') +#@patch("subprocess.check_call") +#@patch("shutil.copyfile") +def test_get_micromamba_new_2_x_version(mock_github_tags, mock_anaconda_api, mock_check_call, mock_copyfile): + # Create a mock response object for get_all_tags_github (GitHub API) + #mock_github_response = MagicMock() + #mock_github_response.status_code = 200 + #mock_github_response.raise_for_status = MagicMock() + #mock_github_response.json.return_value = [ + #{"name": "2.0.5-0"}, + #{"name": "latest"}, + #] + + # Create a mock response object for the Anaconda API (Anaconda API) + #mock_anaconda_response = MagicMock() + #mock_anaconda_response.status_code = 200 + #mock_anaconda_response.raise_for_status = MagicMock() + + ## Mock the response from the Anaconda API + #mock_response = MagicMock() + #mock_response.status_code = 200 + #mock_response.raise_for_status = MagicMock() + + # Mock request content to return a byte string + #mocked_content = b"some random binary data representing a tar.bz2 file" + #mock_response.content = mocked_content + + #sha256 = hashlib.sha256() + #sha256.update(mocked_content) + #computed_checksum = sha256.hexdigest() + + #mock_response.json.return_value = { + #"distributions": [ + #{ + #"attrs": { + #"subdir": "linux-64", + #"build_number": 1 + #}, + #"download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/linux-64/micromamba-10.11.12-1.tar.bz2", + #"sha256": computed_checksum, + #"basename": "linux-64/micromamba-10.11.12-linux-64.tar.bz2", + #"version": "10.11.12" + #}, + #{ + #"attrs": { + #"subdir": "linux-aarch64", + #"build_number": 1 + #}, + #"download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/linux-aarch64/micromamba-10.11.12-1.tar.bz2", + #"sha256": computed_checksum, + #"basename": "linux-aarch64/micromamba-10.11.12-linux-aarch64.tar.bz2", + #"version": "10.11.12" + #}, + #{ + #"attrs": { + #"subdir": "linux-ppc64le", + #"build_number": 1 + #}, + #"download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/linux-ppc64le/micromamba-10.11.12-1.tar.bz2", + #"sha256": computed_checksum, + #"basename": "linux-ppc64le/micromamba-10.11.12-linux-ppc64le.tar.bz2", + #"version": "10.11.12" + #}, + #{ + #"attrs": { + #"subdir": "win-64", + #"build_number": 1 + #}, + #"download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/win-64/micromamba-10.11.12-1.tar.bz2", + #"sha256": computed_checksum, + #"basename": "win-64/micromamba-10.11.12-win-64.tar.bz2", + #"version": "10.11.12" + #}, + #{ + #"attrs": { + #"subdir": "osx-64", + #"build_number": 1 + #}, + #"download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/osx-64/micromamba-10.11.12-1.tar.bz2", + #"sha256": computed_checksum, + #"basename": "osx-64/micromamba-10.11.12-osx-64.tar.bz2", + #"version": "10.11.12" + #}, + #{ + #"attrs": { + #"subdir": "osx-arm64", + #"build_number": 1 + #}, + #"download_url": "https://anaconda-api/conda-forge/micromamba/10.11.12/osx-arm64/micromamba-10.11.12-1.tar.bz2", + #"sha256": computed_checksum, + #"basename": "osx-arm64/micromamba-10.11.12-osx-arm64.tar.bz2", + #"version": "10.11.12" + #} + #] + #} + + # Use side_effect to simulate different responses for different requests + #def side_effect(url, timeout=10): + #if "github.com" in url: + #return mock_github_response + #elif "anaconda.org" in url: + #return mock_anaconda_response + #else: + #raise ValueError(f"Unexpected URL: {url}") + + # Set the side_effect to mock_get + #mock_get.side_effect = side_effect + + #mock_get.return_value = mock_response + + # Mock subprocess.check_call to prevent actual command execution + #mock_check_call.return_value = None # Simulate a successful call + + # Mock shutil.copyfile to prevent file copying + #mock_copyfile.return_value = None # Simulate successful copy + # Mock shutil.copyfile to simulate copying a valid file + #def mock_copyfile_side_effect(*args, **kwargs): #src=None, dst=None, follow_symlinks=True): + ## Simulate that the source file exists + ## TODO use different versions and subdirs + ##if src == 'micromamba-10.11.12-1-linux-64/bin/micromamba': + ### Simulate successful copy operation + ##assert dst == 'releases/micromamba-linux-64' + ##else: + #if not args and not kwargs: + #raise FileNotFoundError(f"File {src} not found") + + #mock_copyfile.side_effect = mock_copyfile_side_effect + + # Mock existing GitHub tags to simulate the version already being tagged + #with patch.object(fetch_release, 'get_all_tags_github', return_value={'2.0.5-0'}): + # Run the method with the mocked data + fetch_release.get_micromamba('10.11.12', False) + + # Check that the `requests.get` method was called as expected + #mock_get.assert_called_once_with("https://api.anaconda.org/release/conda-forge/micromamba/10.11.12") + + # Ensure that subprocess.check_call was called to extract the archive + #mock_check_call.assert_called_once_with( + #["micromamba", "package", "extract", "micromamba-10.11.12-1-linux-64.tar.bz2", "micromamba-10.11.12-1-linux-64"]) + + # Ensure shutil.copyfile is called with the expected paths + #mock_copyfile.assert_called_once_with( + #'micromamba-10.11.12-1-linux-64/bin/micromamba', 'releases/micromamba-linux-64' + #) + + + # Check that the `requests.get` method was called as expected + #mock_get.assert_called_once_with("https://api.anaconda.org/release/conda-forge/micromamba/10.11.12") + + # Ensure that subprocess.check_call was called to extract the archive + #mock_check_call.assert_called_once_with( + #["micromamba", "package", "extract", "micromamba-10.11.12-1-linux-64.tar.bz2", "micromamba-10.11.12-1-linux-64"]) + + # Ensure shutil.copyfile is called with the expected paths + #mock_copyfile.assert_called_once_with( + #'micromamba-10.11.12-1-linux-64/bin/micromamba', 'releases/micromamba-linux-64' + #) + + assert get_output_value("MICROMAMBA_NEW_VERSION") == "true" + + +