diff options
author | Harmen Stoppels <me@harmenstoppels.nl> | 2024-08-14 10:15:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-14 08:15:15 +0000 |
commit | 7b10aae3560c6b9f0277b498be79dbe536e708c4 (patch) | |
tree | 3e2535d9098a80a6ed1fbab138578a48d5232a0b /lib | |
parent | b61cd74707fb37bb09f322060fb123b664d7ebe6 (diff) | |
download | spack-7b10aae3560c6b9f0277b498be79dbe536e708c4.tar.gz spack-7b10aae3560c6b9f0277b498be79dbe536e708c4.tar.bz2 spack-7b10aae3560c6b9f0277b498be79dbe536e708c4.tar.xz spack-7b10aae3560c6b9f0277b498be79dbe536e708c4.zip |
Show underlying errors on fetch failure (#45714)
- unwrap/flatten nested exceptions
- improve tests
- unify curl lookup
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/spack/fetch_strategy.py | 101 | ||||
-rw-r--r-- | lib/spack/spack/stage.py | 34 | ||||
-rw-r--r-- | lib/spack/spack/test/config.py | 1 | ||||
-rw-r--r-- | lib/spack/spack/test/conftest.py | 8 | ||||
-rw-r--r-- | lib/spack/spack/test/packaging.py | 15 | ||||
-rw-r--r-- | lib/spack/spack/test/stage.py | 25 | ||||
-rw-r--r-- | lib/spack/spack/test/url_fetch.py | 187 | ||||
-rw-r--r-- | lib/spack/spack/test/web.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/util/web.py | 65 |
9 files changed, 198 insertions, 240 deletions
diff --git a/lib/spack/spack/fetch_strategy.py b/lib/spack/spack/fetch_strategy.py index 4aa7f339de..4bbc143fc9 100644 --- a/lib/spack/spack/fetch_strategy.py +++ b/lib/spack/spack/fetch_strategy.py @@ -30,6 +30,7 @@ import re import shutil import urllib.error import urllib.parse +import urllib.request from pathlib import PurePath from typing import List, Optional @@ -273,10 +274,7 @@ class URLFetchStrategy(FetchStrategy): @property def curl(self): if not self._curl: - try: - self._curl = which("curl", required=True) - except CommandNotFoundError as exc: - tty.error(str(exc)) + self._curl = web_util.require_curl() return self._curl def source_id(self): @@ -297,27 +295,23 @@ class URLFetchStrategy(FetchStrategy): @_needs_stage def fetch(self): if self.archive_file: - tty.debug("Already downloaded {0}".format(self.archive_file)) + tty.debug(f"Already downloaded {self.archive_file}") return - url = None - errors = [] + errors: List[Exception] = [] for url in self.candidate_urls: - if not web_util.url_exists(url): - tty.debug("URL does not exist: " + url) - continue - try: self._fetch_from_url(url) break except FailedDownloadError as e: - errors.append(str(e)) - - for msg in errors: - tty.debug(msg) + errors.extend(e.exceptions) + else: + raise FailedDownloadError(*errors) if not self.archive_file: - raise FailedDownloadError(url) + raise FailedDownloadError( + RuntimeError(f"Missing archive {self.archive_file} after fetching") + ) def _fetch_from_url(self, url): if spack.config.get("config:url_fetch_method") == "curl": @@ -336,19 +330,20 @@ class URLFetchStrategy(FetchStrategy): @_needs_stage def _fetch_urllib(self, url): save_file = self.stage.save_filename - tty.msg("Fetching {0}".format(url)) - # Run urllib but grab the mime type from the http headers + request = urllib.request.Request(url, headers={"User-Agent": web_util.SPACK_USER_AGENT}) + try: - url, headers, response = web_util.read_from_url(url) - except web_util.SpackWebError as e: + response = web_util.urlopen(request) + except (TimeoutError, urllib.error.URLError) as e: # clean up archive on failure. if self.archive_file: os.remove(self.archive_file) if os.path.lexists(save_file): os.remove(save_file) - msg = "urllib failed to fetch with error {0}".format(e) - raise FailedDownloadError(url, msg) + raise FailedDownloadError(e) from e + + tty.msg(f"Fetching {url}") if os.path.lexists(save_file): os.remove(save_file) @@ -356,7 +351,7 @@ class URLFetchStrategy(FetchStrategy): with open(save_file, "wb") as _open_file: shutil.copyfileobj(response, _open_file) - self._check_headers(str(headers)) + self._check_headers(str(response.headers)) @_needs_stage def _fetch_curl(self, url): @@ -365,7 +360,7 @@ class URLFetchStrategy(FetchStrategy): if self.stage.save_filename: save_file = self.stage.save_filename partial_file = self.stage.save_filename + ".part" - tty.msg("Fetching {0}".format(url)) + tty.msg(f"Fetching {url}") if partial_file: save_args = [ "-C", @@ -405,8 +400,8 @@ class URLFetchStrategy(FetchStrategy): try: web_util.check_curl_code(curl.returncode) - except spack.error.FetchError as err: - raise spack.fetch_strategy.FailedDownloadError(url, str(err)) + except spack.error.FetchError as e: + raise FailedDownloadError(e) from e self._check_headers(headers) @@ -560,7 +555,7 @@ class OCIRegistryFetchStrategy(URLFetchStrategy): os.remove(self.archive_file) if os.path.lexists(file): os.remove(file) - raise FailedDownloadError(self.url, f"Failed to fetch {self.url}: {e}") from e + raise FailedDownloadError(e) from e if os.path.lexists(file): os.remove(file) @@ -1312,35 +1307,41 @@ class S3FetchStrategy(URLFetchStrategy): @_needs_stage def fetch(self): if self.archive_file: - tty.debug("Already downloaded {0}".format(self.archive_file)) + tty.debug(f"Already downloaded {self.archive_file}") return parsed_url = urllib.parse.urlparse(self.url) if parsed_url.scheme != "s3": raise spack.error.FetchError("S3FetchStrategy can only fetch from s3:// urls.") - tty.debug("Fetching {0}".format(self.url)) - basename = os.path.basename(parsed_url.path) + request = urllib.request.Request( + self.url, headers={"User-Agent": web_util.SPACK_USER_AGENT} + ) with working_dir(self.stage.path): - _, headers, stream = web_util.read_from_url(self.url) + try: + response = web_util.urlopen(request) + except (TimeoutError, urllib.error.URLError) as e: + raise FailedDownloadError(e) from e + + tty.debug(f"Fetching {self.url}") with open(basename, "wb") as f: - shutil.copyfileobj(stream, f) + shutil.copyfileobj(response, f) - content_type = web_util.get_header(headers, "Content-type") + content_type = web_util.get_header(response.headers, "Content-type") if content_type == "text/html": warn_content_type_mismatch(self.archive_file or "the archive") if self.stage.save_filename: - llnl.util.filesystem.rename( - os.path.join(self.stage.path, basename), self.stage.save_filename - ) + fs.rename(os.path.join(self.stage.path, basename), self.stage.save_filename) if not self.archive_file: - raise FailedDownloadError(self.url) + raise FailedDownloadError( + RuntimeError(f"Missing archive {self.archive_file} after fetching") + ) @fetcher @@ -1366,17 +1367,23 @@ class GCSFetchStrategy(URLFetchStrategy): if parsed_url.scheme != "gs": raise spack.error.FetchError("GCSFetchStrategy can only fetch from gs:// urls.") - tty.debug("Fetching {0}".format(self.url)) - basename = os.path.basename(parsed_url.path) + request = urllib.request.Request( + self.url, headers={"User-Agent": web_util.SPACK_USER_AGENT} + ) with working_dir(self.stage.path): - _, headers, stream = web_util.read_from_url(self.url) + try: + response = web_util.urlopen(request) + except (TimeoutError, urllib.error.URLError) as e: + raise FailedDownloadError(e) from e + + tty.debug(f"Fetching {self.url}") with open(basename, "wb") as f: - shutil.copyfileobj(stream, f) + shutil.copyfileobj(response, f) - content_type = web_util.get_header(headers, "Content-type") + content_type = web_util.get_header(response.headers, "Content-type") if content_type == "text/html": warn_content_type_mismatch(self.archive_file or "the archive") @@ -1385,7 +1392,9 @@ class GCSFetchStrategy(URLFetchStrategy): os.rename(os.path.join(self.stage.path, basename), self.stage.save_filename) if not self.archive_file: - raise FailedDownloadError(self.url) + raise FailedDownloadError( + RuntimeError(f"Missing archive {self.archive_file} after fetching") + ) @fetcher @@ -1722,9 +1731,9 @@ class NoCacheError(spack.error.FetchError): class FailedDownloadError(spack.error.FetchError): """Raised when a download fails.""" - def __init__(self, url, msg=""): - super().__init__("Failed to fetch file from URL: %s" % url, msg) - self.url = url + def __init__(self, *exceptions: Exception): + super().__init__("Failed to download") + self.exceptions = exceptions class NoArchiveFileError(spack.error.FetchError): diff --git a/lib/spack/spack/stage.py b/lib/spack/spack/stage.py index a635d95aeb..847c64d03f 100644 --- a/lib/spack/spack/stage.py +++ b/lib/spack/spack/stage.py @@ -13,7 +13,7 @@ import shutil import stat import sys import tempfile -from typing import Callable, Dict, Iterable, Optional, Set +from typing import Callable, Dict, Iterable, List, Optional, Set import llnl.string import llnl.util.lang @@ -40,6 +40,7 @@ import spack.paths import spack.resource import spack.spec import spack.stage +import spack.util.crypto import spack.util.lock import spack.util.path as sup import spack.util.pattern as pattern @@ -534,32 +535,29 @@ class Stage(LockableStagingDir): for fetcher in dynamic_fetchers: yield fetcher - def print_errors(errors): - for msg in errors: - tty.debug(msg) - - errors = [] + errors: List[str] = [] for fetcher in generate_fetchers(): try: fetcher.stage = self self.fetcher = fetcher self.fetcher.fetch() break - except spack.fetch_strategy.NoCacheError: + except fs.NoCacheError: # Don't bother reporting when something is not cached. continue + except fs.FailedDownloadError as f: + errors.extend(f"{fetcher}: {e.__class__.__name__}: {e}" for e in f.exceptions) + continue except spack.error.SpackError as e: - errors.append("Fetching from {0} failed.".format(fetcher)) - tty.debug(e) + errors.append(f"{fetcher}: {e.__class__.__name__}: {e}") continue else: - print_errors(errors) - self.fetcher = self.default_fetcher - default_msg = "All fetchers failed for {0}".format(self.name) - raise spack.error.FetchError(err_msg or default_msg, None) - - print_errors(errors) + if err_msg: + raise spack.error.FetchError(err_msg) + raise spack.error.FetchError( + f"All fetchers failed for {self.name}", "\n".join(f" {e}" for e in errors) + ) def steal_source(self, dest): """Copy the source_path directory in its entirety to directory dest @@ -1188,7 +1186,7 @@ def _fetch_and_checksum(url, options, keep_stage, action_fn=None): # Checksum the archive and add it to the list checksum = spack.util.crypto.checksum(hashlib.sha256, stage.archive_file) return checksum, None - except FailedDownloadError: + except fs.FailedDownloadError: return None, f"[WORKER] Failed to fetch {url}" except Exception as e: return None, f"[WORKER] Something failed on {url}, skipping. ({e})" @@ -1208,7 +1206,3 @@ class RestageError(StageError): class VersionFetchError(StageError): """Raised when we can't determine a URL to fetch a package.""" - - -# Keep this in namespace for convenience -FailedDownloadError = fs.FailedDownloadError diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py index 7dccdcc182..7c1c8f365b 100644 --- a/lib/spack/spack/test/config.py +++ b/lib/spack/spack/test/config.py @@ -18,6 +18,7 @@ from llnl.util.filesystem import join_path, touch, touchp import spack.config import spack.directory_layout import spack.environment as ev +import spack.fetch_strategy import spack.main import spack.package_base import spack.paths diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py index 8a1f887fbe..cb978b97f3 100644 --- a/lib/spack/spack/test/conftest.py +++ b/lib/spack/spack/test/conftest.py @@ -59,6 +59,7 @@ import spack.util.gpg import spack.util.parallel import spack.util.spack_yaml as syaml import spack.util.url as url_util +import spack.util.web import spack.version from spack.fetch_strategy import URLFetchStrategy from spack.util.pattern import Bunch @@ -1812,12 +1813,7 @@ def mock_curl_configs(mock_config_data, monkeypatch): tty.msg("curl: (22) The requested URL returned error: 404") self.returncode = 22 - def mock_curl(*args): - return MockCurl() - - monkeypatch.setattr(spack.util.web, "_curl", mock_curl) - - yield + monkeypatch.setattr(spack.util.web, "require_curl", MockCurl) @pytest.fixture(scope="function") diff --git a/lib/spack/spack/test/packaging.py b/lib/spack/spack/test/packaging.py index aa1e50468a..e79cdd91c8 100644 --- a/lib/spack/spack/test/packaging.py +++ b/lib/spack/spack/test/packaging.py @@ -11,6 +11,7 @@ import os import pathlib import platform import shutil +import urllib.error from collections import OrderedDict import pytest @@ -21,6 +22,7 @@ from llnl.util.symlink import readlink, symlink import spack.binary_distribution as bindist import spack.cmd.buildcache as buildcache import spack.error +import spack.fetch_strategy import spack.package_base import spack.repo import spack.store @@ -478,7 +480,7 @@ def test_macho_make_paths(): @pytest.fixture() -def mock_download(): +def mock_download(monkeypatch): """Mock a failing download strategy.""" class FailedDownloadStrategy(spack.fetch_strategy.FetchStrategy): @@ -487,19 +489,14 @@ def mock_download(): def fetch(self): raise spack.fetch_strategy.FailedDownloadError( - "<non-existent URL>", "This FetchStrategy always fails" + urllib.error.URLError("This FetchStrategy always fails") ) - fetcher = FailedDownloadStrategy() - @property def fake_fn(self): - return fetcher + return FailedDownloadStrategy() - orig_fn = spack.package_base.PackageBase.fetcher - spack.package_base.PackageBase.fetcher = fake_fn - yield - spack.package_base.PackageBase.fetcher = orig_fn + monkeypatch.setattr(spack.package_base.PackageBase, "fetcher", fake_fn) @pytest.mark.parametrize( diff --git a/lib/spack/spack/test/stage.py b/lib/spack/spack/test/stage.py index 7bb7f3753a..084d95475c 100644 --- a/lib/spack/spack/test/stage.py +++ b/lib/spack/spack/test/stage.py @@ -18,6 +18,7 @@ from llnl.util.filesystem import getuid, mkdirp, partition_path, touch, working_ from llnl.util.symlink import readlink import spack.error +import spack.fetch_strategy import spack.paths import spack.stage import spack.util.executable @@ -323,17 +324,11 @@ def failing_search_fn(): return _mock -@pytest.fixture -def failing_fetch_strategy(): - """Returns a fetch strategy that fails.""" - - class FailingFetchStrategy(spack.fetch_strategy.FetchStrategy): - def fetch(self): - raise spack.fetch_strategy.FailedDownloadError( - "<non-existent URL>", "This implementation of FetchStrategy always fails" - ) - - return FailingFetchStrategy() +class FailingFetchStrategy(spack.fetch_strategy.FetchStrategy): + def fetch(self): + raise spack.fetch_strategy.FailedDownloadError( + "<non-existent URL>", "This implementation of FetchStrategy always fails" + ) @pytest.fixture @@ -511,8 +506,8 @@ class TestStage: stage.fetch() check_destroy(stage, self.stage_name) - def test_no_search_mirror_only(self, failing_fetch_strategy, failing_search_fn): - stage = Stage(failing_fetch_strategy, name=self.stage_name, search_fn=failing_search_fn) + def test_no_search_mirror_only(self, failing_search_fn): + stage = Stage(FailingFetchStrategy(), name=self.stage_name, search_fn=failing_search_fn) with stage: try: stage.fetch(mirror_only=True) @@ -527,8 +522,8 @@ class TestStage: (None, "All fetchers failed"), ], ) - def test_search_if_default_fails(self, failing_fetch_strategy, search_fn, err_msg, expected): - stage = Stage(failing_fetch_strategy, name=self.stage_name, search_fn=search_fn) + def test_search_if_default_fails(self, search_fn, err_msg, expected): + stage = Stage(FailingFetchStrategy(), name=self.stage_name, search_fn=search_fn) with stage: with pytest.raises(spack.error.FetchError, match=expected): diff --git a/lib/spack/spack/test/url_fetch.py b/lib/spack/spack/test/url_fetch.py index 74505b3688..96460cd9b3 100644 --- a/lib/spack/spack/test/url_fetch.py +++ b/lib/spack/spack/test/url_fetch.py @@ -4,8 +4,10 @@ # SPDX-License-Identifier: (Apache-2.0 OR MIT) import collections +import filecmp import os import sys +import urllib.error import pytest @@ -24,6 +26,14 @@ from spack.stage import Stage from spack.util.executable import which +@pytest.fixture +def missing_curl(monkeypatch): + def require_curl(): + raise spack.error.FetchError("curl is required but not found") + + monkeypatch.setattr(web_util, "require_curl", require_curl) + + @pytest.fixture(params=list(crypto.hashes.keys())) def checksum_type(request): return request.param @@ -66,66 +76,62 @@ def pkg_factory(): return factory -@pytest.mark.parametrize("_fetch_method", ["curl", "urllib"]) -def test_urlfetchstrategy_sans_url(_fetch_method): +def test_urlfetchstrategy_sans_url(): """Ensure constructor with no URL fails.""" - with spack.config.override("config:url_fetch_method", _fetch_method): - with pytest.raises(ValueError): - with fs.URLFetchStrategy(None): - pass + with pytest.raises(ValueError): + fs.URLFetchStrategy(None) -@pytest.mark.parametrize("_fetch_method", ["curl", "urllib"]) -def test_urlfetchstrategy_bad_url(tmpdir, _fetch_method): +@pytest.mark.parametrize("method", ["curl", "urllib"]) +def test_urlfetchstrategy_bad_url(tmp_path, mutable_config, method): """Ensure fetch with bad URL fails as expected.""" - testpath = str(tmpdir) - with spack.config.override("config:url_fetch_method", _fetch_method): - with pytest.raises(fs.FailedDownloadError): - fetcher = fs.URLFetchStrategy(url="file:///does-not-exist") - assert fetcher is not None + mutable_config.set("config:url_fetch_method", method) + fetcher = fs.URLFetchStrategy(url=(tmp_path / "does-not-exist").as_uri()) - with Stage(fetcher, path=testpath) as stage: - assert stage is not None - assert fetcher.archive_file is None - fetcher.fetch() + with Stage(fetcher, path=str(tmp_path / "stage")): + with pytest.raises(fs.FailedDownloadError) as exc: + fetcher.fetch() + assert len(exc.value.exceptions) == 1 + exception = exc.value.exceptions[0] -def test_fetch_options(tmpdir, mock_archive): - testpath = str(tmpdir) + if method == "curl": + assert isinstance(exception, spack.error.FetchError) + assert "Curl failed with error 37" in str(exception) # FILE_COULDNT_READ_FILE + elif method == "urllib": + assert isinstance(exception, urllib.error.URLError) + assert isinstance(exception.reason, FileNotFoundError) + + +def test_fetch_options(tmp_path, mock_archive): with spack.config.override("config:url_fetch_method", "curl"): fetcher = fs.URLFetchStrategy( url=mock_archive.url, fetch_options={"cookie": "True", "timeout": 10} ) - assert fetcher is not None - with Stage(fetcher, path=testpath) as stage: - assert stage is not None + with Stage(fetcher, path=str(tmp_path)): assert fetcher.archive_file is None fetcher.fetch() + assert filecmp.cmp(fetcher.archive_file, mock_archive.archive_file) @pytest.mark.parametrize("_fetch_method", ["curl", "urllib"]) -def test_archive_file_errors(tmpdir, mock_archive, _fetch_method): +def test_archive_file_errors(tmp_path, mutable_config, mock_archive, _fetch_method): """Ensure FetchStrategy commands may only be used as intended""" - testpath = str(tmpdir) with spack.config.override("config:url_fetch_method", _fetch_method): fetcher = fs.URLFetchStrategy(url=mock_archive.url) - assert fetcher is not None - with pytest.raises(fs.FailedDownloadError): - with Stage(fetcher, path=testpath) as stage: - assert stage is not None - assert fetcher.archive_file is None - with pytest.raises(fs.NoArchiveFileError): - fetcher.archive(testpath) - with pytest.raises(fs.NoArchiveFileError): - fetcher.expand() - with pytest.raises(fs.NoArchiveFileError): - fetcher.reset() - stage.fetch() - with pytest.raises(fs.NoDigestError): - fetcher.check() - assert fetcher.archive_file is not None - fetcher._fetch_from_url("file:///does-not-exist") + with Stage(fetcher, path=str(tmp_path)) as stage: + assert fetcher.archive_file is None + with pytest.raises(fs.NoArchiveFileError): + fetcher.archive(str(tmp_path)) + with pytest.raises(fs.NoArchiveFileError): + fetcher.expand() + with pytest.raises(fs.NoArchiveFileError): + fetcher.reset() + stage.fetch() + with pytest.raises(fs.NoDigestError): + fetcher.check() + assert filecmp.cmp(fetcher.archive_file, mock_archive.archive_file) files = [(".tar.gz", "z"), (".tgz", "z")] @@ -271,16 +277,15 @@ def test_url_with_status_bar(tmpdir, mock_archive, monkeypatch, capfd): @pytest.mark.parametrize("_fetch_method", ["curl", "urllib"]) -def test_url_extra_fetch(tmpdir, mock_archive, _fetch_method): +def test_url_extra_fetch(tmp_path, mutable_config, mock_archive, _fetch_method): """Ensure a fetch after downloading is effectively a no-op.""" - with spack.config.override("config:url_fetch_method", _fetch_method): - testpath = str(tmpdir) - fetcher = fs.URLFetchStrategy(mock_archive.url) - with Stage(fetcher, path=testpath) as stage: - assert fetcher.archive_file is None - stage.fetch() - assert fetcher.archive_file is not None - fetcher.fetch() + mutable_config.set("config:url_fetch_method", _fetch_method) + fetcher = fs.URLFetchStrategy(mock_archive.url) + with Stage(fetcher, path=str(tmp_path)) as stage: + assert fetcher.archive_file is None + stage.fetch() + assert filecmp.cmp(fetcher.archive_file, mock_archive.archive_file) + fetcher.fetch() @pytest.mark.parametrize( @@ -316,49 +321,25 @@ def test_candidate_urls(pkg_factory, url, urls, version, expected, _fetch_method @pytest.mark.regression("19673") -def test_missing_curl(tmpdir, monkeypatch): +def test_missing_curl(tmp_path, missing_curl, mutable_config, monkeypatch): """Ensure a fetch involving missing curl package reports the error.""" - err_fmt = "No such command {0}" - - def _which(*args, **kwargs): - err_msg = err_fmt.format(args[0]) - raise spack.util.executable.CommandNotFoundError(err_msg) - - # Patching the 'which' symbol imported by fetch_strategy needed due - # to 'from spack.util.executable import which' in this module. - monkeypatch.setattr(fs, "which", _which) - - testpath = str(tmpdir) - url = "http://github.com/spack/spack" - with spack.config.override("config:url_fetch_method", "curl"): - fetcher = fs.URLFetchStrategy(url=url) - assert fetcher is not None - with pytest.raises(TypeError, match="object is not callable"): - with Stage(fetcher, path=testpath) as stage: - out = stage.fetch() - assert err_fmt.format("curl") in out + mutable_config.set("config:url_fetch_method", "curl") + fetcher = fs.URLFetchStrategy(url="http://example.com/file.tar.gz") + with pytest.raises(spack.error.FetchError, match="curl is required but not found"): + with Stage(fetcher, path=str(tmp_path)) as stage: + stage.fetch() -def test_url_fetch_text_without_url(tmpdir): +def test_url_fetch_text_without_url(): with pytest.raises(spack.error.FetchError, match="URL is required"): web_util.fetch_url_text(None) -def test_url_fetch_text_curl_failures(tmpdir, monkeypatch): +def test_url_fetch_text_curl_failures(mutable_config, missing_curl, monkeypatch): """Check fetch_url_text if URL's curl is missing.""" - err_fmt = "No such command {0}" - - def _which(*args, **kwargs): - err_msg = err_fmt.format(args[0]) - raise spack.util.executable.CommandNotFoundError(err_msg) - - # Patching the 'which' symbol imported by spack.util.web needed due - # to 'from spack.util.executable import which' in this module. - monkeypatch.setattr(spack.util.web, "which", _which) - - with spack.config.override("config:url_fetch_method", "curl"): - with pytest.raises(spack.error.FetchError, match="Missing required curl"): - web_util.fetch_url_text("https://github.com/") + mutable_config.set("config:url_fetch_method", "curl") + with pytest.raises(spack.error.FetchError, match="curl is required but not found"): + web_util.fetch_url_text("https://example.com/") def test_url_check_curl_errors(): @@ -372,24 +353,14 @@ def test_url_check_curl_errors(): web_util.check_curl_code(60) -def test_url_missing_curl(tmpdir, monkeypatch): +def test_url_missing_curl(mutable_config, missing_curl, monkeypatch): """Check url_exists failures if URL's curl is missing.""" - err_fmt = "No such command {0}" - - def _which(*args, **kwargs): - err_msg = err_fmt.format(args[0]) - raise spack.util.executable.CommandNotFoundError(err_msg) - - # Patching the 'which' symbol imported by spack.util.web needed due - # to 'from spack.util.executable import which' in this module. - monkeypatch.setattr(spack.util.web, "which", _which) - - with spack.config.override("config:url_fetch_method", "curl"): - with pytest.raises(spack.error.FetchError, match="Missing required curl"): - web_util.url_exists("https://github.com/") + mutable_config.set("config:url_fetch_method", "curl") + with pytest.raises(spack.error.FetchError, match="curl is required but not found"): + web_util.url_exists("https://example.com/") -def test_url_fetch_text_urllib_bad_returncode(tmpdir, monkeypatch): +def test_url_fetch_text_urllib_bad_returncode(mutable_config, monkeypatch): class response: def getcode(self): return 404 @@ -397,19 +368,19 @@ def test_url_fetch_text_urllib_bad_returncode(tmpdir, monkeypatch): def _read_from_url(*args, **kwargs): return None, None, response() - monkeypatch.setattr(spack.util.web, "read_from_url", _read_from_url) + monkeypatch.setattr(web_util, "read_from_url", _read_from_url) + mutable_config.set("config:url_fetch_method", "urllib") - with spack.config.override("config:url_fetch_method", "urllib"): - with pytest.raises(spack.error.FetchError, match="failed with error code"): - web_util.fetch_url_text("https://github.com/") + with pytest.raises(spack.error.FetchError, match="failed with error code"): + web_util.fetch_url_text("https://example.com/") -def test_url_fetch_text_urllib_web_error(tmpdir, monkeypatch): +def test_url_fetch_text_urllib_web_error(mutable_config, monkeypatch): def _raise_web_error(*args, **kwargs): raise web_util.SpackWebError("bad url") - monkeypatch.setattr(spack.util.web, "read_from_url", _raise_web_error) + monkeypatch.setattr(web_util, "read_from_url", _raise_web_error) + mutable_config.set("config:url_fetch_method", "urllib") - with spack.config.override("config:url_fetch_method", "urllib"): - with pytest.raises(spack.error.FetchError, match="fetch failed to verify"): - web_util.fetch_url_text("https://github.com/") + with pytest.raises(spack.error.FetchError, match="fetch failed to verify"): + web_util.fetch_url_text("https://example.com/") diff --git a/lib/spack/spack/test/web.py b/lib/spack/spack/test/web.py index a2b64798d0..cf89e2e3a4 100644 --- a/lib/spack/spack/test/web.py +++ b/lib/spack/spack/test/web.py @@ -432,7 +432,7 @@ def test_ssl_curl_cert_file(cert_exists, tmpdir, ssl_scrubbed_env, mutable_confi if cert_exists: open(mock_cert, "w").close() assert os.path.isfile(mock_cert) - curl = spack.util.web._curl() + curl = spack.util.web.require_curl() # arbitrary call to query the run env dump_env = {} diff --git a/lib/spack/spack/util/web.py b/lib/spack/spack/util/web.py index b681bb4950..6b27c6ae68 100644 --- a/lib/spack/spack/util/web.py +++ b/lib/spack/spack/util/web.py @@ -28,10 +28,11 @@ from llnl.util.filesystem import mkdirp, rename, working_dir import spack.config import spack.error +import spack.util.executable import spack.util.path import spack.util.url as url_util -from .executable import CommandNotFoundError, Executable, which +from .executable import CommandNotFoundError, Executable from .gcs import GCSBlob, GCSBucket, GCSHandler from .s3 import UrllibS3Handler, get_s3_session @@ -198,7 +199,7 @@ def read_from_url(url, accept_content_type=None): try: response = urlopen(request) except (TimeoutError, URLError) as e: - raise SpackWebError(f"Download of {url.geturl()} failed: {e}") + raise SpackWebError(f"Download of {url.geturl()} failed: {e.__class__.__name__}: {e}") if accept_content_type: try: @@ -307,45 +308,44 @@ def base_curl_fetch_args(url, timeout=0): return curl_args -def check_curl_code(returncode): +def check_curl_code(returncode: int) -> None: """Check standard return code failures for provided arguments. Arguments: - returncode (int): curl return code + returncode: curl return code Raises FetchError if the curl returncode indicates failure """ - if returncode != 0: - if returncode == 22: - # This is a 404. Curl will print the error. - raise spack.error.FetchError("URL was not found!") - - if returncode == 60: - # This is a certificate error. Suggest spack -k - raise spack.error.FetchError( - "Curl was unable to fetch due to invalid certificate. " - "This is either an attack, or your cluster's SSL " - "configuration is bad. If you believe your SSL " - "configuration is bad, you can try running spack -k, " - "which will not check SSL certificates." - "Use this at your own risk." - ) + if returncode == 0: + return + elif returncode == 22: + # This is a 404. Curl will print the error. + raise spack.error.FetchError("URL was not found!") + elif returncode == 60: + # This is a certificate error. Suggest spack -k + raise spack.error.FetchError( + "Curl was unable to fetch due to invalid certificate. " + "This is either an attack, or your cluster's SSL " + "configuration is bad. If you believe your SSL " + "configuration is bad, you can try running spack -k, " + "which will not check SSL certificates." + "Use this at your own risk." + ) - raise spack.error.FetchError("Curl failed with error {0}".format(returncode)) + raise spack.error.FetchError(f"Curl failed with error {returncode}") -def _curl(curl=None): - if not curl: - try: - curl = which("curl", required=True) - except CommandNotFoundError as exc: - tty.error(str(exc)) - raise spack.error.FetchError("Missing required curl fetch method") +def require_curl() -> Executable: + try: + path = spack.util.executable.which_string("curl", required=True) + except CommandNotFoundError as e: + raise spack.error.FetchError(f"curl is required but not found: {e}") from e + curl = spack.util.executable.Executable(path) set_curl_env_for_ssl_certs(curl) return curl -def fetch_url_text(url, curl=None, dest_dir="."): +def fetch_url_text(url, curl: Optional[Executable] = None, dest_dir="."): """Retrieves text-only URL content using the configured fetch method. It determines the fetch method from: @@ -379,10 +379,7 @@ def fetch_url_text(url, curl=None, dest_dir="."): fetch_method = spack.config.get("config:url_fetch_method") tty.debug("Using '{0}' to fetch {1} into {2}".format(fetch_method, url, path)) if fetch_method == "curl": - curl_exe = _curl(curl) - if not curl_exe: - raise spack.error.FetchError("Missing required fetch method (curl)") - + curl_exe = curl or require_curl() curl_args = ["-O"] curl_args.extend(base_curl_fetch_args(url)) @@ -439,9 +436,7 @@ def url_exists(url, curl=None): "config:url_fetch_method", "urllib" ) == "curl" and url_result.scheme not in ("gs", "s3") if use_curl: - curl_exe = _curl(curl) - if not curl_exe: - return False + curl_exe = curl or require_curl() # Telling curl to fetch the first byte (-r 0-0) is supposed to be # portable. |