Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for marking installed module file as new default version #2110

Merged
merged 18 commits into from
Mar 24, 2017

Conversation

victorusu
Copy link
Contributor

@victorusu victorusu commented Feb 9, 2017

This commit adds support to define a default module version on command
line.

The command line is:
--set-default-module

And both Tcl and Lua modules are supported.

boegel and others added 2 commits February 3, 2017 13:03
This commit adds support to define a default module version on command
line.

The command line is:
--set-as-default

And both Tcl and Lua modules are supported.
@pforai
Copy link
Contributor

pforai commented Feb 9, 2017

LGTM

Adding unittest to
* symlink and readlink (test.framework.filetools)
* set_default (test.framework.module_generator)

Currently the unittests pass, except for test.framework.module_generator
test_set_default because the environment is cleaned up in the test and LMod
complains about the cache.

The results for the unittests are:
python -m test.framework.filetools test_symlink_readlink_file
INFO: This is (based on) vsc.install.shared_setup 0.10.22
Filtered FileToolsTest tests using 'test_symlink_readlink_file',
retained 1/33 tests: test_symlink_readlink_file
.
----------------------------------------------------------------------
Ran 1 test in 0.740s

OK

python -m test.framework.module_generator test_set_default
INFO: This is (based on) vsc.install.shared_setup 0.10.22
Filtered TclModuleGeneratorTest tests using 'test_set_default', retained
1/16 tests: test_set_default
Filtered LuaModuleGeneratorTest tests using 'test_set_default', retained
1/16 tests: test_set_default
.E
======================================================================
ERROR: test_set_default (__main__.LuaModuleGeneratorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File
"/Users/hvictor/Programs/easybuild/easybuild-framework/test/framework/module_generator.py",
line 186, in test_set_default
    self.modtool.load([module_name])
  File "easybuild/tools/modules.py", line 509, in load
    self.run_module('load', mod)
  File "easybuild/tools/modules.py", line 674, in run_module
    raise EasyBuildError("Changing environment as dictated by module
failed: %s (%s)", err, out)
EasyBuildError: 'Changing environment as dictated by module failed: name
\'false\' is not defined (stdout: \nfalse\n, stderr: Lmod has detected
the following error: The following module(s) are unknown:
"dummy"\n\nPlease check the spelling or version number. Also try "module
spider ..."\nIt is also possible your cache file is out-of-date try:\n
$ module --ignore-cache load "dummy"\n\n\n\n)'

----------------------------------------------------------------------
Ran 2 tests in 2.653s

FAILED (errors=1)
@@ -383,6 +383,9 @@ def override_options(self):
None, 'store_true', False),
'zip-logs': ("Zip logs that are copied to install directory, using specified command",
None, 'store_or_None', 'gzip'),

'set-as-default': ("Set the generated module as default", None, 'store_true', False),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indent is off here

mod_filepath = os.path.join(module_folder_path, module_file)

default_filepath = os.path.join(module_folder_path, 'default')
os.symlink(mod_filepath, default_filepath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if there's already a symlink?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, use symlink from filetools

@@ -692,6 +704,14 @@ def getenv_cmd(self, envvar):
"""
return 'os.getenv("%s")' % envvar

def set_as_default(self, module_folder_path, module_version):

module_file = module_version + '.lua'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use self.MODULE_FILE_EXTENSION rather than hardcoding .lua

@@ -2132,6 +2172,8 @@ def make_module_step(self, fake=False):
txt += self.make_module_extra()
txt += self.make_module_footer()



Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moar whitespace!

@@ -2163,6 +2205,8 @@ def make_module_step(self, fake=False):
if not fake:
self.make_devel_module()

self._set_module_as_default(fake=fake)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd include the if build_option(...) here instead?

self.dry_run_msg("Generating default module file %s\n", mod_filepath)
else:
self.module_generator.set_as_default(mod_folderpath, self.version)
self.log.info("Module default version file %s written", mod_filepath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be here, should log in modulegenerator instead

self.module_generator.set_as_default(mod_folderpath, self.version)
self.log.info("Module default version file %s written", mod_filepath)

# invalidate relevant 'module avail'/'module show' cache entries
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?! comment does not make any sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of this can be removed totally

@@ -2075,6 +2075,46 @@ def _sanity_check_step(self, custom_paths=None, custom_commands=None, extension=
else:
self.log.debug("Sanity check passed!")

def _set_module_as_default(self, fake=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring?

sets the default module version except if we are in dry run.
"""
mod_filepath = self.module_generator.get_module_filepath()
mod_folderpath = os.path.dirname(mod_filepath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not combine these lines together, mod_filepath is only used in one place?

mod_folderpath = os.path.dirname(mod_filepath)

if not self.dry_run:
self.module_generator.set_as_default(mod_folderpath, self.version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe emit a meaningful message in the case of dry run?

if self.dry_run:
    dry_run_msg("Marked %s v%s as default version" % (self.name, self.version))
else:
    self.module_generator.set_as_default(mod_folderpath, self.version)

@@ -2139,6 +2151,7 @@ def make_module_step(self, fake=False):
for line in txt.split('\n'):
self.dry_run_msg(' ' * 4 + line)

self.dry_run_msg("Generating file to set default module version to %s\n", self.version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, drop this, see dry run msg in _set_module_as_default?

:param symlink_path: symlink file path
"""

# note: we can't use try-except-finally, because Python 2.4 doesn't support it as a single block
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal required version is Python 2.6 now, so you can use try/except/finally

but, not relevant here, so please just drop this comment

# note: we can't use try-except-finally, because Python 2.4 doesn't support it as a single block
try:
target_symlink_path = os.readlink(symlink_path)
except IOError, err:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be OSError rather than IOError?

also, use

except OSError as err:

@@ -40,13 +40,16 @@
from easybuild.tools import config
from easybuild.tools.module_generator import ModuleGeneratorLua, ModuleGeneratorTcl
from easybuild.tools.module_naming_scheme.utilities import is_valid_module_name
from easybuild.tools.filetools import mkdir, write_file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort line alphabetically please?

from easybuild.framework.easyblock import EasyBlock
from easybuild.framework.easyconfig.easyconfig import EasyConfig, ActiveMNS
from easybuild.tools.build_log import EasyBuildError
from easybuild.tools.modules import Lmod
from easybuild.tools.utilities import quote_str
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, find_full_path, init_config

from vsc.utils.missing import nub
from easybuild.tools.modules import curr_module_paths, reset_module_caches
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be moved up...

Test load part in generated module file.
"""
# if not self.MODULE_GENERATOR_CLASS == ModuleGeneratorTcl:
# return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove?

mkdir(base_path)

# creating package module
module_name = 'dummy'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dummy has a particular meaning, use just a random module name instead like foobar or whatever

if txt:
txt += '\n'

foo_version = 'foo_version'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not make this 'proper' versions like 1.0 and 2.0?

Fixed MR and all unittests pass now

python -m test.framework.filetools test_symlink_readlink_file
INFO: This is (based on) vsc.install.shared_setup 0.10.22
Filtered FileToolsTest tests using 'test_symlink_readlink_file',
retained 1/33 tests: test_symlink_readlink_file
.
----------------------------------------------------------------------
Ran 1 test in 0.763s

OK

python -m test.framework.module_generator test_set_default
INFO: This is (based on) vsc.install.shared_setup 0.10.22
Filtered TclModuleGeneratorTest tests using 'test_set_default', retained
1/16 tests: test_set_default
Filtered LuaModuleGeneratorTest tests using 'test_set_default', retained
1/16 tests: test_set_default
..
----------------------------------------------------------------------
Ran 2 tests in 2.831s

OK
@victorusu victorusu changed the title Default module version WIP: Default module version Feb 10, 2017
* Removed bug on remove_file - that did not remove invalid links
* Removed own introduced bug on symlink that called build_options too
* early
* Removed own introduced bug that set full path links to default LMod
* modules
This commit is comprised of:
* tests remove_file for valid and invalid symlinks
* improved error message for symlink errors
@@ -180,7 +204,8 @@ def remove_file(path):
return

try:
if os.path.exists(path):
# note: file may also be a broken symlink...
if os.path.exists(path) or os.path.islink(path):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@victorusu please add a dedicated test case for this fixed bug?

Copy link
Contributor Author

@victorusu victorusu Feb 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added as test_remove_symlinks

@@ -452,6 +456,20 @@ def getenv_cmd(self, envvar):
"""
return '$env(%s)' % envvar

# noinspection PyPackageRequirements,PyPackageRequirements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@victorusu what's this about (and why twice the same thing)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed. It was a mistake...

@@ -693,6 +711,29 @@ def getenv_cmd(self, envvar):
return 'os.getenv("%s")' % envvar


def set_as_default(self, module_folder_path, module_version):
"""
Create file called "default" inside the package's module folder in order
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not a file, but a symlink?

default_filepath = os.path.join(module_folder_path, 'default')

if os.path.islink(default_filepath):
points_to = readlink(default_filepath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to link_target?

cwd = os.getcwd()
#os.chdir(module_folder_path)
symlink(module_version + self.MODULE_FILE_EXTENSION, default_filepath)
#os.chdir(cwd)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please clean up the uncommented code, and you can drop the cwd = line too (not used anymore)

# checking if file is symlink
self.assertTrue(os.path.islink(link))
# reading link target and comparing to file name
self.assertEqual(fp, ft.readlink(link))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, be careful here, what if fp is a file in a symlinked location?

maybe use os.path.realpath or ft.readlink when defining fp?

@@ -46,7 +48,7 @@
from easybuild.tools.modules import Lmod
from easybuild.tools.utilities import quote_str
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, find_full_path, init_config

from vsc.utils.missing import nub
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this up, next to the other imports from vsc.utils?

# removing all available modules from the environment
mod_paths = [p for p in nub(curr_module_paths()) if os.path.exists(p)]
for mod_path in nub(mod_paths):
self.modtool.unuse(mod_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you really need to do this? if not, then don't?

@boegel boegel added this to the 3.2.0 milestone Feb 14, 2017
This is an attempt to remove the issues related to LMod 5.8
@boegel
Copy link
Member

boegel commented Feb 16, 2017

@victorusu if you consider this fit for rereview, please update PR title and ping me in a comment

@victorusu
Copy link
Contributor Author

@boegel, I changed the symlink function in order to allow non-full absolute source paths. Which is a required for older versions of LMod (< 7.1 at least). So now, all the LMod tests are passing.
I am still experiencing a failure due to TMod. The test test_set_default is creating .lua module files, even though the env module system is TMod. So I wonder if this is the desired behaviour and I should add an if statement like the following in order to skip this case?

if self.MODULE_GENERATOR_CLASS == ModuleGeneratorLua and os.getenv("EASYBUILD_MODULE_SYNTAX") == "Tcl":
pass

Thanks!

@boegel
Copy link
Member

boegel commented Feb 20, 2017

@victorusu How can the test still be creating Lua module file, we're using self.modgen.MODULE_FILE_EXTENSION to make sure we create the right type of module files?

Unless I'm missing something, the test should work unconditionally regardless of which modules tool & syntax is being used?

@boegel
Copy link
Member

boegel commented Feb 20, 2017

OK, I think I see the problem now...

The tests in test/framework/module_generator.py are being run twice automagically, once for Tcl syntax, and once for Lua syntax.

However, running them with Lua syntax only works when Lmod is being used as a modules tool, of course.

So, yes @victorusu, you'll need to add a check like:

if self.MODULE_GENERATOR_CLASS == ModuleGeneratorLua and not isinstance(self.modtool, Lmod):
    pass

@victorusu
Copy link
Contributor Author

@boegel, If think the issue is related to the fact that the test.suite calls

suite.addTests(TestLoaderFiltered().loadTestsFromTestCase(TclModuleGeneratorTest, sys.argv[1:]))
suite.addTests(TestLoaderFiltered().loadTestsFromTestCase(LuaModuleGeneratorTest, sys.argv[1:]))

So, the generator is Lua but the environment is Tcl. This issue was not observed before because no, previous, test generated any module. The current tests only load and unload modules.

Isn't it?

This commit skips the creation of luamodules for the Tcl Environment
module system
Test load part in generated module file.
"""
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorLua and not isinstance(self.modtool, Lmod):
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@victorusu: you'll need to make this a return rather than a pass to actually skip the test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks!

@victorusu victorusu changed the title WIP: Default module version Default module version Feb 22, 2017
@victorusu
Copy link
Contributor Author

@boegel, The unittests are finally passing! 👍
I removed the WIP from the title. I am waiting for feedback. :-)
Thanks!

@boegel boegel changed the title Default module version add support for marking installed module file as new default version Feb 23, 2017
Copy link
Member

@boegel boegel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final review, a couple more minor things

largest remark is about making the option name a bit clearer

Create a symlink at the specified path to the given path.

:param source_path: source file path
:param symlink_path: symlink file path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use_abspath_source is not documented in docstring, please fix

_log.info("Symlinked %s to %s", os.path.abspath(source_path), symlink_path)
else:
os.symlink(source_path, symlink_path)
_log.info("Symlinked %s to %s", source_path, symlink_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can shorten this a bit:

if use_abspath_source:
    source_path = os.path.abspath(source_path)

 os.symlink(source_path, symlink_path)
_log.info("Symlinked %s to %s", source_path, symlink_path)


# noinspection PyPackageRequirements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove?

remove_file(default_filepath)
self.log.info("Removed default version marking from %s.", link_target)
elif os.path.exists(default_filepath):
raise EasyBuildError('Found an unexpected file called default in dir %s' % module_folder_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/called default/named 'default'/g

@@ -383,6 +383,8 @@ def override_options(self):
None, 'store_true', False),
'zip-logs': ("Zip logs that are copied to install directory, using specified command",
None, 'store_or_None', 'gzip'),
'set-as-default': ("Set the generated module as default", None, 'store_true', False),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename to --set-default-module? leaves less room for misinterpretation...

# checking if file is symlink
self.assertTrue(os.path.islink(link))
# reading link target and comparing to file name
self.assertEqual(os.path.realpath(fp), ft.readlink(link))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I think this may fail, e.g. if self.test_prefix is a symlink...

os.path.realpath will give you the fully resolved location, while ft.readlink will only resolve one symlink 'level', I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right! Setting

self.assertEqual(os.path.realpath(fp), os.path.realpath(ft.readlink(link)))

worked for me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ok, but then the test is worthless, no?

I'm starting to think we should change ft.readlink to ft.resolve_symlink, and basically make it call os.path.realpath in a try-except?

Test load part in generated module file.
"""
if self.MODULE_GENERATOR_CLASS == ModuleGeneratorLua and not isinstance(self.modtool, Lmod):
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include a short comment to explain why you're bailing out here, this looks suspicious, but we're not cheating here :)

@boegel
Copy link
Member

boegel commented Mar 23, 2017

@victorusu victorusu#1 should make this good to go

rename filetools.readlink to filetools.resolve_path, use os.path.realpath, enhance test for symlink & resolve_path
@boegel
Copy link
Member

boegel commented Mar 24, 2017

Going in, thanks @victorusu!

@boegel boegel merged commit 297a07b into easybuilders:develop Mar 24, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants