Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django test runner fails in virtualenv on Ubuntu

I've been struggling with a problem with the Django test runner installed in a Python virtualenv on Ubuntu 14.04. The same software runs fine on MacOS, and I think it was fine on an earlier version of Ubuntu.

The failure message is:

ImportError: '<test>' module incorrectly imported from '<base-env>/local/lib/python2.7/site-packages/<package-dir>'. Expected '<base-env>/lib/python2.7/site-packages/<package-dir>'. Is this module globally installed?

And the full stack trace from the error is:

  Traceback (most recent call last):
    File "/home/annalist/anenv/bin/django-admin", line 11, in <module>
      sys.exit(execute_from_command_line())
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 385, in execute_from_command_line
      utility.execute()
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 377, in execute
      self.fetch_command(subcommand).run_from_argv(self.argv)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/commands/test.py", line 50, in run_from_argv
      super(Command, self).run_from_argv(argv)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/base.py", line 288, in run_from_argv
      self.execute(*args, **options.__dict__)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/commands/test.py", line 71, in execute
      super(Command, self).execute(*args, **options)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/base.py", line 338, in execute
      output = self.handle(*args, **options)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/core/management/commands/test.py", line 88, in handle
      failures = test_runner.run_tests(test_labels)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/test/runner.py", line 147, in run_tests
      suite = self.build_suite(test_labels, extra_tests)
    File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/test/runner.py", line 96, in build_suite
      tests = self.test_loader.discover(start_dir=label, **kwargs)
    File "/usr/lib/python2.7/unittest/loader.py", line 206, in discover
      tests = list(self._find_tests(start_dir, pattern))
    File "/usr/lib/python2.7/unittest/loader.py", line 287, in _find_tests
      for test in self._find_tests(full_path, pattern):
    File "/usr/lib/python2.7/unittest/loader.py", line 287, in _find_tests
      for test in self._find_tests(full_path, pattern):
    File "/usr/lib/python2.7/unittest/loader.py", line 267, in _find_tests
      raise ImportError(msg % (mod_name, module_dir, expected_dir))
  ImportError: 'test_entity' module incorrectly imported from '/home/annalist/anenv/local/lib/python2.7/site-packages/annalist_root/annalist/tests'. Expected '/home/annalist/anenv/lib/python2.7/site-packages/annalist_root/annalist/tests'. Is this module globally installed?

The test cases run fine in the development environment, and they also run fine when installed from a source distribution kit into a new virtualenv environment on the MAcOS development host. But when I install the same package into a new virtualenv on an Ubuntu 14.04 host, the test runner fails with the above message.

The problems came up in a management utility I created that invokes some functions of django-admin (as well as some other stuff).

Web searches revealed reports of bugs with virtualenv and posix compatibility, which have been addressed relatively recently (2013/14) in Ubuntu distributions by creating a local directory in the virtual environment which in turn contains symlinks to directories that are also accessible from the top-level virtual environment directory. The paths shown in the error message correspond to these aliased directory paths.

(I'm posting this as a question so I can post the outcome and answer from my investigations, in the hope that it might be useful to others. Hence, I'm not trying to give a detailed description of my particular software setup.)

like image 667
Graham Klyne Avatar asked Sep 10 '14 21:09

Graham Klyne


2 Answers

I had exactly the same problem and couldn't figure out what was going on. Finally it was a stupid thing:

I had a layout similar to this one:

my_app/
    __init__.py
    tests.py
    tests/
        __init__.py
       test_foo.py

The problem was generated by having both a "tests.py" module and a "tests" package in the same folder.

Just deleting the "tests.py" file solved the problem for me.

Hope it helps.

like image 82
Martin Zugnoni Avatar answered Nov 12 '22 20:11

Martin Zugnoni


Short answer

The fix in my code was to use os.path.realpath to get a canonicalized version of the installed package path, and pass this value on the command line that invokes the django-admin utility. In my case it looks something like this:

approot = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))

and:

with ChangeCurrentDir(approot):
    subprocess_command = (
        "django-admin test --pythonpath=%s --settings=%s --top-level-directory=%s"%
            (approot, settings_module_name, approot)
        )
    status = subprocess.call(subprocess_command.split())

(where ChangeCurrentDir is a context handler that runs the enclosed code with a specified current working directory).

More details

Some further experimentation showed that I could "fix" the problem by strategic replacement of os.path.abspath with os.path.realpath in the Python and/or Django library code.

The core problem I found was in:

/usr/lib/python2.7/unittest/loader.py

Specifically:

File "/usr/lib/python2.7/unittest/loader.py", line 267, in _find_tests
    raise ImportError(msg % (mod_name, module_dir, expected_dir))
ImportError: 'test_entity' module incorrectly imported from ...

The offending code in loader.pyleading up to this is:

        if realpath.lower() != fullpath_noext.lower():
            module_dir = os.path.dirname(realpath)
            mod_name = os.path.splitext(os.path.basename(full_path))[0]
            expected_dir = os.path.dirname(full_path)
            msg = ("%r module incorrectly imported from %r. Expected %r. "
                   "Is this module globally installed?")
            raise ImportError(msg % (mod_name, module_dir, expected_dir))

If I replace the if statement with this:

        if os.path.realpath(realpath).lower() != fullpath_noext.lower():

Then everything is happy. This confirms that it is a symlink aliasing problem, as os.path.realpath() resolves any symlinks to the underlying path. But this is not a solution for an installable software package, as it involves modifying the underlying Python installation. So, having probed the underlying problem, I need for something more accessible to attack.

Next port of call was the Django test runner library, which is installed in the virtual environment.

<base-env>/local/lib/python2.7/site-packages/django/test/runner.py

In particular, focusing on this part of the stack trace:

  File "/home/annalist/anenv/local/lib/python2.7/site-packages/django/test/runner.py", line 96, in build_suite
    tests = self.test_loader.discover(start_dir=label, **kwargs)

Digging around this code, I was able to identify the problem was related to the label parameter, which defaults to '.' (i.e. current directory). No easy fix is apparent here, but it suggests the problem may be related to the current directory and/or path used when running django-admin. This led to the above solution (which may be overkill - I'm not sure that the --pythonpath= option is needed, but it is working for me).

like image 31
Graham Klyne Avatar answered Nov 12 '22 22:11

Graham Klyne