@@ -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,
@@ -1924,6 +1926,16 @@
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;
@@ -1941,6 +1953,24 @@
dirstate_unset = Py_BuildValue("ciii", 'n', 0, -1, -1);
}
+static int check_python_version()
+{
+ 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,
@@ -1952,6 +1982,8 @@
PyMODINIT_FUNC PyInit_parsers(void)
{
+ if (check_python_version() == -1)
+ return;
PyObject *mod = PyModule_Create(&parsers_module);
module_init(mod);
return mod;
@@ -1959,6 +1991,8 @@
#else
PyMODINIT_FUNC initparsers(void)
{
+ if (check_python_version() == -1)
+ return;
PyObject *mod = Py_InitModule3("parsers", methods, parsers_doc);
module_init(mod);
}
@@ -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 @@
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)