diff options
-rw-r--r-- | SCons/SConsign.py | 11 | ||||
-rw-r--r-- | SCons/SConsignTests.py | 6 | ||||
-rw-r--r-- | SCons/Util.py | 78 | ||||
-rw-r--r-- | SCons/UtilTests.py | 176 | ||||
-rw-r--r-- | test/Configure/ConfigureDryRunError.py | 13 | ||||
-rw-r--r-- | test/Configure/VariantDir-SConscript.py | 2 | ||||
-rw-r--r-- | test/Configure/implicit-cache.py | 28 | ||||
-rw-r--r-- | test/Configure/option--config.py | 6 | ||||
-rw-r--r-- | test/Repository/variants.py | 66 | ||||
-rw-r--r-- | test/option/hash-format.py | 42 | ||||
-rw-r--r-- | test/option/option-n.py | 15 | ||||
-rw-r--r-- | test/question/Configure.py | 14 | ||||
-rw-r--r-- | test/sconsign/corrupt.py | 13 | ||||
-rw-r--r-- | test/sconsign/script/Configure.py | 4 | ||||
-rw-r--r-- | testing/framework/TestSCons.py | 22 |
15 files changed, 395 insertions, 101 deletions
diff --git a/SCons/SConsign.py b/SCons/SConsign.py index ade8a10..95ceac1 100644 --- a/SCons/SConsign.py +++ b/SCons/SConsign.py @@ -64,10 +64,17 @@ def Get_DataBase(dir): if DB_Name is None: hash_format = SCons.Util.get_hash_format() - if hash_format is None: + current_hash_algorithm = SCons.Util.get_current_hash_algorithm_used() + # if the user left the options defaulted AND the default algorithm set by + # SCons is md5, then set the database name to be the special default name + # + # otherwise, if it defaults to something like 'sha1' or the user explicitly + # set 'md5' as the hash format, set the database name to .sconsign_<algorithm> + # eg .sconsign_sha1, etc. + if hash_format is None and current_hash_algorithm == 'md5': DB_Name = ".sconsign" else: - DB_Name = ".sconsign_%s" % hash_format + DB_Name = ".sconsign_%s" % current_hash_algorithm top = dir.fs.Top if not os.path.isabs(DB_Name) and top.repositories: diff --git a/SCons/SConsignTests.py b/SCons/SConsignTests.py index d4f4418..e9f2071 100644 --- a/SCons/SConsignTests.py +++ b/SCons/SConsignTests.py @@ -28,6 +28,7 @@ import TestCmd import SCons.dblite import SCons.SConsign +from SCons.Util import get_hash_format, get_current_hash_algorithm_used class BuildInfo: def merge(self, object): @@ -295,7 +296,10 @@ class SConsignFileTestCase(SConsignTestCase): file = test.workpath('sconsign_file') assert SCons.SConsign.DataBase == {}, SCons.SConsign.DataBase - assert SCons.SConsign.DB_Name == ".sconsign", SCons.SConsign.DB_Name + if get_hash_format() is None and get_current_hash_algorithm_used() == 'md5': + assert SCons.SConsign.DB_Name == ".sconsign", SCons.SConsign.DB_Name + else: + assert SCons.SConsign.DB_Name == ".sconsign_{}".format(get_current_hash_algorithm_used()), SCons.SConsign.DB_Name assert SCons.SConsign.DB_Module is SCons.dblite, SCons.SConsign.DB_Module SCons.SConsign.File(file) diff --git a/SCons/Util.py b/SCons/Util.py index 8b79a3e..cbd99b0 100644 --- a/SCons/Util.py +++ b/SCons/Util.py @@ -29,7 +29,6 @@ import os import pprint import re import sys -import inspect from collections import UserDict, UserList, UserString, OrderedDict from collections.abc import MappingView from contextlib import suppress @@ -1670,12 +1669,12 @@ def AddMethod(obj, function, name=None): # Default hash function and format. SCons-internal. -_DEFAULT_HASH_FORMATS = ['md5', 'sha1', 'sha256'] +DEFAULT_HASH_FORMATS = ['md5', 'sha1', 'sha256'] ALLOWED_HASH_FORMATS = [] _HASH_FUNCTION = None _HASH_FORMAT = None -def _attempt_init_of_python_3_9_hash_object(hash_function_object): +def _attempt_init_of_python_3_9_hash_object(hash_function_object, sys_used=sys): """Python 3.9 and onwards lets us initialize the hash function object with the key "usedforsecurity"=false. This lets us continue to use algorithms that have been deprecated either by FIPS or by Python itself, as the MD5 algorithm SCons @@ -1696,7 +1695,7 @@ def _attempt_init_of_python_3_9_hash_object(hash_function_object): # however, for our purposes checking the version is greater than or equal to 3.9 is good enough, as # the API is guaranteed to have support for the 'usedforsecurity' flag in 3.9. See # https://docs.python.org/3/library/hashlib.html#:~:text=usedforsecurity for the version support notes. - if (sys.version_info.major > 3) or (sys.version_info.major == 3 and sys.version_info.minor >= 9): + if (sys_used.version_info.major > 3) or (sys_used.version_info.major == 3 and sys_used.version_info.minor >= 9): return hash_function_object(usedforsecurity=False) # note that this can throw a ValueError in FIPS-enabled versions of Linux prior to 3.9 @@ -1704,7 +1703,7 @@ def _attempt_init_of_python_3_9_hash_object(hash_function_object): # the caller to diagnose the ValueError & potentially display the error to screen. return hash_function_object() -def _set_allowed_viable_default_hashes(hashlib_used): +def _set_allowed_viable_default_hashes(hashlib_used, sys_used=sys): """Checks if SCons has ability to call the default algorithms normally supported. This util class is sometimes called prior to setting the user-selected hash algorithm, @@ -1712,7 +1711,7 @@ def _set_allowed_viable_default_hashes(hashlib_used): and throw an exception in set_hash_format. A common case is using the SConf options, which can run prior to main, and thus ignore the options.hash_format variable. - This function checks the _DEFAULT_HASH_FORMATS and sets the ALLOWED_HASH_FORMATS + This function checks the DEFAULT_HASH_FORMATS and sets the ALLOWED_HASH_FORMATS to only the ones that can be called. In Python >= 3.9 this will always default to MD5 as in Python 3.9 there is an optional attribute "usedforsecurity" set for the method. @@ -1724,7 +1723,7 @@ def _set_allowed_viable_default_hashes(hashlib_used): # otherwise it keeps appending valid formats to the string ALLOWED_HASH_FORMATS = [] - for test_algorithm in _DEFAULT_HASH_FORMATS: + for test_algorithm in DEFAULT_HASH_FORMATS: _test_hash = getattr(hashlib_used, test_algorithm, None) # we know hashlib claims to support it... check to see if we can call it. if _test_hash is not None: @@ -1733,7 +1732,7 @@ def _set_allowed_viable_default_hashes(hashlib_used): # throw if it's a bad algorithm, otherwise it will append it to the known # good formats. try: - _attempt_init_of_python_3_9_hash_object(_test_hash) + _attempt_init_of_python_3_9_hash_object(_test_hash, sys_used) ALLOWED_HASH_FORMATS.append(test_algorithm) except ValueError as e: _last_error = e @@ -1761,8 +1760,27 @@ def get_hash_format(): """ return _HASH_FORMAT +def _attempt_get_hash_function(hash_name, hashlib_used=hashlib, sys_used=sys): + """Wrapper used to try to initialize a hash function given. -def set_hash_format(hash_format, hashlib_used=hashlib): + If successful, returns the name of the hash function back to the user. + + Otherwise returns None. + """ + try: + _fetch_hash = getattr(hashlib_used, hash_name, None) + if _fetch_hash is None: + return None + _attempt_init_of_python_3_9_hash_object(_fetch_hash, sys_used) + return hash_name + except ValueError: + # if attempt_init_of_python_3_9 throws, this is typically due to FIPS being enabled + # however, if we get to this point, the viable hash function check has either been + # bypassed or otherwise failed to properly restrict the user to only the supported + # functions. As such throw the UserError as an internal assertion-like error. + return None + +def set_hash_format(hash_format, hashlib_used=hashlib, sys_used=sys): """Sets the default hash format used by SCons. If `hash_format` is ``None`` or @@ -1782,7 +1800,7 @@ def set_hash_format(hash_format, hashlib_used=hashlib): # user can select something not supported by their OS but normally supported by # SCons, example, selecting MD5 in an OS with FIPS-mode turned on. Therefore we first # check if SCons supports it, and then if their local OS supports it. - if hash_format_lower in _DEFAULT_HASH_FORMATS: + if hash_format_lower in DEFAULT_HASH_FORMATS: raise UserError('While hash format "%s" is supported by SCons, the ' 'local system indicates only the following hash ' 'formats are supported by the hashlib library: %s' % @@ -1793,7 +1811,7 @@ def set_hash_format(hash_format, hashlib_used=hashlib): # the hash format isn't supported by SCons in any case. Warn the user, and # if we detect that SCons supports more algorithms than their local system # supports, warn the user about that too. - if ALLOWED_HASH_FORMATS == _DEFAULT_HASH_FORMATS: + if ALLOWED_HASH_FORMATS == DEFAULT_HASH_FORMATS: raise UserError('Hash format "%s" is not supported by SCons. Only ' 'the following hash formats are supported: %s' % (hash_format_lower, @@ -1805,23 +1823,14 @@ def set_hash_format(hash_format, hashlib_used=hashlib): 'is reporting; SCons supports: %s. Your local system only ' 'supports: %s' % (hash_format_lower, - ', '.join(_DEFAULT_HASH_FORMATS), + ', '.join(DEFAULT_HASH_FORMATS), ', '.join(ALLOWED_HASH_FORMATS)) ) # this is not expected to fail. If this fails it means the set_allowed_viable_default_hashes # function did not throw, or when it threw, the exception was caught and ignored, or # the global ALLOWED_HASH_FORMATS was changed by an external user. - try: - _HASH_FUNCTION = None - _attempt_init_of_python_3_9_hash_object(getattr(hashlib_used, hash_format_lower, None)) - _HASH_FUNCTION = hash_format_lower - except ValueError: - # if attempt_init_of_python_3_9 throws, this is typically due to FIPS being enabled - # however, if we get to this point, the viable hash function check has either been - # bypassed or otherwise failed to properly restrict the user to only the supported - # functions. As such throw the UserError as an internal assertion-like error. - pass + _HASH_FUNCTION = _attempt_get_hash_function(hash_format_lower, hashlib_used, sys_used) if _HASH_FUNCTION is None: from SCons.Errors import UserError # pylint: disable=import-outside-toplevel @@ -1838,12 +1847,7 @@ def set_hash_format(hash_format, hashlib_used=hashlib): # in FIPS-compliant systems this usually defaults to SHA1, unless that too has been # disabled. for choice in ALLOWED_HASH_FORMATS: - try: - _HASH_FUNCTION = None - _attempt_init_of_python_3_9_hash_object(getattr(hashlib_used, choice, None)) - _HASH_FUNCTION = choice - except ValueError: - continue + _HASH_FUNCTION = _attempt_get_hash_function(choice, hashlib_used, sys_used) if _HASH_FUNCTION is not None: break @@ -1864,7 +1868,19 @@ def set_hash_format(hash_format, hashlib_used=hashlib): set_hash_format(None) -def _get_hash_object(hash_format, hashlib_used=hashlib): +def get_current_hash_algorithm_used(): + """Returns the current hash algorithm name used. + + Where the python version >= 3.9, this is expected to return md5. + If python's version is <= 3.8, this returns md5 on non-FIPS-mode platforms, and + sha1 or sha256 on FIPS-mode Linux platforms. + + This function is primarily useful for testing, where one expects a value to be + one of N distinct hashes, and therefore the test needs to know which hash to select. + """ + return _HASH_FUNCTION + +def _get_hash_object(hash_format, hashlib_used=hashlib, sys_used=sys): """Allocates a hash object using the requested hash format. Args: @@ -1879,7 +1895,7 @@ def _get_hash_object(hash_format, hashlib_used=hashlib): raise UserError('There is no default hash function. Did you call ' 'a hashing function before SCons was initialized?') - return _attempt_init_of_python_3_9_hash_object(getattr(hashlib_used, _HASH_FUNCTION, None)) + return _attempt_init_of_python_3_9_hash_object(getattr(hashlib_used, _HASH_FUNCTION, None), sys_used) if not hasattr(hashlib, hash_format): from SCons.Errors import UserError # pylint: disable=import-outside-toplevel @@ -1888,7 +1904,7 @@ def _get_hash_object(hash_format, hashlib_used=hashlib): 'Hash format "%s" is not available in your Python interpreter.' % hash_format) - return _attempt_init_of_python_3_9_hash_object(getattr(hashlib, hash_format)) + return _attempt_init_of_python_3_9_hash_object(getattr(hashlib, hash_format), sys_used) def hash_signature(s, hash_format=None): diff --git a/SCons/UtilTests.py b/SCons/UtilTests.py index 616ea37..4417ba5 100644 --- a/SCons/UtilTests.py +++ b/SCons/UtilTests.py @@ -26,13 +26,17 @@ import io import os import sys import unittest -from collections import UserDict, UserList, UserString +import unittest.mock +import hashlib +import warnings +from collections import UserDict, UserList, UserString, namedtuple import TestCmd import SCons.Errors import SCons.compat from SCons.Util import ( + ALLOWED_HASH_FORMATS, AddPathIfNotExists, AppendPath, CLVar, @@ -42,6 +46,9 @@ from SCons.Util import ( Proxy, Selector, WhereIs, + _attempt_init_of_python_3_9_hash_object, + _get_hash_object, + _set_allowed_viable_default_hashes, adjustixes, containsAll, containsAny, @@ -61,6 +68,7 @@ from SCons.Util import ( is_Tuple, print_tree, render_tree, + set_hash_format, silent_intern, splitext, to_String, @@ -838,12 +846,17 @@ class HashTestCase(unittest.TestCase): '25235f0fcab8767b7b5ac6568786fbc4f7d5d83468f0626bf07c3dbeed391a7a', 'f8d3d0729bf2427e2e81007588356332e7e8c4133fae4bceb173b93f33411d17'), }.items(): - hs = functools.partial(hash_signature, hash_format=algorithm) - s = list(map(hs, ('111', '222', '333'))) - - assert expected[0] == hash_collect(s[0:1], hash_format=algorithm) - assert expected[1] == hash_collect(s[0:2], hash_format=algorithm) - assert expected[2] == hash_collect(s, hash_format=algorithm) + # if the current platform does not support the algorithm we're looking at, + # skip the test steps for that algorithm, but display a warning to the user + if algorithm not in ALLOWED_HASH_FORMATS: + warnings.warn("Missing hash algorithm {} on this platform, cannot test with it".format(algorithm), ResourceWarning) + else: + hs = functools.partial(hash_signature, hash_format=algorithm) + s = list(map(hs, ('111', '222', '333'))) + + assert expected[0] == hash_collect(s[0:1], hash_format=algorithm) + assert expected[1] == hash_collect(s[0:2], hash_format=algorithm) + assert expected[2] == hash_collect(s, hash_format=algorithm) def test_MD5signature(self): """Test generating a signature""" @@ -855,11 +868,150 @@ class HashTestCase(unittest.TestCase): 'sha256': ('f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae', '9b871512327c09ce91dd649b3f96a63b7408ef267c8cc5710114e629730cb61f'), }.items(): - s = hash_signature('111', hash_format=algorithm) - assert expected[0] == s, s - - s = hash_signature('222', hash_format=algorithm) - assert expected[1] == s, s + # if the current platform does not support the algorithm we're looking at, + # skip the test steps for that algorithm, but display a warning to the user + if algorithm not in ALLOWED_HASH_FORMATS: + warnings.warn("Missing hash algorithm {} on this platform, cannot test with it".format(algorithm), ResourceWarning) + else: + s = hash_signature('111', hash_format=algorithm) + assert expected[0] == s, s + + s = hash_signature('222', hash_format=algorithm) + assert expected[1] == s, s + +# this uses mocking out, which is platform specific, however, the FIPS +# behavior this is testing is also platform-specific, and only would be +# visible in hosts running Linux with the fips_mode kernel flag along +# with using OpenSSL. + +class FIPSHashTestCase(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(FIPSHashTestCase, self).__init__(*args, **kwargs) + + ############################### + # algorithm mocks, can check if we called with usedforsecurity=False for python >= 3.9 + self.fake_md5=lambda usedforsecurity=True: (usedforsecurity, 'md5') + self.fake_sha1=lambda usedforsecurity=True: (usedforsecurity, 'sha1') + self.fake_sha256=lambda usedforsecurity=True: (usedforsecurity, 'sha256') + ############################### + + ############################### + # hashlib mocks + md5Available = unittest.mock.Mock(md5=self.fake_md5) + del md5Available.sha1 + del md5Available.sha256 + self.md5Available=md5Available + + md5Default = unittest.mock.Mock(md5=self.fake_md5, sha1=self.fake_sha1) + del md5Default.sha256 + self.md5Default=md5Default + + sha1Default = unittest.mock.Mock(sha1=self.fake_sha1, sha256=self.fake_sha256) + del sha1Default.md5 + self.sha1Default=sha1Default + + sha256Default = unittest.mock.Mock(sha256=self.fake_sha256, **{'md5.side_effect': ValueError, 'sha1.side_effect': ValueError}) + self.sha256Default=sha256Default + + all_throw = unittest.mock.Mock(**{'md5.side_effect': ValueError, 'sha1.side_effect': ValueError, 'sha256.side_effect': ValueError}) + self.all_throw=all_throw + + no_algorithms = unittest.mock.Mock() + del no_algorithms.md5 + del no_algorithms.sha1 + del no_algorithms.sha256 + self.no_algorithms=no_algorithms + + unsupported_algorithm = unittest.mock.Mock(unsupported=self.fake_sha256) + del unsupported_algorithm.md5 + del unsupported_algorithm.sha1 + del unsupported_algorithm.sha256 + self.unsupported_algorithm=unsupported_algorithm + ############################### + + ############################### + # system version mocks + VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial') + v3_8 = VersionInfo(3, 8, 199, 'super-beta', 1337) + v3_9 = VersionInfo(3, 9, 0, 'alpha', 0) + v4_8 = VersionInfo(4, 8, 0, 'final', 0) + + self.sys_v3_8 = unittest.mock.Mock(version_info=v3_8) + self.sys_v3_9 = unittest.mock.Mock(version_info=v3_9) + self.sys_v4_8 = unittest.mock.Mock(version_info=v4_8) + ############################### + + def test_usedforsecurity_flag_behavior(self): + """Test usedforsecurity flag -> should be set to 'True' on older versions of python, and 'False' on Python >= 3.9""" + for version, expected in { + self.sys_v3_8: (True, 'md5'), + self.sys_v3_9: (False, 'md5'), + self.sys_v4_8: (False, 'md5'), + }.items(): + assert _attempt_init_of_python_3_9_hash_object(self.fake_md5, version) == expected + + def test_automatic_default_to_md5(self): + """Test automatic default to md5 even if sha1 available""" + for version, expected in { + self.sys_v3_8: (True, 'md5'), + self.sys_v3_9: (False, 'md5'), + self.sys_v4_8: (False, 'md5'), + }.items(): + _set_allowed_viable_default_hashes(self.md5Default, version) + set_hash_format(None, self.md5Default, version) + assert _get_hash_object(None, self.md5Default, version) == expected + + def test_automatic_default_to_sha256(self): + """Test automatic default to sha256 if other algorithms available but throw""" + for version, expected in { + self.sys_v3_8: (True, 'sha256'), + self.sys_v3_9: (False, 'sha256'), + self.sys_v4_8: (False, 'sha256'), + }.items(): + _set_allowed_viable_default_hashes(self.sha256Default, version) + set_hash_format(None, self.sha256Default, version) + assert _get_hash_object(None, self.sha256Default, version) == expected + + def test_automatic_default_to_sha1(self): + """Test automatic default to sha1 if md5 is missing from hashlib entirely""" + for version, expected in { + self.sys_v3_8: (True, 'sha1'), + self.sys_v3_9: (False, 'sha1'), + self.sys_v4_8: (False, 'sha1'), + }.items(): + _set_allowed_viable_default_hashes(self.sha1Default, version) + set_hash_format(None, self.sha1Default, version) + assert _get_hash_object(None, self.sha1Default, version) == expected + + def test_no_available_algorithms(self): + """expect exceptions on no available algorithms or when all algorithms throw""" + self.assertRaises(SCons.Errors.SConsEnvironmentError, _set_allowed_viable_default_hashes, self.no_algorithms) + self.assertRaises(SCons.Errors.SConsEnvironmentError, _set_allowed_viable_default_hashes, self.all_throw) + self.assertRaises(SCons.Errors.SConsEnvironmentError, _set_allowed_viable_default_hashes, self.unsupported_algorithm) + + def test_bad_algorithm_set_attempt(self): + """expect exceptions on user setting an unsupported algorithm selections, either by host or by SCons""" + + # nonexistant hash algorithm, not supported by SCons + _set_allowed_viable_default_hashes(self.md5Available) + self.assertRaises(SCons.Errors.UserError, set_hash_format, 'blah blah blah', hashlib_used=self.no_algorithms) + + # md5 is default-allowed, but in this case throws when we attempt to use it + _set_allowed_viable_default_hashes(self.md5Available) + self.assertRaises(SCons.Errors.UserError, set_hash_format, 'md5', hashlib_used=self.all_throw) + + # user attempts to use an algorithm that isn't supported by their current system but is supported by SCons + _set_allowed_viable_default_hashes(self.sha1Default) + self.assertRaises(SCons.Errors.UserError, set_hash_format, 'md5', hashlib_used=self.all_throw) + + # user attempts to use an algorithm that is supported by their current system but isn't supported by SCons + _set_allowed_viable_default_hashes(self.sha1Default) + self.assertRaises(SCons.Errors.UserError, set_hash_format, 'unsupported', hashlib_used=self.unsupported_algorithm) + + def tearDown(self): + """Return SCons back to the normal global state for the hashing functions.""" + _set_allowed_viable_default_hashes(hashlib, sys) + set_hash_format(None) class NodeListTestCase(unittest.TestCase): diff --git a/test/Configure/ConfigureDryRunError.py b/test/Configure/ConfigureDryRunError.py index 3648518..224154b 100644 --- a/test/Configure/ConfigureDryRunError.py +++ b/test/Configure/ConfigureDryRunError.py @@ -31,6 +31,8 @@ import os import TestSCons +from SCons.Util import get_current_hash_algorithm_used + _obj = TestSCons._obj test = TestSCons.TestSCons() @@ -65,7 +67,16 @@ test.run(arguments='-n', status=2, stderr=expect) test.must_not_exist('config.log') test.subdir('.sconf_temp') -conftest_0_c = os.path.join(".sconf_temp", "conftest_df286a1d2f67e69d030b4eff75ca7e12_0.c") +# depending on which default hash function we're using, we'd expect one of the following filenames. +# The filenames are generated by the conftest changes in #3543 : https://github.com/SCons/scons/pull/3543/files +possible_filenames = { + 'md5': "conftest_df286a1d2f67e69d030b4eff75ca7e12_0.c", + 'sha1': "conftest_6e784ac3248d146c68396335df2f9428d78c1d24_0.c", + 'sha256': "conftest_be4b3c1d600e20dfc3e8d98748cd8193c850331643466d800ef8a4229a5be410_0.c" +} +test_filename = possible_filenames[get_current_hash_algorithm_used()] + +conftest_0_c = os.path.join(".sconf_temp", test_filename) SConstruct_file_line = test.python_file_line(SConstruct_path, 6)[:-1] expect = """ diff --git a/test/Configure/VariantDir-SConscript.py b/test/Configure/VariantDir-SConscript.py index deb7b8f..5818fc7 100644 --- a/test/Configure/VariantDir-SConscript.py +++ b/test/Configure/VariantDir-SConscript.py @@ -128,7 +128,7 @@ test.checkLogAndStdout( ["Checking for C header file math.h... ", import shutil shutil.rmtree(test.workpath(".sconf_temp")) -test.unlink(".sconsign.dblite") +test.unlink(test.get_sconsignname()+".dblite") # now with SConscriptChdir(1) test.run(arguments='chdir=yes') diff --git a/test/Configure/implicit-cache.py b/test/Configure/implicit-cache.py index f4f3e94..4078a98 100644 --- a/test/Configure/implicit-cache.py +++ b/test/Configure/implicit-cache.py @@ -55,6 +55,7 @@ get longer and longer until it blew out the users's memory. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import TestSConsign +from SCons.Util import get_hash_format, get_current_hash_algorithm_used test = TestSConsign.TestSConsign() @@ -75,7 +76,28 @@ test.write('foo.h', "#define FOO 1\n") test.run(arguments = '.') -test.run_sconsign('-d .sconf_temp -e conftest_5a3fa36d51dd2a28d521d6cc0e2e1d04_0.c --raw .sconsign.dblite') +# depending on which default hash function we're using, we'd expect one of the following filenames. +# The filenames are generated by the conftest changes in #3543 : https://github.com/SCons/scons/pull/3543/files +# this test is different than the other tests, as the database name is used here. +# when the database defaults to md5, that's a different name than when the user selects md5 directly. +possible_filenames = { + 'default': "conftest_5a3fa36d51dd2a28d521d6cc0e2e1d04_0.c", + 'md5': "conftest_5a3fa36d51dd2a28d521d6cc0e2e1d04_0.c", + 'sha1': "conftest_80e5b88f2c7427a92f0e6c7184f144f874f10e60_0.c", + 'sha256': "conftest_ba8270c26647ad00993cd7777f4c5d3751018372b97d16eb993563bea051c3df_0.c" +} +# user left algorithm default, it defaulted to md5, with the special database name +if get_hash_format() is None and get_current_hash_algorithm_used() == 'md5': + test_filename = possible_filenames['default'] +# either user selected something (like explicitly setting md5) or algorithm defaulted to something else. +# SCons can default to something else if it detects the hashlib doesn't support it, example md5 in FIPS +# mode prior to Python 3.9 +else: + test_filename = possible_filenames[get_current_hash_algorithm_used()] + +database_name=test.get_sconsignname() + ".dblite" + +test.run_sconsign(f'-d .sconf_temp -e {test_filename} --raw {database_name}') old_sconsign_dblite = test.stdout() # Second run: Have the configure subsystem also look for foo.h, so @@ -88,11 +110,11 @@ old_sconsign_dblite = test.stdout() test.run(arguments = '--implicit-cache USE_FOO=1 .') -test.run_sconsign('-d .sconf_temp -e conftest_5a3fa36d51dd2a28d521d6cc0e2e1d04_0.c --raw .sconsign.dblite') +test.run_sconsign(f'-d .sconf_temp -e {test_filename} --raw {database_name}') new_sconsign_dblite = test.stdout() if old_sconsign_dblite != new_sconsign_dblite: - print(".sconsign.dblite did not match:") + print(f"{database_name} did not match:") print("FIRST RUN ==========") print(old_sconsign_dblite) print("SECOND RUN ==========") diff --git a/test/Configure/option--config.py b/test/Configure/option--config.py index f31336f..3b38892 100644 --- a/test/Configure/option--config.py +++ b/test/Configure/option--config.py @@ -32,6 +32,7 @@ import os.path from TestSCons import TestSCons, ConfigCheckInfo, _obj from TestCmd import IS_WINDOWS +from SCons.Util import get_current_hash_algorithm_used test = TestSCons() @@ -42,6 +43,11 @@ CR = test.CR # cached rebuild (up to date) NCF = test.NCF # non-cached build failure CF = test.CF # cached build failure +# as this test is somewhat complicated, skip it if the library doesn't support md5 +# as the default hashing algorithm. +if get_current_hash_algorithm_used() != 'md5': + test.skip_test('Skipping test as could not continue without the hash algorithm set to md5!') + SConstruct_path = test.workpath('SConstruct') test.write(SConstruct_path, """ diff --git a/test/Repository/variants.py b/test/Repository/variants.py index f89605b..c95e853 100644 --- a/test/Repository/variants.py +++ b/test/Repository/variants.py @@ -216,12 +216,14 @@ repository/src1/bbb.c: REPOSITORY_FOO repository/src1/main.c: REPOSITORY_FOO """) +database_name=test.get_sconsignname() + test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) test.run(program=repository_build2_foo_src2_xxx_xxx, stdout="""\ repository/src2/include/my_string.h: FOO @@ -236,11 +238,11 @@ repository/src2/xxx/main.c: BAR """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) # Make the entire repository non-writable, so we'll detect # if we try to write into it accidentally. @@ -270,11 +272,11 @@ repository/src1/main.c: REPOSITORY_BAR """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) test.up_to_date(chdir='work1', options=opts + " OS=bar", arguments='build1') @@ -301,11 +303,11 @@ repository/src1/main.c: WORK_BAR """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) test.up_to_date(chdir='work1', options=opts + " OS=bar", arguments='build1') @@ -319,11 +321,11 @@ repository/src1/main.c: WORK_FOO """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) test.up_to_date(chdir='work1', options=opts + " OS=foo", arguments='build1') @@ -376,11 +378,11 @@ repository/src2/xxx/main.c: BAR """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) # Ensure file time stamps will be newer. time.sleep(2) @@ -411,11 +413,11 @@ repository/src2/xxx/main.c: BAR """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) # test.unlink(['work2', 'src2', 'include', 'my_string.h']) @@ -435,11 +437,11 @@ repository/src2/xxx/main.c: BAR """) test.fail_test(os.path.exists( - test.workpath('repository', 'src1', '.sconsign'))) + test.workpath('repository', 'src1', database_name))) test.fail_test(os.path.exists( - test.workpath('repository', 'src2', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work1', 'src1', '.sconsign'))) -test.fail_test(os.path.exists(test.workpath('work2', 'src2', '.sconsign'))) + test.workpath('repository', 'src2', database_name))) +test.fail_test(os.path.exists(test.workpath('work1', 'src1', database_name))) +test.fail_test(os.path.exists(test.workpath('work2', 'src2', database_name))) # test.pass_test() diff --git a/test/option/hash-format.py b/test/option/hash-format.py index 9fa10ee..f0156f3 100644 --- a/test/option/hash-format.py +++ b/test/option/hash-format.py @@ -27,28 +27,52 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import hashlib import os import TestSCons +import warnings +from SCons.Util import ALLOWED_HASH_FORMATS, DEFAULT_HASH_FORMATS # Test passing the hash format by command-line. INVALID_ALGORITHM = 'testfailure' -for algorithm in ['md5', 'sha1', 'sha256', INVALID_ALGORITHM, None]: + +for algorithm in [*DEFAULT_HASH_FORMATS, INVALID_ALGORITHM, None]: test = TestSCons.TestSCons() test.dir_fixture('hash-format') + # Expect failure due to an unsupported/invalid algorithm. + # The error message however changes if SCons detects that the host system doesn't support one or more algorithms + # Primary reason the message changes is so user doesn't have to start with unsupported algorithm A and then attempt + # to switch to unsupported algorithm B. + # On normal systems (allowed=default) this will output a fixed message, but on FIPS-enabled or other weird systems + # that don't have default allowed algorithms, it informs the user of the mismatch _and_ the currently supported + # algorithms on the system they're using. + # In Python 3.9 this becomes somewhat obselete as the hashlib is informed we don't use hashing for security but + # for loose integrity. if algorithm == INVALID_ALGORITHM: - # Expect failure due to an unsupported/invalid algorithm. - test.run('--hash-format=%s .' % algorithm, stderr=r""" -scons: \*\*\* Hash format "{}" is not supported by SCons. Only the following hash formats are supported: md5, sha1, sha256 + if ALLOWED_HASH_FORMATS == DEFAULT_HASH_FORMATS: + test.run('--hash-format=%s .' % algorithm, stderr=r""" +scons: \*\*\* Hash format "{}" is not supported by SCons. Only the following hash formats are supported: {} +File "[^"]+", line \d+, in \S+ +""".format(algorithm, ', '.join(DEFAULT_HASH_FORMATS)), status=2, match=TestSCons.match_re) + else: + test.run('--hash-format=%s .' % algorithm, stderr=r""" +scons: \*\*\* Hash format "{}" is not supported by SCons. SCons supports more hash formats than your local system is reporting; SCons supports: {}. Your local system only supports: {} File "[^"]+", line \d+, in \S+ -""".format(algorithm), status=2, match=TestSCons.match_re) +""".format(algorithm, ', '.join(DEFAULT_HASH_FORMATS), ', '.join(ALLOWED_HASH_FORMATS)), status=2, match=TestSCons.match_re) continue elif algorithm is not None: - # Skip any algorithm that the Python interpreter doesn't have. - if hasattr(hashlib, algorithm): + if algorithm in ALLOWED_HASH_FORMATS: expected_dblite = test.workpath('.sconsign_%s.dblite' % algorithm) test.run('--hash-format=%s .' % algorithm) else: - print('Skipping test with --hash-format=%s because that ' - 'algorithm is not available.' % algorithm) + test.run('--hash-format=%s' % algorithm, stderr=r""" +scons: \*\*\* While hash format "{}" is supported by SCons, the local system indicates only the following hash formats are supported by the hashlib library: {} +File "[^"]+", line \d+, in \S+ +Error in atexit._run_exitfuncs: +Traceback \(most recent call last\): + File "[^"]+", line \d+, in \S+ + assert csig == '[a-z0-9]+', csig +AssertionError: [a-z0-9]+ +""".format(algorithm, ', '.join(ALLOWED_HASH_FORMATS)), status=2, match=TestSCons.match_re) + continue else: # The SConsign file in the hash-format folder has logic to call # SCons.Util.set_hash_format('sha256') if the default algorithm is diff --git a/test/option/option-n.py b/test/option/option-n.py index e647b8e..eb8eb62 100644 --- a/test/option/option-n.py +++ b/test/option/option-n.py @@ -43,6 +43,7 @@ import os import re import TestSCons +from SCons.Util import get_current_hash_algorithm_used _python_ = TestSCons._python_ @@ -118,7 +119,7 @@ test.fail_test(not os.path.exists(test.workpath('f1.out'))) expect = test.wrap_stdout("""\ %(_python_)s build.py f1.out """ % locals()) -test.unlink('.sconsign.dblite') +test.unlink(test.get_sconsignname()+'.dblite') test.write('f1.out', "X1.out\n") test.run(arguments='-n f1.out', stdout=expect) test.run(arguments='-n f1.out', stdout=expect) @@ -204,13 +205,23 @@ test.run(arguments="-n", stderr=stderr, status=2, test.fail_test(os.path.exists(test.workpath("configure", "config.test"))) test.fail_test(os.path.exists(test.workpath("configure", "config.log"))) + +# depending on which default hash function we're using, we'd expect one of the following filenames. +# The filenames are generated by the conftest changes in #3543 : https://github.com/SCons/scons/pull/3543/files +possible_filenames = { + 'md5': "conftest_b10a8db164e0754105b7a99be72e3fe5_0.in", + 'sha1': "conftest_0a4d55a8d778e5022fab701977c5d840bbc486d0_0.in", + 'sha256': "conftest_a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e_0.in" +} +test_filename = possible_filenames[get_current_hash_algorithm_used()] + # test that targets are not built, if conf_dir exists. # verify that .cache and config.log are not created. # an error should be raised stderr = r""" scons: \*\*\* Cannot update configure test "%s" within a dry-run\. File \S+, line \S+, in \S+ -""" % re.escape(os.path.join("config.test", "conftest_b10a8db164e0754105b7a99be72e3fe5_0.in")) +""" % re.escape(os.path.join("config.test", test_filename)) test.subdir(['configure', 'config.test']) test.run(arguments="-n", stderr=stderr, status=2, chdir=test.workpath("configure")) diff --git a/test/question/Configure.py b/test/question/Configure.py index 7df29f5..d01d0fa 100644 --- a/test/question/Configure.py +++ b/test/question/Configure.py @@ -36,6 +36,7 @@ import re import TestCmd import TestSCons +from SCons.Util import get_current_hash_algorithm_used test = TestSCons.TestSCons(match = TestCmd.match_re_dotall) @@ -80,13 +81,22 @@ test.run(arguments="-q aaa.out",stderr=stderr,status=2) test.must_not_exist(test.workpath("config.test")) test.must_not_exist(test.workpath("config.log")) +# depending on which default hash function we're using, we'd expect one of the following filenames. +# The filenames are generated by the conftest changes in #3543 : https://github.com/SCons/scons/pull/3543/files +possible_filenames = { + 'md5': "conftest_b10a8db164e0754105b7a99be72e3fe5_0.in", + 'sha1': "conftest_0a4d55a8d778e5022fab701977c5d840bbc486d0_0.in", + 'sha256': "conftest_a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e_0.in" +} +test_filename = possible_filenames[get_current_hash_algorithm_used()] + # test that targets are not built, if conf_dir exists. # verify that .cache and config.log are not created. # an error should be raised stderr=r""" scons: \*\*\* Cannot update configure test "%s" within a dry-run\. File \S+, line \S+, in \S+ -""" % re.escape(os.path.join("config.test", "conftest_b10a8db164e0754105b7a99be72e3fe5_0.in")) +""" % re.escape(os.path.join("config.test", test_filename)) test.subdir('config.test') @@ -94,7 +104,7 @@ test.run(arguments="-q aaa.out",stderr=stderr,status=2) test.must_not_exist(test.workpath("config.test", ".cache")) test.must_not_exist(test.workpath("config.test", "conftest_0")) -test.must_not_exist(test.workpath("config.test", "conftest_b10a8db164e0754105b7a99be72e3fe5_0.in")) +test.must_not_exist(test.workpath("config.test", test_filename)) test.must_not_exist(test.workpath("config.log")) # test that no error is raised, if all targets are up-to-date. In this diff --git a/test/sconsign/corrupt.py b/test/sconsign/corrupt.py index 25b48e2..61da3a2 100644 --- a/test/sconsign/corrupt.py +++ b/test/sconsign/corrupt.py @@ -30,14 +30,19 @@ Test that we get proper warnings when .sconsign* files are corrupt. import TestSCons import TestCmd +import re test = TestSCons.TestSCons(match = TestCmd.match_re) test.subdir('work1', ['work1', 'sub'], 'work2', ['work2', 'sub']) -work1__sconsign_dblite = test.workpath('work1', '.sconsign.dblite') -work2_sub__sconsign = test.workpath('work2', 'sub', '.sconsign') +database_filename = test.get_sconsignname() + ".dblite" + +# for test1 we're using the default database filename +work1__sconsign_dblite = test.workpath('work1', database_filename) +# for test 2 we have an explicit hardcode to .sconsign +work2_sub__sconsign = test.workpath('work2', 'sub', ".sconsign") SConstruct_contents = """\ def build1(target, source, env): @@ -57,9 +62,9 @@ test.write(['work1', 'SConstruct'], SConstruct_contents) test.write(['work1', 'foo.in'], "work1/foo.in\n") stderr = r''' -scons: warning: Ignoring corrupt .sconsign file: \.sconsign\.dblite +scons: warning: Ignoring corrupt .sconsign file: {} .* -''' +'''.format(re.escape(database_filename)) stdout = test.wrap_stdout(r'build1\(\["sub.foo\.out"\], \["foo\.in"\]\)' + '\n') diff --git a/test/sconsign/script/Configure.py b/test/sconsign/script/Configure.py index 7bf1f05..02a2c20 100644 --- a/test/sconsign/script/Configure.py +++ b/test/sconsign/script/Configure.py @@ -90,7 +90,9 @@ conftest_%(sig_re)s_0_%(sig_re)s%(_obj)s: %(CC_file)s: %(sig_re)s \d+ \d+ """ % locals() -test.run_sconsign(arguments = ".sconsign", +# grab .sconsign or .sconsign_<hashname> +database_name=test.get_sconsignname() +test.run_sconsign(arguments = database_name, stdout = expect) test.pass_test() diff --git a/testing/framework/TestSCons.py b/testing/framework/TestSCons.py index 2a19818..29bd123 100644 --- a/testing/framework/TestSCons.py +++ b/testing/framework/TestSCons.py @@ -45,6 +45,7 @@ from collections import namedtuple from TestCommon import * from TestCommon import __all__ +from SCons.Util import get_hash_format, get_current_hash_algorithm_used from TestCmd import Popen from TestCmd import PIPE @@ -719,6 +720,27 @@ class TestSCons(TestCommon): for p in patterns: result.extend(sorted(glob.glob(p))) return result + + def get_sconsignname(self): + """Get the scons database name used, and return both the prefix and full filename. + if the user left the options defaulted AND the default algorithm set by + SCons is md5, then set the database name to be the special default name + + otherwise, if it defaults to something like 'sha1' or the user explicitly + set 'md5' as the hash format, set the database name to .sconsign_<algorithm> + eg .sconsign_sha1, etc. + + Returns: + a pair containing: the current dbname, the dbname.dblite filename + """ + hash_format = get_hash_format() + current_hash_algorithm = get_current_hash_algorithm_used() + if hash_format is None and current_hash_algorithm == 'md5': + return ".sconsign" + else: + database_prefix=".sconsign_%s" % current_hash_algorithm + return database_prefix + def unlink_sconsignfile(self, name='.sconsign.dblite'): """Delete the sconsign file. |