Repository: easybuilders/easybuild-framework

Issue:
New checksum listing with dictionaries leads to confusing errors if you forget to update them
From a PR which had forgotten to inject new checksums
```python
name = 'VTK'
version = '9.2.6'
...
checksums = [
    {'VTK-9.2.2.tar.gz': '1c5b0a2be71fac96ff4831af69e350f7a0ea3168981f790c000709dcf9121075'},
    {'VTKData-9.2.2.tar.gz': '4b0ac438c77773ff741fd02c0e3b0dc1406b4c51e6cbe4e8304306fa125f27bf'},
    {'vtk-version.egg-info': '787b82415ae7a4a1f815b4db0e25f7abc809a05fc85d7d219627f3a7e5d3867b'},
]
```
with the problem being that they are mapping to the old versions (and, of course, the checksums being old as well.
They are technically correct for version 9.2.2 (so there isn't actually any errors here if one were to nitpick)

but it does lead to a very confusing error message
```
== 2023-05-05 17:55:58,351 build_log.py:171 ERROR EasyBuild crashed with an error (at easybuild/easybuild-framework/easybuild/base/exceptions.py:126 in __init__): Invalid checksum spec 'None', should be a string (MD5) or 2-tuple (type, value). (at easybuild/easybuild-framework/easybuild/tools/filetools.py:1306 in verify_checksum)
```

Invalid checksum spec 'None'? It should detect the lack of a checksum for the particular source and give a proper error for that specifically.


Discussion:

Resolving patch:
diff --git a/easybuild/tools/filetools.py b/easybuild/tools/filetools.py
--- a/easybuild/tools/filetools.py
+++ b/easybuild/tools/filetools.py
@@ -1264,14 +1264,11 @@ def verify_checksum(path, checksums):
 
     for checksum in checksums:
         if isinstance(checksum, dict):
-            if filename in checksum:
+            try:
                 # Set this to a string-type checksum
                 checksum = checksum[filename]
-            elif build_option('enforce_checksums'):
-                raise EasyBuildError("Missing checksum for %s", filename)
-            else:
-                # Set to None and allow to fail elsewhere
-                checksum = None
+            except KeyError:
+                raise EasyBuildError("Missing checksum for %s in %s", filename, checksum)
 
         if isinstance(checksum, string_type):
             # if no checksum type is specified, it is assumed to be MD5 (32 characters) or SHA256 (64 characters)
@@ -1301,7 +1298,8 @@ def verify_checksum(path, checksums):
                 # no matching checksums
                 return False
         else:
-            raise EasyBuildError("Invalid checksum spec '%s', should be a string (MD5) or 2-tuple (type, value).",
+            raise EasyBuildError("Invalid checksum spec '%s': should be a string (MD5 or SHA256), "
+                                 "2-tuple (type, value), or tuple of alternative checksum specs.",
                                  checksum)
 
         actual_checksum = compute_checksum(path, typ)


Test patch:
diff --git a/test/framework/filetools.py b/test/framework/filetools.py
--- a/test/framework/filetools.py
+++ b/test/framework/filetools.py
@@ -349,6 +349,14 @@ def test_checksums(self):
         alt_checksums = ('7167b64b1ca062b9674ffef46f9325db7167b64b1ca062b9674ffef46f9325db', broken_checksums['sha256'])
         self.assertFalse(ft.verify_checksum(fp, alt_checksums))
 
+        # Check dictionary
+        alt_checksums = (known_checksums['sha256'],)
+        self.assertTrue(ft.verify_checksum(fp, {os.path.basename(fp): known_checksums['sha256']}))
+        faulty_dict = {'wrong-name': known_checksums['sha256']}
+        self.assertErrorRegex(EasyBuildError,
+                              "Missing checksum for " + os.path.basename(fp) + " in .*wrong-name.*",
+                              ft.verify_checksum, fp, faulty_dict)
+
         # check whether missing checksums are enforced
         build_options = {
             'enforce_checksums': True,
@@ -363,6 +371,8 @@ def test_checksums(self):
         for checksum in [known_checksums[x] for x in ('md5', 'sha256')]:
             dict_checksum = {os.path.basename(fp): checksum, 'foo': 'baa'}
             self.assertTrue(ft.verify_checksum(fp, dict_checksum))
+            del dict_checksum[os.path.basename(fp)]
+            self.assertErrorRegex(EasyBuildError, "Missing checksum for", ft.verify_checksum, fp, dict_checksum)
 
     def test_common_path_prefix(self):
         """Test get common path prefix for a list of paths."""