Repository: easybuilders/easybuild-framework

Issue:
Failed to parse configuration options: --module-syntax
Parameter `--module-syntax` seems to be ignored.

Log output: https://gist.github.com/omula/62ce29ea4943e1303f1f75fc7f160ebb

OS: Centos 6.4
```
$ python --version
Python 2.6.6
```


Discussion:
The error:

```
ERROR: Failed to parse configuration options: 'Generating Lua module files requires Lmod as modules tool; use --module-syntax to specify a different module syntax to use (Lua, Tcl)'
```

Relevant info from debug log:

```
== 2018-09-05 18:55:01,455 generaloption.py:1263 DEBUG parseoptions: options from environment ['--installpath=/home/software/RHEL/6/Nehalem', '--modules-tool=EnvironmentModulesC', '--prefix=/home/software/RHEL/6/Nehalem', '--sourcepath=/home/software/sources', '--tmp-logdir=/home/software/tmp']
== 2018-09-05 18:55:01,455 generaloption.py:1264 DEBUG parseoptions: options from commandline ['--module-syntax=Tcl', '--show-config']
== 2018-09-05 18:55:01,455 generaloption.py:1272 DEBUG Found options {'module_syntax': 'Tcl',  'modules_tool': 'EnvironmentModulesC', ...} args []
```

Relevant code from `easybuild/tools/options.py`:

```python
if self.options.module_syntax == ModuleGeneratorLua.SYNTAX and self.options.modules_tool != Lmod.__name__:
```

(see https://github.com/easybuilders/easybuild-framework/blob/master/easybuild/tools/options.py#L777)

The log clearly shows that the error doesn't make any sense...

`ModuleGeneratorLua.SYNTAX` is equivalent to `'Lua'`, which is not `'Tcl'`, so the error can never be triggered...

Is this a vanilla EasyBuild framework installation @omula?!
@omula With EasyBuild 3.6.2, you should have these MD5 checksums:

```
$ md5 easybuild/tools/options.py easybuild/tools/module_generator.py
MD5 (easybuild/tools/options.py) = 7b74862fc3269ef84d2de600212a1988
MD5 (easybuild/tools/module_generator.py) = 993838f773940d6535781e4ab6a89358
```
After a bit of back & forth with @omula on Slack, it turns out this is a bug in `--show-config`, or at least in the way in which the checks that are done in `postprocess` in `options.py` interact with `--show-config`...

The problem is that when `--show-config` is used, we reprocess the configuration on each of the different configuration levels separately (so we can tell what is configured on which level).
Therefore, if `--module-syntax Tcl` is used in combination with `export EASYBUILD_MODULES_TOOL=EnvironmentModulesC` (or with `modules_tool` set in a configuration file), it triggers and error when the configuration specified in the environment is parsed because `Lua` is the default for `--module-syntax`...

I'll look into a fix for this. Until that's in place, a workaround is to configure the modules tool & syntax on the same configuration level (e.g. both in the environment).

Resolving patch:
diff --git a/easybuild/tools/options.py b/easybuild/tools/options.py
--- a/easybuild/tools/options.py
+++ b/easybuild/tools/options.py
@@ -218,6 +218,7 @@ def __init__(self, *args, **kwargs):
         """Constructor."""
 
         self.with_include = kwargs.pop('with_include', True)
+        self.single_cfg_level = kwargs.pop('single_cfg_level', False)
 
         self.default_repositorypath = [mk_full_default_path('repositorypath')]
         self.default_robot_paths = get_paths_for(subdir=EASYCONFIGS_PKG_SUBDIR, robot_path=None) or []
@@ -771,36 +772,10 @@ def postprocess(self):
             build_easyconfig_constants_dict()  # runs the easyconfig constants sanity check
             self._postprocess_list_avail()
 
-        # fail early if required dependencies for functionality requiring using GitHub API are not available:
-        if self.options.from_pr or self.options.upload_test_report:
-            if not HAVE_GITHUB_API:
-                raise EasyBuildError("Required support for using GitHub API is not available (see warnings).")
-
-        if self.options.module_syntax == ModuleGeneratorLua.SYNTAX and self.options.modules_tool != Lmod.__name__:
-            error_msg = "Generating Lua module files requires Lmod as modules tool; "
-            mod_syntaxes = ', '.join(sorted(avail_module_generators().keys()))
-            error_msg += "use --module-syntax to specify a different module syntax to use (%s)" % mod_syntaxes
-            raise EasyBuildError(error_msg)
-
-        # check whether specified action --detect-loaded-modules is valid
-        if self.options.detect_loaded_modules not in LOADED_MODULES_ACTIONS:
-            raise EasyBuildError("Unknown action specified to --detect-loaded-modules: %s (known values: %s)",
-                                 self.options.detect_loaded_modules, ', '.join(LOADED_MODULES_ACTIONS))
-
-        # make sure a GitHub token is available when it's required
-        if self.options.upload_test_report:
-            if not HAVE_KEYRING:
-                raise EasyBuildError("Python 'keyring' module required for obtaining GitHub token is not available.")
-            if self.options.github_user is None:
-                raise EasyBuildError("No GitHub user name provided, required for fetching GitHub token.")
-            token = fetch_github_token(self.options.github_user)
-            if token is None:
-                raise EasyBuildError("Failed to obtain required GitHub token for user '%s'", self.options.github_user)
-
-        # make sure autopep8 is available when it needs to be
-        if self.options.dump_autopep8:
-            if not HAVE_AUTOPEP8:
-                raise EasyBuildError("Python 'autopep8' module required to reformat dumped easyconfigs as requested")
+        # run configuration checks, unless only a single configuration level is being processed
+        # (this should only happen during --show-config)
+        if not self.single_cfg_level:
+            self._postprocess_checks()
 
         # imply --terse for --last-log to avoid extra output that gets in the way
         if self.options.last_log:
@@ -861,6 +836,43 @@ def _postprocess_include(self):
         if self.options.include_toolchains:
             include_toolchains(self.tmpdir, self.options.include_toolchains)
 
+    def _postprocess_checks(self):
+        """Check whether (combination of) configuration options make sense."""
+
+        # fail early if required dependencies for functionality requiring using GitHub API are not available:
+        if self.options.from_pr or self.options.upload_test_report:
+            if not HAVE_GITHUB_API:
+                raise EasyBuildError("Required support for using GitHub API is not available (see warnings)")
+
+        # using Lua module syntax only makes sense when modules tool being used is Lmod
+        if self.options.module_syntax == ModuleGeneratorLua.SYNTAX and self.options.modules_tool != Lmod.__name__:
+            error_msg = "Generating Lua module files requires Lmod as modules tool; "
+            mod_syntaxes = ', '.join(sorted(avail_module_generators().keys()))
+            error_msg += "use --module-syntax to specify a different module syntax to use (%s)" % mod_syntaxes
+            raise EasyBuildError(error_msg)
+
+        # check whether specified action --detect-loaded-modules is valid
+        if self.options.detect_loaded_modules not in LOADED_MODULES_ACTIONS:
+            error_msg = "Unknown action specified to --detect-loaded-modules: %s (known values: %s)"
+            raise EasyBuildError(error_msg % (self.options.detect_loaded_modules, ', '.join(LOADED_MODULES_ACTIONS)))
+
+        # make sure a GitHub token is available when it's required
+        if self.options.upload_test_report:
+            if not HAVE_KEYRING:
+                raise EasyBuildError("Python 'keyring' module required for obtaining GitHub token is not available")
+            if self.options.github_user is None:
+                raise EasyBuildError("No GitHub user name provided, required for fetching GitHub token")
+            token = fetch_github_token(self.options.github_user)
+            if token is None:
+                raise EasyBuildError("Failed to obtain required GitHub token for user '%s'" % self.options.github_user)
+
+        # make sure autopep8 is available when it needs to be
+        if self.options.dump_autopep8:
+            if not HAVE_AUTOPEP8:
+                raise EasyBuildError("Python 'autopep8' module required to reformat dumped easyconfigs as requested")
+
+        self.log.info("Checks on configuration options passed")
+
     def _postprocess_config(self):
         """Postprocessing of configuration options"""
         if self.options.prefix is not None:
@@ -1044,7 +1056,7 @@ def reparse_cfg(args=None, withcfg=True):
             if args is None:
                 args = []
             cfg = EasyBuildOptions(go_args=args, go_useconfigfiles=withcfg, envvar_prefix=CONFIG_ENV_VAR_PREFIX,
-                                   with_include=False)
+                                   with_include=False, single_cfg_level=True)
 
             return cfg.dict_by_prefix()
 


Test patch:
diff --git a/test/framework/options.py b/test/framework/options.py
--- a/test/framework/options.py
+++ b/test/framework/options.py
@@ -2464,11 +2464,11 @@ def _assert_regexs(self, regexs, txt, assert_true=True):
             else:
                 self.assertFalse(regex.search(txt), "Pattern '%s' NOT found in: %s" % (regex.pattern, txt))
 
-    def _run_mock_eb(self, args, do_build=False, raise_error=False, verbose=False, testing=True, strip=False):
+    def _run_mock_eb(self, args, strip=False, **kwargs):
         """Helper function to mock easybuild runs"""
         self.mock_stdout(True)
         self.mock_stderr(True)
-        self.eb_main(args, do_build=do_build, raise_error=raise_error, verbose=verbose, testing=testing)
+        self.eb_main(args, **kwargs)
         stdout_txt = self.get_stdout()
         stderr_txt = self.get_stderr()
         self.mock_stdout(False)
@@ -2933,6 +2933,67 @@ def test_show_config(self):
         regex = re.compile(r'^include-easyblocks \(E\) = .*/testeasyblocktoinclude.py$', re.M)
         self.assertTrue(regex.search(txt), "Pattern '%s' found in: %s" % (regex.pattern, txt))
 
+    def test_show_config_cfg_levels(self):
+        """Test --show-config in relation to how configuring across multiple configuration levels interacts with it."""
+
+        # make sure default module syntax is used
+        if 'EASYBUILD_MODULE_SYNTAX' in os.environ:
+            del os.environ['EASYBUILD_MODULE_SYNTAX']
+
+        # configuring --modules-tool and --module-syntax on different levels should NOT cause problems
+        # cfr. bug report https://github.com/easybuilders/easybuild-framework/issues/2564
+        os.environ['EASYBUILD_MODULES_TOOL'] = 'EnvironmentModulesC'
+        args = [
+            '--module-syntax=Tcl',
+            '--show-config',
+        ]
+        # set init_config to False to avoid that eb_main (called by _run_mock_eb) re-initialises configuration
+        # this fails because $EASYBUILD_MODULES_TOOL=EnvironmentModulesC conflicts with default module syntax (Lua)
+        stdout, _ = self._run_mock_eb(args, raise_error=True, redo_init_config=False)
+
+        patterns = [
+            "^# Current EasyBuild configuration",
+            "^module-syntax\s*\(C\) = Tcl",
+            "^modules-tool\s*\(E\) = EnvironmentModulesC",
+        ]
+        for pattern in patterns:
+            regex = re.compile(pattern, re.M)
+            self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
+    def test_modules_tool_vs_syntax_check(self):
+        """Verify that check for modules tool vs syntax works."""
+
+        # make sure default module syntax is used
+        if 'EASYBUILD_MODULE_SYNTAX' in os.environ:
+            del os.environ['EASYBUILD_MODULE_SYNTAX']
+
+        # using EnvironmentModulesC modules tool with default module syntax (Lua) is a problem
+        os.environ['EASYBUILD_MODULES_TOOL'] = 'EnvironmentModulesC'
+        args = ['--show-full-config']
+        error_pattern = "Generating Lua module files requires Lmod as modules tool"
+        self.assertErrorRegex(EasyBuildError, error_pattern, self._run_mock_eb, args, raise_error=True)
+
+        patterns = [
+            "^# Current EasyBuild configuration",
+            "^module-syntax\s*\(C\) = Tcl",
+            "^modules-tool\s*\(E\) = EnvironmentModulesC",
+        ]
+
+        # EnvironmentModulesC modules tool + Tcl module syntax is fine
+        args.append('--module-syntax=Tcl')
+        stdout, _ = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False, redo_init_config=False)
+        for pattern in patterns:
+            regex = re.compile(pattern, re.M)
+            self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
+        # default modules tool (Lmod) with Tcl module syntax is also fine
+        del os.environ['EASYBUILD_MODULES_TOOL']
+        patterns[-1] = "^modules-tool\s*\(D\) = Lmod"
+        stdout, _ = self._run_mock_eb(args, do_build=True, raise_error=True, testing=False, redo_init_config=False)
+        for pattern in patterns:
+            regex = re.compile(pattern, re.M)
+            self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout))
+
     def test_prefix(self):
         """Test which configuration settings are affected by --prefix."""
         txt, _ = self._run_mock_eb(['--show-full-config', '--prefix=%s' % self.test_prefix], raise_error=True)
diff --git a/test/framework/utilities.py b/test/framework/utilities.py
--- a/test/framework/utilities.py
+++ b/test/framework/utilities.py
@@ -250,7 +250,7 @@ def reset_modulepath(self, modpaths):
         self.modtool.set_mod_paths()
 
     def eb_main(self, args, do_build=False, return_error=False, logfile=None, verbose=False, raise_error=False,
-                reset_env=True, raise_systemexit=False, testing=True):
+                reset_env=True, raise_systemexit=False, testing=True, redo_init_config=True):
         """Helper method to call EasyBuild main function."""
         cleanup()
 
@@ -282,8 +282,9 @@ def eb_main(self, args, do_build=False, return_error=False, logfile=None, verbos
 
         os.chdir(self.cwd)
 
-        # make sure config is reinitialized
-        init_config(with_include=False)
+        if redo_init_config:
+            # make sure config is reinitialized
+            init_config(with_include=False)
 
         # restore environment to what it was before running main,
         # changes may have been made by eb_main (e.g. $TMPDIR & co)