diff --git a/Misc/NEWS b/Misc/NEWS index 3819d6cb7a7..c6ae2bb43cc 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -200,6 +200,9 @@ Tests Build ----- +- Issue #18215: Add script Tools/ssl/test_multiple_versions.py to compile and + run Python's unit tests with multiple versions of OpenSSL. + - Issue #19922: define _INCLUDE__STDC_A1_SOURCE in HP-UX to include mbstate_t for mbrtowc(). diff --git a/Tools/ssl/test_multiple_versions.py b/Tools/ssl/test_multiple_versions.py new file mode 100644 index 00000000000..dd57dcf18b8 --- /dev/null +++ b/Tools/ssl/test_multiple_versions.py @@ -0,0 +1,241 @@ +#./python +"""Run Python tests with multiple installations of OpenSSL + +The script + + (1) downloads OpenSSL tar bundle + (2) extracts it to ../openssl/src/openssl-VERSION/ + (3) compiles OpenSSL + (4) installs OpenSSL into ../openssl/VERSION/ + (5) forces a recompilation of Python modules using the + header and library files from ../openssl/VERSION/ + (6) runs Python's test suite + +The script must be run with Python's build directory as current working +directory: + + ./python Tools/ssl/test_multiple_versions.py + +The script uses LD_RUN_PATH, LD_LIBRARY_PATH, CPPFLAGS and LDFLAGS to bend +search paths for header files and shared libraries. It's known to work on +Linux with GCC 4.x. + +(c) 2013 Christian Heimes +""" +import logging +import os +import tarfile +import shutil +import subprocess +import sys +from urllib.request import urlopen + +log = logging.getLogger("multissl") + +OPENSSL_VERSIONS = [ + "0.9.7m", "0.9.8i", "0.9.8l", "0.9.8m", "0.9.8y", "1.0.0k", "1.0.1e" +] +FULL_TESTS = [ + "test_asyncio", "test_ftplib", "test_hashlib", "test_httplib", + "test_imaplib", "test_nntplib", "test_poplib", "test_smtplib", + "test_smtpnet", "test_urllib2_localnet", "test_venv" +] +MINIMAL_TESTS = ["test_ssl", "test_hashlib"] +CADEFAULT = True +HERE = os.path.abspath(os.getcwd()) +DEST_DIR = os.path.abspath(os.path.join(HERE, os.pardir, "openssl")) + + +class BuildSSL: + url_template = "https://www.openssl.org/source/openssl-{}.tar.gz" + + module_files = ["Modules/_ssl.c", + "Modules/socketmodule.c", + "Modules/_hashopenssl.c"] + + def __init__(self, version, openssl_compile_args=(), destdir=DEST_DIR): + self._check_python_builddir() + self.version = version + self.openssl_compile_args = openssl_compile_args + # installation directory + self.install_dir = os.path.join(destdir, version) + # source file + self.src_file = os.path.join(destdir, "src", + "openssl-{}.tar.gz".format(version)) + # build directory (removed after install) + self.build_dir = os.path.join(destdir, "src", + "openssl-{}".format(version)) + + @property + def openssl_cli(self): + """openssl CLI binary""" + return os.path.join(self.install_dir, "bin", "openssl") + + @property + def openssl_version(self): + """output of 'bin/openssl version'""" + env = os.environ.copy() + env["LD_LIBRARY_PATH"] = self.lib_dir + cmd = [self.openssl_cli, "version"] + return self._subprocess_output(cmd, env=env) + + @property + def pyssl_version(self): + """Value of ssl.OPENSSL_VERSION""" + env = os.environ.copy() + env["LD_LIBRARY_PATH"] = self.lib_dir + cmd = ["./python", "-c", "import ssl; print(ssl.OPENSSL_VERSION)"] + return self._subprocess_output(cmd, env=env) + + @property + def include_dir(self): + return os.path.join(self.install_dir, "include") + + @property + def lib_dir(self): + return os.path.join(self.install_dir, "lib") + + @property + def has_openssl(self): + return os.path.isfile(self.openssl_cli) + + @property + def has_src(self): + return os.path.isfile(self.src_file) + + def _subprocess_call(self, cmd, stdout=subprocess.DEVNULL, env=None, + **kwargs): + log.debug("Call '{}'".format(" ".join(cmd))) + return subprocess.check_call(cmd, stdout=stdout, env=env, **kwargs) + + def _subprocess_output(self, cmd, env=None, **kwargs): + log.debug("Call '{}'".format(" ".join(cmd))) + out = subprocess.check_output(cmd, env=env) + return out.strip().decode("utf-8") + + def _check_python_builddir(self): + if not os.path.isfile("python") or not os.path.isfile("setup.py"): + raise ValueError("Script must be run in Python build directory") + + def _download_openssl(self): + """Download OpenSSL source dist""" + src_dir = os.path.dirname(self.src_file) + if not os.path.isdir(src_dir): + os.makedirs(src_dir) + url = self.url_template.format(self.version) + log.info("Downloading OpenSSL from {}".format(url)) + req = urlopen(url, cadefault=CADEFAULT) + # KISS, read all, write all + data = req.read() + log.info("Storing {}".format(self.src_file)) + with open(self.src_file, "wb") as f: + f.write(data) + + def _unpack_openssl(self): + """Unpack tar.gz bundle""" + # cleanup + if os.path.isdir(self.build_dir): + shutil.rmtree(self.build_dir) + os.makedirs(self.build_dir) + + tf = tarfile.open(self.src_file) + base = "openssl-{}/".format(self.version) + # force extraction into build dir + members = tf.getmembers() + for member in members: + if not member.name.startswith(base): + raise ValueError(member.name) + member.name = member.name[len(base):] + log.info("Unpacking files to {}".format(self.build_dir)) + tf.extractall(self.build_dir, members) + + def _build_openssl(self): + """Now build openssl""" + log.info("Running build in {}".format(self.install_dir)) + cwd = self.build_dir + cmd = ["./config", "shared", "--prefix={}".format(self.install_dir)] + cmd.extend(self.openssl_compile_args) + self._subprocess_call(cmd, cwd=cwd) + self._subprocess_call(["make"], cwd=cwd) + + def _install_openssl(self, remove=True): + self._subprocess_call(["make", "install"], cwd=self.build_dir) + if remove: + shutil.rmtree(self.build_dir) + + def install_openssl(self): + if not self.has_openssl: + if not self.has_src: + self._download_openssl() + else: + log.debug("Already has src {}".format(self.src_file)) + self._unpack_openssl() + self._build_openssl() + self._install_openssl() + else: + log.info("Already has installation {}".format(self.install_dir)) + # validate installation + version = self.openssl_version + if self.version not in version: + raise ValueError(version) + + def touch_pymods(self): + # force a rebuild of all modules that use OpenSSL APIs + for fname in self.module_files: + os.utime(fname) + + def recompile_pymods(self): + log.info("Using OpenSSL build from {}".format(self.build_dir)) + # overwrite header and library search paths + env = os.environ.copy() + env["CPPFLAGS"] = "-I{}".format(self.include_dir) + env["LDFLAGS"] = "-L{}".format(self.lib_dir) + # set rpath + env["LD_RUN_PATH"] = self.lib_dir + + log.info("Rebuilding Python modules") + self.touch_pymods() + cmd = ["./python", "setup.py", "build"] + self._subprocess_call(cmd, env=env) + + def check_pyssl(self): + version = self.pyssl_version + if self.version not in version: + raise ValueError(version) + + def run_pytests(self, *args): + cmd = ["./python", "-m", "test"] + cmd.extend(args) + self._subprocess_call(cmd, stdout=None) + + def run_python_tests(self, *args): + self.recompile_pymods() + self.check_pyssl() + self.run_pytests(*args) + + +def main(*args): + builders = [] + for version in OPENSSL_VERSIONS: + if version in ("0.9.8i", "0.9.8l"): + openssl_compile_args = ("no-asm",) + else: + openssl_compile_args = () + builder = BuildSSL(version, openssl_compile_args) + builder.install_openssl() + builders.append(builder) + + for builder in builders: + builder.run_python_tests(*args) + # final touch + builder.touch_pymods() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, + format="*** %(levelname)s %(message)s") + args = sys.argv[1:] + if not args: + args = ["-unetwork", "-v"] + args.extend(FULL_TESTS) + main(*args)