Repository: easybuilders/easybuild-framework

Issue:
DEISA fork of modulecmd.tcl is missing a proper header/hashbang, which Tcl environment modules can't handle
The DEISA fork of `modulecmd.tcl`, which is used on several big systems like JUROPA @ JSC, doesn't have a proper `tclsh` hashbang, which is causing problems when a command like `modulecmd.tcl python load GCC/4.6.3` is being run via `subprocess.Popen`, which runs it in a bash shell by default.

For Tcl environment modules, any `subprocess.Popen` calls of `modulecmd.tcl` should be using the `tclsh` shell instead to dance around this problem.



Discussion:
@boegel can be easily fixed by defining the executable in run_cmd and passing it to the Popen call. also make this change for run_qa; and try to add a unit test for this (ie so something will break when using vsc.utils.run).

Hi,

I was sitting next to Alan when this issue emerged and,
if I recall correctly, the offer above fixes only half of the issue (ie. picking up the correct interpreter);
the other half would be to ensure that tclsh can pick modulecmd.tcl from the correct path
(ie. unlike bash, it does not iterate over `$PATH` to find it), so some extra fix is needed about it.


Resolving patch:
diff --git a/easybuild/tools/config.py b/easybuild/tools/config.py
--- a/easybuild/tools/config.py
+++ b/easybuild/tools/config.py
@@ -328,16 +328,6 @@ def create_dir(dirtype, dirname):
                     _log.warn('The %s directory %s does not exist or does not have proper permissions' % (key, directory))
                     create_dir(key, directory)
 
-    # update MODULEPATH if required
-    ebmodpath = os.path.join(install_path(typ='modules'), 'all')
-    # modulepath without ebmodpath
-    # ebmodpath is then always inserted in 1st place
-    modulepath = [x for x in os.environ.get('MODULEPATH', '').split(':') if len(x) > 0 and not x == ebmodpath]
-    _log.info("Prepend MODULEPATH %s with module install path used by EasyBuild %s" % (modulepath, ebmodpath))
-    modulepath.insert(0, ebmodpath)
-
-    os.environ['MODULEPATH'] = ':'.join(modulepath)
-
 
 def build_path():
     """
diff --git a/easybuild/tools/filetools.py b/easybuild/tools/filetools.py
--- a/easybuild/tools/filetools.py
+++ b/easybuild/tools/filetools.py
@@ -176,7 +176,21 @@ def extract_file(fn, dest, cmd=None, extra_options=None, overwrite=False):
 
     return find_base_dir()
 
+
+def which(cmd):
+    """Return (first) path in $PATH for specified command, or None if command is not found."""
+    paths = os.environ.get('PATH', '').split(os.pathsep)
+    for path in paths:
+        cmd_path = os.path.join(path, cmd)
+        # only accept path is command is there, and both readable and executable
+        if os.access(cmd_path, os.R_OK | os.X_OK):
+            _log.info("Command %s found at %s" % (cmd, cmd_path))
+            return cmd_path
+    _log.warning("Could not find command '%s' (with permissions to read/execute it) in $PATH (%s)" % (cmd, paths))
+    return None
+
 def download_file(filename, url, path):
+    """Download a file from the given URL, to the specified path."""
 
     _log.debug("Downloading %s from %s to %s" % (filename, url, path))
 
diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py
--- a/easybuild/tools/modules.py
+++ b/easybuild/tools/modules.py
@@ -46,9 +46,9 @@
 from vsc.utils.missing import get_subclasses
 
 from easybuild.tools.build_log import EasyBuildError
-from easybuild.tools.config import get_modules_tool
-from easybuild.tools.filetools import convert_name, run_cmd, read_file
-from easybuild.tools.module_generator import det_full_module_name, DEVEL_MODULE_SUFFIX
+from easybuild.tools.config import get_modules_tool, install_path
+from easybuild.tools.filetools import convert_name, run_cmd, read_file, which
+from easybuild.tools.module_generator import det_full_module_name, DEVEL_MODULE_SUFFIX, GENERAL_CLASS
 from easybuild.tools.toolchain import DUMMY_TOOLCHAIN_NAME, DUMMY_TOOLCHAIN_VERSION
 from vsc.utils.missing import nub
 
@@ -61,7 +61,7 @@
 # see e.g., https://bugzilla.redhat.com/show_bug.cgi?id=719785
 LD_LIBRARY_PATH = os.getenv('LD_LIBRARY_PATH', '')
 
-outputMatchers = {
+output_matchers = {
     # matches whitespace and module-listing headers
     'whitespace': re.compile(r"^\s*$|^(-+).*(-+)$"),
     # matches errors such as "cmdTrace.c(713):ERROR:104: 'asdfasdf' is an unrecognized subcommand"
@@ -69,10 +69,48 @@
     # ModuleCmd_Avail.c(529):ERROR:57: Error while reading directory '/usr/local/modulefiles/SCIENTIFIC'
     # ModuleCmd_Avail.c(804):ERROR:64: Directory '/usr/local/modulefiles/SCIENTIFIC/tremolo' not found
     'error': re.compile(r"^\S+:(?P<level>\w+):(?P<code>(?!57|64)\d+):\s+(?P<msg>.*)$"),
-    # available with --terse has one module per line
-    # matches modules such as "ictce/3.2.1.015.u4"
-    # lines ending in ':' are ignored (the modulepath(s) in --terse)
-    'available': re.compile(r"^\s*(?P<mod_name>[^\(\s:]+)(?P<default>\(default\))?\s*[^:\S]*$"),
+    # 'available' with --terse has one module per line, with some extra lines (module path(s), module directories...)
+    # regex below matches modules like 'ictce/3.2.1.015.u4', 'OpenMPI/1.6.4-no-OFED', ...
+    #
+    # Module lines notes:
+    # * module name may have '(default)' appeneded [modulecmd]
+    # ignored lines:
+    # * module paths lines may start with a (empty) set of '-'s, which will be followed by a space [modulecmd.tcl]
+    # * module paths may end with a ':' [modulecmd, lmod]
+    # * module directories lines may end with a '/' [lmod >= 5.1.5]
+    #
+    # Note: module paths may be relative paths!
+    #
+    # Example outputs for the different supported module tools, for the same set of modules files (only two, both GCC):
+    #
+    #   $ modulecmd python avail --terse GCC > /dev/null
+    #       /path/tomodules:
+    #       GCC/4.6.3
+    #       GCC/4.6.4(default)
+    #
+    #   $ lmod python avail --terse GCC > /dev/null
+    #       /path/to/modules:
+    #       GCC/
+    #       GCC/4.6.3
+    #       GCC/4.6.4
+    #
+    #   $ modulecmd.tcl python avail -t GCC > /dev/null
+    #       -------- /path/to/modules --------
+    #       GCC/4.6.3
+    #       GCC/4.6.4
+    #
+    # Note on modulecmd.tcl: if the terminal is not wide enough, or the module path too long, the '-'s are not there!
+    #
+    # Any modules with a name that does not match the regex constructed below, will be HIDDEN from EasyBuild
+    #
+    'available': re.compile(r"""
+        ^(?!-*\s)                     # disallow lines starting with (empty) list of '-'s followed by a space
+        (?P<mod_name>                 # start named group for module name
+            [^\s\(]*[^:/]             # module name must not have '(' or whitespace in it, must not end with ':' or '/'
+        )                             # end named group for module name
+        (?P<default>\(default\))?     # optional '(default)' that's not part of module name
+        \s*$                          # ignore whitespace at the end of the line
+        """, re.VERBOSE),
 }
 
 _log = fancylogger.getLogger('modules', fname=False)
@@ -102,6 +140,9 @@ def __init__(self, mod_paths=None):
         # actual module command (i.e., not the 'module' wrapper function, but the binary)
         self.cmd = None
 
+        # shell that should be used to run module command (specified above) in (if any)
+        self.shell = None
+
         # version of modules tool
         self.version = None
 
@@ -116,36 +157,49 @@ def modules(self):
 
     def check_cmd_avail(self):
         """Check whether modules tool command is available."""
-        which_ec = subprocess.call(["which", self.cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        if which_ec != 0:
-            self.log.error("%s modules tool can not be used, '%s' command is not available." % (self.__class__.__name__, self.cmd))
+        cmd_path = which(self.cmd)
+        if cmd_path is not None:
+            self.cmd = cmd_path
+            self.log.info("Full path for module command is %s, so using it" % self.cmd)
+        else:
+            mod_tool = self.__class__.__name__
+            self.log.error("%s modules tool can not be used, '%s' command is not available." % (mod_tool, self.cmd))
 
     def check_module_path(self):
         """
         Check if MODULEPATH is set and change it if necessary.
         """
-        if not 'MODULEPATH' in os.environ:
-            errormsg = 'MODULEPATH not found in environment'
-            # check if environment-modules is found
-            module_regexp = re.compile(r"^module is a function\s*\nmodule\s*()")
-            cmd = "type module"
-            (out, ec) = run_cmd(cmd, log_all=False, log_ok=False)
-            if ec != 0 or not module_regexp.match(out):
-                errormsg += "; environment-modules doesn't seem to be installed: "
-                errormsg += "'%s' failed with exit code %s and output: '%s'" % (cmd, ec, out.strip('\n'))
-            self.log.error(errormsg)
-
-        if self.mod_paths:
-            # set the module path environment accordingly
-            os.environ['MODULEPATH'] = ':'.join(self.mod_paths)
-            self.log.debug("$MODULEPATH set based on supplied list of module paths: %s" % os.environ['MODULEPATH'])
-        else:
+        # if self.mod_paths is not specified, use $MODULEPATH and make sure the EasyBuild module path is in there (first)
+        if self.mod_paths is None:
             # take module path from environment
-            self.mod_paths = nub(os.environ['MODULEPATH'].split(':'))
+            self.mod_paths = [x for x in nub(os.environ.get('MODULEPATH', '').split(':')) if len(x) > 0]
             self.log.debug("self.mod_paths set based on $MODULEPATH: %s" % self.mod_paths)
 
-        if not 'LOADEDMODULES' in os.environ:
-            os.environ['LOADEDMODULES'] = ''
+            # determine module path for EasyBuild install path to be included in $MODULEPATH
+            eb_modpath = os.path.join(install_path(typ='modules'), GENERAL_CLASS)
+
+            # make sure EasyBuild module path is in 1st place
+            self.mod_paths = [x for x in self.mod_paths if not x == eb_modpath]
+            self.mod_paths.insert(0, eb_modpath)
+            self.log.info("Prepended list of module paths with path used by EasyBuild: %s" % eb_modpath)
+
+        # set the module path environment accordingly
+        os.environ['MODULEPATH'] = ':'.join(self.mod_paths)
+        self.log.info("$MODULEPATH set based on list of module paths: %s" % os.environ['MODULEPATH'])
+
+    def use_module_paths(self):
+        """Run 'module use' on all paths in $MODULEPATH."""
+        # we need to run '<module command> python use <path>' on all paths in $MODULEPATH
+        # not all modules tools follow whatever is in $MODULEPATH, some take extra action for every module path
+        # usually, additional environment variables are set, e.g. $LMOD_DEFAULT_MODULEPATH or $MODULEPATH_modshare
+        # note: we're stepping through the mod_paths in reverse order to preserve order in $MODULEPATH in the end
+        for modpath in self.mod_paths[::-1]:
+            if not os.path.isabs(modpath):
+                modpath = os.path.join(os.getcwd(), modpath)
+            if os.path.exists(modpath):
+                self.run_module(['use', modpath])
+            else:
+                self.log.warning("Ignoring non-existing module path in $MODULEPATH: %s" % modpath)
 
     def available(self, mod_name=None):
         """
@@ -279,6 +333,10 @@ def module_software_name(self, mod_name):
         """Get the software name for a given module name."""
         raise NotImplementedError
 
+    def set_ld_library_path(self, ld_library_paths):
+        """Set $LD_LIBRARY_PATH to the given list of paths."""
+        os.environ['LD_LIBRARY_PATH'] = ':'.join(ld_library_paths)
+
     def run_module(self, *args, **kwargs):
         """
         Run module command.
@@ -289,41 +347,61 @@ def run_module(self, *args, **kwargs):
             args = list(args)
 
         if args[0] in ('available', 'avail', 'list',):
-            self.add_terse_opt_fn(args)  # run these in terse mode for better machinereading
-
-        originalModulePath = os.environ['MODULEPATH']
-        if kwargs.get('mod_paths', None):
-            os.environ['MODULEPATH'] = kwargs.get('mod_paths')
-        elif kwargs.get('modulePath', None):
-            os.environ['MODULEPATH'] = kwargs.get('modulePath')
-            self.log.deprecated("Use of 'modulePath' named argument in 'run_module', should use 'mod_paths'.", "2.0")
+            self.add_terse_opt_fn(args)  # run these in terse mode for easier machine reading
+
+        module_path_key = None
+        original_module_path = None
+        if 'mod_paths' in kwargs:
+            module_path_key =  'mod_paths'
+        elif 'modulePath' in kwargs:
+            module_path_key =  'modulePath'
+        if module_path_key is not None:
+            original_module_path = os.environ['MODULEPATH']
+            os.environ['MODULEPATH'] = kwargs[module_path_key]
+            self.log.deprecated("Use of '%s' named argument in 'run_module'" % module_path_key, '2.0')
+
+        # after changing $MODULEPATH, we should adjust self.mod_paths and run use_module_paths(),
+        # but we can't do that here becaue it would yield infinite recursion on run_module
         self.log.debug('Current MODULEPATH: %s' % os.environ['MODULEPATH'])
-        self.log.debug("Running '%s python %s' from %s..." % (self.cmd, ' '.join(args), os.getcwd()))
+
         # change our LD_LIBRARY_PATH here
         environ = os.environ.copy()
         environ['LD_LIBRARY_PATH'] = LD_LIBRARY_PATH
-        self.log.debug("Adjusted LD_LIBRARY_PATH from '%s' to '%s'" %
-                       (os.environ.get('LD_LIBRARY_PATH', ''), environ['LD_LIBRARY_PATH']))
+        cur_ld_library_path = os.environ.get('LD_LIBRARY_PATH', '')
+        new_ld_library_path = environ['LD_LIBRARY_PATH']
+        self.log.debug("Adjusted LD_LIBRARY_PATH from '%s' to '%s'" % (cur_ld_library_path, new_ld_library_path))
+
+        # prefix if a particular shell is specified, using shell argument to Popen doesn't work (no output produced (?))
+        cmdlist = [self.cmd, 'python']
+        if self.shell is not None:
+            cmdlist.insert(0, self.shell)
 
-        # module command is now getting an outdated LD_LIBRARY_PATH, which will be adjusted on loading a module
-        # this needs to be taken into account when updating the environment via produced output, see below
-        self.log.debug("Running module cmd '%s python %s'" % (self.cmd, ' '.join(args)))
-        proc = subprocess.Popen([self.cmd, 'python'] + args, stdout=PIPE, stderr=PIPE, env=environ)
+        self.log.debug("Running module command '%s' from %s" % (' '.join(cmdlist + args), os.getcwd()))
+        proc = subprocess.Popen(cmdlist + args, stdout=PIPE, stderr=PIPE, env=environ)
         # stdout will contain python code (to change environment etc)
         # stderr will contain text (just like the normal module command)
         (stdout, stderr) = proc.communicate()
-        os.environ['MODULEPATH'] = originalModulePath
+        if original_module_path is not None:
+            os.environ['MODULEPATH'] = original_module_path
+            self.log.deprecated("Restoring $MODULEPATH back to what it was before running module command/.", '2.0')
+            # after changing $MODULEPATH, we should adjust self.mod_paths and run use_module_paths(),
+            # but we can't do that here becaue it would yield infinite recursion on run_module
 
         if kwargs.get('return_output', False):
             return stdout + stderr
         else:
+            # the module command was run with an outdated LD_LIBRARY_PATH, which will be adjusted on loading a module
+            # this needs to be taken into account when updating the environment via produced output, see below
+
             # keep track of current LD_LIBRARY_PATH, so we can correct the adjusted LD_LIBRARY_PATH below
             prev_ld_library_path = os.environ.get('LD_LIBRARY_PATH', '').split(':')[::-1]
 
             # Change the environment
             try:
-                clean_stdout = '\n'.join([line for line in stdout.split('\n') if line.startswith('os.environ[')])
-                exec clean_stdout
+                tweak_fn = kwargs.get('tweak_stdout')
+                if tweak_fn is not None:
+                    stdout = tweak_fn(stdout)
+                exec stdout
             except Exception, err:
                 out = "stdout: %s, stderr: %s" % (stdout, stderr)
                 raise EasyBuildError("Changing environment as dictated by module failed: %s (%s)" % (err, out))
@@ -335,27 +413,33 @@ def run_module(self, *args, **kwargs):
 
             self.log.debug("Correcting paths in LD_LIBRARY_PATH from %s to %s" %
                            (curr_ld_library_path, new_ld_library_path))
-            os.environ['LD_LIBRARY_PATH'] = ':'.join(new_ld_library_path)
+            self.set_ld_library_path(new_ld_library_path)
 
             # Process stderr
             result = []
             for line in stderr.split('\n'):  # IGNORE:E1103
-                if outputMatchers['whitespace'].search(line):
+                if output_matchers['whitespace'].search(line):
                     continue
 
-                error = outputMatchers['error'].search(line)
+                error = output_matchers['error'].search(line)
                 if error:
                     self.log.error(line)
                     raise EasyBuildError(line)
 
-                modules = outputMatchers['available'].finditer(line)
+                modules = output_matchers['available'].finditer(line)
                 for module in modules:
                     result.append(module.groupdict())
             return result
 
     def loaded_modules(self):
         """Return a list of loaded modules."""
-        raise NotImplementedError
+        # obtain list of loaded modules from 'module list' using --terse
+        mods = [mod['mod_name'] for mod in self.run_module('list')]
+
+        # filter out devel modules
+        loaded_modules = [mod for mod in mods if not mod.endswith(DEVEL_MODULE_SUFFIX)]
+
+        return loaded_modules
 
     # depth=sys.maxint should be equivalent to infinite recursion depth
     def dependencies_for(self, mod_name, depth=sys.maxint):
@@ -399,31 +483,15 @@ def __init__(self, *args, **kwargs):
         self.cmd = "modulecmd"
         self.check_cmd_avail()
 
+        # run 'modulecmd python use <path>' for all paths in $MODULEPATH
+        self.use_module_paths()
+
     def module_software_name(self, mod_name):
         """Get the software name for a given module name."""
         # line that specified conflict contains software name
         name_re = re.compile('^conflict\s*(?P<name>\S+).*$', re.M)
         return self.get_value_from_modulefile(mod_name, name_re)
 
-    def loaded_modules(self):
-        """Return a list of loaded modules."""
-
-        loaded_modules = []
-        mods = []
-
-        # 'modulecmd python list' doesn't yield anything useful, prints to stdout
-        # rely on $LOADEDMODULES
-        if os.getenv('LOADEDMODULES'):
-            # format: name1/version1:name2/version2:...:nameN/versionN
-            mods = os.getenv('LOADEDMODULES').split(':')
-        else:
-            self.log.debug("No way found to determine loaded modules, assuming no modules are loaded.")
-
-        # filter devel modules, since they cannot be split like this
-        loaded_modules = [mod for mod in mods if not mod.endswith(DEVEL_MODULE_SUFFIX)]
-
-        return loaded_modules
-
     def update(self):
         """Update after new modules were added."""
         pass
@@ -436,10 +504,59 @@ def __init__(self, *args, **kwargs):
         """Constructor, set modulecmd.tcl-specific class variable values."""
         super(EnvironmentModulesC, self).__init__(*args, **kwargs)
         self.cmd = 'modulecmd.tcl'
+        # older versions of modulecmd.tcl don't have a decent hashbang, so we run it under a tclsh shell
+        self.shell = 'tclsh'
         self.check_cmd_avail()
+
         # Tcl environment modules have no --terse (yet), -t must be added after the command ('avail', 'list', etc.)
         self.add_terse_opt_fn = lambda x: x.insert(1, '-t')
 
+        # run 'modulecmd.tcl python use <path>' for all paths in $MODULEPATH
+        self.use_module_paths()
+
+    def set_ld_library_path(self, ld_library_paths):
+        """Set $LD_LIBRARY_PATH to the given list of paths."""
+        super(EnvironmentModulesTcl, self).set_ld_library_path(ld_library_paths)
+        # for Tcl environment modules, we need to make sure the _modshare env var is kept in sync
+        os.environ['LD_LIBRARY_PATH_modshare'] = ':1:'.join(ld_library_paths)
+
+    def run_module(self, *args, **kwargs):
+        """
+        Run module command, tweak output that is exec'ed if necessary.
+        """
+        if isinstance(args[0], (list, tuple,)):
+            args = args[0]
+
+        # old versions of modulecmd.tcl spit out something like "exec '<file>'" for load commands,
+        # which is not correct Python code (and it knows, as the comments in modulecmd.tcl indicate)
+        # so, rewrite "exec '/tmp/modulescript_X'" to the correct "execfile('/tmp/modulescript_X')"
+        def tweak_stdout(txt):
+            """Tweak stdout before it's exec'ed as Python code."""
+            modulescript_regex = "^exec\s+[\"'](?P<modulescript>/tmp/modulescript_[0-9_]+)[\"']$"
+            return re.sub(modulescript_regex, r"execfile('\1')", txt)
+
+        tweak_stdout_fn = None
+        # for 'active' module (sub)commands that yield changes in environment, we need to tweak stdout before exec'ing
+        if args[0] in ['load', 'purge', 'unload', 'use']:
+            tweak_stdout_fn = tweak_stdout
+        kwargs.update({'tweak_stdout': tweak_stdout_fn})
+
+        return super(EnvironmentModulesTcl, self).run_module(*args, **kwargs)
+
+    def available(self, mod_name=None):
+        """
+        Return a list of available modules for the given (partial) module name;
+        use None to obtain a list of all available modules.
+
+        @param name: a (partial) module name for filtering (default: None)
+        """
+        mods = super(EnvironmentModulesTcl, self).available(mod_name=mod_name)
+        # strip off slash at beginning, if it's there
+        # under certain circumstances, modulecmd.tcl (DEISA variant) spits out available modules like this
+        clean_mods = [mod.lstrip(os.path.sep) for mod in mods]
+
+        return clean_mods
+
 
 class Lmod(ModulesTool):
     """Interface to Lmod."""
@@ -469,7 +586,7 @@ def __init__(self, *args, **kwargs):
             stdout = open(stdout_fn, 'w')
             stderr = open(stdout_fn, 'w')
             # version is printed in 'lmod help' output
-            subprocess.call(["lmod", "help"], stdout=stdout, stderr=stderr)
+            subprocess.call([self.cmd, "help"], stdout=stdout, stderr=stderr)
             stdout.close()
             stderr.close()
 
@@ -481,7 +598,7 @@ def __init__(self, *args, **kwargs):
                 self.version = LooseVersion(res.group('version'))
                 self.log.info("Found Lmod version %s" % self.version)
             else:
-                self.log.error("Failed to determine Lmod version from 'lmod help' output: %s" % txt)
+                self.log.error("Failed to determine Lmod version from '%s help' output: %s" % (self.cmd, txt))
             stderr.close()
         except (IOError, OSError), err:
             self.log.error("Failed to check Lmod version: %s" % err)
@@ -495,16 +612,8 @@ def __init__(self, *args, **kwargs):
             vers = (self.REQ_VERSION, self.OPT_VERSION, self.version)
             self.log.error("EasyBuild requires Lmod version >= %s (>= %s recommended), found v%s" % vers)
 
-        # we need to run 'lmod python use <path>' to make sure all paths in $MODULEPATH are taken into account
-        # note: we're stepping through the mod_paths in reverse order to preserve order in $MODULEPATH in the end
-        for modpath in self.mod_paths[::-1]:
-            if not os.path.isabs(modpath):
-                modpath = os.path.join(os.getcwd(), modpath)
-            full_cmd = [self.cmd, 'python', 'use', modpath]
-            self.log.debug("Running %s" % ' '.join(full_cmd))
-            proc = subprocess.Popen(full_cmd, stdout=PIPE, stderr=PIPE, env=os.environ)
-            (stdout, stderr) = proc.communicate()
-            exec stdout
+        # run 'lmod python use <path>' for all paths in $MODULEPATH
+        self.use_module_paths()
 
         # make sure lmod spider cache is up to date
         self.update()
@@ -543,12 +652,6 @@ def is_mod(mod):
 
         return correct_real_mods
 
-    def loaded_modules(self):
-        """Return a list of loaded modules."""
-        # run_module already returns a list of Python dictionaries for loaded modules
-        # only retain 'mod_name' keys, get rid of any other keys
-        return [mod['mod_name'] for mod in self.run_module('list')]
-
     def update(self):
         """Update after new modules were added."""
         cmd = ['spider', '-o', 'moduleT', os.environ['MODULEPATH']]
@@ -560,7 +663,7 @@ def update(self):
 
         try:
             cache_filefn = os.path.join(os.path.expanduser('~'), '.lmod.d', '.cache', 'moduleT.lua')
-            self.log.debug("Updating lmod spider cache %s with output from '%s'" % (cache_filefn, ' '.join(cmd)))
+            self.log.debug("Updating Lmod spider cache %s with output from '%s'" % (cache_filefn, ' '.join(cmd)))
             cache_dir = os.path.dirname(cache_filefn)
             if not os.path.exists(cache_dir):
                 os.makedirs(cache_dir)
@@ -634,7 +737,7 @@ def curr_module_paths():
     """
     Return a list of current module paths.
     """
-    return os.environ['MODULEPATH'].split(':')
+    return os.environ.get('MODULEPATH', '').split(':')
 
 
 def mk_module_path(paths):
@@ -657,7 +760,7 @@ def avail_modules_tools():
 
 def modules_tool(mod_paths=None):
     """
-    Return interface to modules tool (environment modules, lmod)
+    Return interface to modules tool (environment modules (C, Tcl), or Lmod)
     """
     # get_modules_tool might return none (e.g. if config was not initialized yet)
     modules_tool = get_modules_tool()


Test patch:
diff --git a/test/framework/filetools.py b/test/framework/filetools.py
--- a/test/framework/filetools.py
+++ b/test/framework/filetools.py
@@ -191,6 +191,15 @@ def test_patch_perl_script_autoflush(self):
         os.remove(fp)
         os.remove("%s.eb.orig" % fp)
 
+    def test_which(self):
+        """Test which function for locating commands."""
+        python = ft.which('python')
+        self.assertTrue(python and os.path.exists(python) and os.path.isabs(python))
+
+        path = ft.which('i_really_do_not_expect_a_command_with_a_name_like_this_to_be_available')
+        self.assertTrue(path is None)
+
+
 def suite():
     """ returns all the testcases in this module """
     return TestLoader().loadTestsFromTestCase(FileToolsTest)
diff --git a/test/framework/modules.py b/test/framework/modules.py
--- a/test/framework/modules.py
+++ b/test/framework/modules.py
@@ -32,6 +32,8 @@
 
 import os
 import re
+import tempfile
+import shutil
 
 import easybuild.tools.options as eboptions
 from easybuild.tools import config
@@ -53,94 +55,136 @@ def setUp(self):
         config.init(eb_go.options, eb_go.get_options_by_section('config'))
 
         self.cwd = os.getcwd()
+        self.orig_modulepaths = os.environ.get('MODULEPATH', '').split(os.pathsep)
+
+        self.testmods = None
+
+        # purge with original $MODULEPATH before running each test
+        # purging fails if module path for one of the loaded modules is no longer in $MODULEPATH
+        modules_tool().purge()
+
+    def init_testmods(self, test_modules_paths=None):
+        """Initialize set of test modules for test."""
+
+        if test_modules_paths is None:
+            test_modules_paths = [os.path.abspath(os.path.join(os.path.dirname(__file__), 'modules'))]
+        self.testmods = modules_tool(test_modules_paths)
+
+    # for Lmod, this test has to run first, to avoid that it fails;
+    # no modules are found if another test ran before it, but using a (very) long module path works fine interactively
+    def test_long_module_path(self):
+        """Test dealing with a (very) long module path."""
+
+        # create a really long modules install path
+        tmpdir = tempfile.mkdtemp()
+        long_mod_path = tmpdir
+        subdir = 'foo'
+        # Lmod v5.1.5 doesn't support module paths longer than 256 characters, so stay just under that magic limit
+        while (len(os.path.abspath(long_mod_path)) + len(subdir)) < 240:
+            long_mod_path = os.path.join(long_mod_path, subdir)
+
+        # copy one of the test modules there
+        gcc_mod_dir = os.path.join(long_mod_path, 'GCC')
+        os.makedirs(gcc_mod_dir)
+        gcc_mod_path = os.path.join(os.path.dirname(__file__), 'modules', 'GCC', '4.6.3')
+        shutil.copy2(gcc_mod_path, gcc_mod_dir)
+
+        # try and use long modules path
+        self.init_testmods(test_modules_paths=[long_mod_path])
+        ms = self.testmods.available()
+
+        self.assertEqual(ms, ['GCC/4.6.3'])
+
+        shutil.rmtree(tmpdir)
 
     def test_avail(self):
         """Test if getting a (restricted) list of available modules works."""
-        testmods = modules_tool([os.path.join(os.path.dirname(__file__), 'modules')])
+        self.init_testmods()
 
         # test modules include 3 GCC modules
-        ms = testmods.available('GCC')
+        ms = self.testmods.available('GCC')
         self.assertEqual(ms, ['GCC/4.6.3', 'GCC/4.6.4', 'GCC/4.7.2'])
 
         # test modules include one GCC/4.6.3 module
-        ms = testmods.available(mod_name='GCC/4.6.3')
+        ms = self.testmods.available(mod_name='GCC/4.6.3')
         self.assertEqual(ms, ['GCC/4.6.3'])
 
         # all test modules are accounted for
-        ms = testmods.available()
+        ms = self.testmods.available()
         self.assertEqual(len(ms), TEST_MODULES_COUNT)
 
     def test_exists(self):
         """Test if testing for module existence works."""
-        testmods = modules_tool([os.path.join(os.path.dirname(__file__), 'modules')])
-        self.assertTrue(testmods.exists('OpenMPI/1.6.4-GCC-4.6.4'))
-        self.assertTrue(not testmods.exists(mod_name='foo/1.2.3'))
+        self.init_testmods()
+        self.assertTrue(self.testmods.exists('OpenMPI/1.6.4-GCC-4.6.4'))
+        self.assertTrue(not self.testmods.exists(mod_name='foo/1.2.3'))
 
     def test_load(self):
         """ test if we load one module it is in the loaded_modules """
-        testmods = modules_tool([os.path.join(os.path.dirname(__file__), 'modules')])
-        ms = testmods.available()
+        self.init_testmods()
+        ms = self.testmods.available()
 
         for m in ms:
-            testmods.load([m])
-            self.assertTrue(m in testmods.loaded_modules())
-            testmods.purge()
+            self.testmods.load([m])
+            self.assertTrue(m in self.testmods.loaded_modules())
+            self.testmods.purge()
 
         # deprecated version
         for m in ms:
-            testmods.add_module([m])
-            testmods.load()
+            self.testmods.add_module([m])
+            self.testmods.load()
 
-            self.assertTrue(m in testmods.loaded_modules())
+            self.assertTrue(m in self.testmods.loaded_modules())
 
             # remove module again and purge to avoid conflicts when loading modules
-            testmods.remove_module([m])
-            testmods.purge()
+            self.testmods.remove_module([m])
+            self.testmods.purge()
 
-    def test_LD_LIBRARY_PATH(self):
+    def test_ld_library_path(self):
         """Make sure LD_LIBRARY_PATH is what it should be when loaded multiple modules."""
+        self.init_testmods()
 
         testpath = '/this/is/just/a/test'
-
         os.environ['LD_LIBRARY_PATH'] = testpath
 
-        testmods = modules_tool([os.path.join(os.path.dirname(__file__), 'modules')])
-
         # load module and check that previous LD_LIBRARY_PATH is still there, at the end
-        testmods.load(['GCC/4.6.3'])
+        self.testmods.load(['GCC/4.6.3'])
         self.assertTrue(re.search("%s$" % testpath, os.environ['LD_LIBRARY_PATH']))
-        testmods.purge()
+        self.testmods.purge()
 
         # deprecated version
-        testmods.add_module([('GCC', '4.6.3')])
-        testmods.load()
+        self.testmods.add_module([('GCC', '4.6.3')])
+        self.testmods.load()
 
         # check that previous LD_LIBRARY_PATH is still there, at the end
         self.assertTrue(re.search("%s$" % testpath, os.environ['LD_LIBRARY_PATH']))
-        testmods.purge()
+        self.testmods.purge()
 
     def test_purge(self):
         """Test if purging of modules works."""
-        m = modules_tool([os.path.join(os.path.dirname(__file__), 'modules')])
-        ms = m.available()
+        self.init_testmods()
+        ms = self.testmods.available()
 
-        m.load([ms[0]])
-        self.assertTrue(len(m.loaded_modules()) > 0)
+        self.testmods.load([ms[0]])
+        self.assertTrue(len(self.testmods.loaded_modules()) > 0)
 
-        m.purge()
-        self.assertTrue(len(m.loaded_modules()) == 0)
+        self.testmods.purge()
+        self.assertTrue(len(self.testmods.loaded_modules()) == 0)
 
         # deprecated version
-        m.add_module([ms[0]])
-        m.load()
-        self.assertTrue(len(m.loaded_modules()) > 0)
+        self.testmods.add_module([ms[0]])
+        self.testmods.load()
+        self.assertTrue(len(self.testmods.loaded_modules()) > 0)
 
-        m.purge()
-        self.assertTrue(len(m.loaded_modules()) == 0)
+        self.testmods.purge()
+        self.assertTrue(len(self.testmods.loaded_modules()) == 0)
 
     def tearDown(self):
         """cleanup"""
         os.chdir(self.cwd)
+        os.environ['MODULEPATH'] = os.pathsep.join(self.orig_modulepaths)
+        # reinitialize a modules tool, to trigger 'module use' on module paths
+        modules_tool()
 
 def suite():
     """ returns all the testcases in this module """
diff --git a/test/framework/options.py b/test/framework/options.py
--- a/test/framework/options.py
+++ b/test/framework/options.py
@@ -294,6 +294,7 @@ def test_skip(self):
             pass
         outtxt = read_file(self.logfile)
 
+        found_msg = "Module toy/1.2.3.4.5.6.7.8.9 found."
         found = re.search(found_msg, outtxt)
         self.assertTrue(not found, "Module found message not there with --skip for non-existing modules: %s" % outtxt)
 
@@ -301,11 +302,15 @@ def test_skip(self):
         not_found = re.search(not_found_msg, outtxt)
         self.assertTrue(not_found, "Module not found message there with --skip for non-existing modules: %s" % outtxt)
 
+        modules_tool().purge()
+
         # restore original MODULEPATH
         if orig_modulepath is not None:
             os.environ['MODULEPATH'] = orig_modulepath
         else:
             os.environ.pop('MODULEPATH')
+        # reinitialize modules tool with original $MODULEPATH, to avoid problems with future tests
+        modules_tool()
 
         # cleanup
         shutil.rmtree(buildpath)
diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py
--- a/test/framework/toy_build.py
+++ b/test/framework/toy_build.py
@@ -101,7 +101,8 @@ def test_toy_build(self):
         outtxt = read_file(self.logfile)
 
         # if the module exists, it should be fine
-        self.assertTrue(os.path.exists(os.path.join(installpath, 'modules', 'all', 'toy', '0.0')))
+        toy_module = os.path.join(installpath, 'modules', 'all', 'toy', '0.0')
+        self.assertTrue(os.path.exists(toy_module), "toy build succeeded: %s" % outtxt)
 
         # check for success
         success = re.compile("COMPLETED: Installation ended successfully")