diff --git a/mercurial/parsers.c b/mercurial/parsers.c --- a/mercurial/parsers.c +++ b/mercurial/parsers.c @@ -14,6 +14,8 @@ #include "util.h" +static char *versionerrortext = "Python minor version mismatch"; + static int8_t hextable[256] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, @@ -1911,6 +1913,16 @@ void dirs_module_init(PyObject *mod); static void module_init(PyObject *mod) { + /* This module constant has two purposes. First, it lets us unit test + * the ImportError raised without hard-coding any error text. This + * means we can change the text in the future without breaking tests, + * even across changesets without a recompile. Second, its presence + * can be used to determine whether the version-checking logic is + * present, which also helps in testing across changesets without a + * recompile. Note that this means the pure-Python version of parsers + * should not have this module constant. */ + PyModule_AddStringConstant(mod, "versionerrortext", versionerrortext); + dirs_module_init(mod); indexType.tp_new = PyType_GenericNew; @@ -1928,6 +1940,24 @@ static void module_init(PyObject *mod) dirstate_unset = Py_BuildValue("ciii", 'n', 0, -1, -1); } +static int check_python_version(void) +{ + PyObject *sys = PyImport_ImportModule("sys"); + long hexversion = PyInt_AsLong(PyObject_GetAttrString(sys, "hexversion")); + /* sys.hexversion is a 32-bit number by default, so the -1 case + * should only occur in unusual circumstances (e.g. if sys.hexversion + * is manually set to an invalid value). */ + if ((hexversion == -1) || (hexversion >> 16 != PY_VERSION_HEX >> 16)) { + PyErr_Format(PyExc_ImportError, "%s: The Mercurial extension " + "modules were compiled with Python " PY_VERSION ", but " + "Mercurial is currently using Python with sys.hexversion=%ld: " + "Python %s\n at: %s", versionerrortext, hexversion, + Py_GetVersion(), Py_GetProgramFullPath()); + return -1; + } + return 0; +} + #ifdef IS_PY3K static struct PyModuleDef parsers_module = { PyModuleDef_HEAD_INIT, @@ -1939,6 +1969,8 @@ static struct PyModuleDef parsers_module PyMODINIT_FUNC PyInit_parsers(void) { + if (check_python_version() == -1) + return; PyObject *mod = PyModule_Create(&parsers_module); module_init(mod); return mod; @@ -1946,6 +1978,8 @@ PyMODINIT_FUNC PyInit_parsers(void) #else PyMODINIT_FUNC initparsers(void) { + if (check_python_version() == -1) + return; PyObject *mod = Py_InitModule3("parsers", methods, parsers_doc); module_init(mod); } diff --git a/tests/test-parseindex2.py b/tests/test-parseindex2.py --- a/tests/test-parseindex2.py +++ b/tests/test-parseindex2.py @@ -1,8 +1,13 @@ -"""This unit test tests parsers.parse_index2().""" +"""This unit test primarily tests parsers.parse_index2(). + +It also checks certain aspects of the parsers module as a whole. +""" from mercurial import parsers from mercurial.node import nullid, nullrev import struct +import subprocess +import sys # original python implementation def gettype(q): @@ -95,7 +100,70 @@ def parse_index2(data, inline): index, chunkcache = parsers.parse_index2(data, inline) return list(index), chunkcache +def importparsers(hexversion): + """Import mercurial.parsers with the given sys.hexversion.""" + # The file parsers.c inspects sys.hexversion to determine the version + # of the currently-running Python interpreter, so we monkey-patch + # sys.hexversion to simulate using different versions. + code = ("import sys; sys.hexversion=%s; " + "import mercurial.parsers" % hexversion) + cmd = "python -c \"%s\"" % code + # We need to do these tests inside a subprocess because parser.c's + # version-checking code happens inside the module init function, and + # when using reload() to reimport an extension module, "The init function + # of extension modules is not called a second time" + # (from http://docs.python.org/2/library/functions.html?#reload). + p = subprocess.Popen(cmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + return p.communicate() # returns stdout, stderr + +def printhexfail(testnumber, hexversion, stdout, expected): + try: + hexstring = hex(hexversion) + except TypeError: + hexstring = None + print ("FAILED: version test #%s with Python %s and patched " + "sys.hexversion %r (%r):\n Expected %s but got:\n-->'%s'\n" % + (testnumber, sys.version_info, hexversion, hexstring, expected, + stdout)) + +def testversionokay(testnumber, hexversion): + stdout, stderr = importparsers(hexversion) + if stdout: + printhexfail(testnumber, hexversion, stdout, expected="no stdout") + +def testversionfail(testnumber, hexversion): + stdout, stderr = importparsers(hexversion) + # We include versionerrortext to distinguish from other ImportErrors. + errtext = "ImportError: %s" % parsers.versionerrortext + if errtext not in stdout: + printhexfail(testnumber, hexversion, stdout, + expected="stdout to contain %r" % errtext) + +def makehex(major, minor, micro): + return int("%x%02x%02x00" % (major, minor, micro), 16) + +def runversiontests(): + """Check the version-detection logic when importing parsers.""" + info = sys.version_info + major, minor, micro = info[0], info[1], info[2] + # Test same major-minor versions. + testversionokay(1, makehex(major, minor, micro)) + testversionokay(2, makehex(major, minor, micro + 1)) + # Test different major-minor versions. + testversionfail(3, makehex(major + 1, minor, micro)) + testversionfail(4, makehex(major, minor + 1, micro)) + testversionfail(5, "'foo'") + def runtest() : + # Only test the version-detection logic if it is present. + try: + parsers.versionerrortext + except AttributeError: + pass + else: + runversiontests() + # Check that parse_index2() raises TypeError on bad arguments. try: parse_index2(0, True)