-
Notifications
You must be signed in to change notification settings - Fork 203
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
Copy build log and artifacts to a permanent location after failures #4601
base: develop
Are you sure you want to change the base?
Changes from all commits
9323ab8
1e866dc
17bb6d2
e572d36
23ae7a8
7ca6553
c9d77ce
34683b1
1c9f555
fe16762
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,6 +106,7 @@ | |
DEFAULT_PATH_SUBDIRS = { | ||
'buildpath': 'build', | ||
'containerpath': 'containers', | ||
'errorlogpath': 'error_log', | ||
'installpath': '', | ||
'packagepath': 'packages', | ||
'repositorypath': 'ebfiles_repo', | ||
|
@@ -478,6 +479,7 @@ class ConfigurationVariables(BaseConfigurationVariables): | |
'buildpath', | ||
'config', | ||
'containerpath', | ||
'errorlogpath', | ||
'installpath', | ||
'installpath_modules', | ||
'installpath_software', | ||
|
@@ -843,6 +845,33 @@ def log_path(ec=None): | |
return log_file_format(return_directory=True, ec=ec, date=date, timestamp=timestamp) | ||
|
||
|
||
def error_log_path(ec=None): | ||
""" | ||
Return the default error log path | ||
|
||
This is a path where file from the build_log_path can be stored permanently | ||
:param ec: dict-like value that provides values for %(name)s and %(version)s template values | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't match what is used. It expects There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used the Should we avoid using template names as default values or explain their use in a better way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see. Isn't trivial to understand for me what is meant by that. I'd change the documentation to
I would not use a default and just fail hard if the keys do not exist because that is what is documented: The function expects a dict with those keys. If you don't provide one it is the fault of the caller. If we do it right in easybuild the keys will always exist, so no need for fallbacks. Or am I missing anything? |
||
""" | ||
error_log_path = ConfigurationVariables()['errorlogpath'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a good idea to have a variable with the same name as a function. Maybe rename the function to |
||
|
||
if ec is None: | ||
ec = {} | ||
|
||
name, version = ec.get('name', '%(name)s'), ec.get('version', '%(version)s') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would you want |
||
date = time.strftime("%Y%m%d") | ||
timestamp = time.strftime("%H%M%S") | ||
|
||
base_path = '/'.join([error_log_path, name + '-' + version, date + '-' + timestamp]) | ||
|
||
path = base_path | ||
inc_no = 1 | ||
while os.path.exists(path): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe rather use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need just the directory name to copy a directory to a new location, not to create a directory. The I will try to decouple the 2 functions and extract a function for creating a file name in a different commit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That "coupling" is intentional: If you don't create the directory you have a race condition for process running in parallel defeating the purpose of this function. Why don't you want that directory created? Can't you just fill it later using the created directory? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You couple through the file system, got it. |
||
path = base_path + '_' + str(inc_no) | ||
inc_no += 1 | ||
|
||
return path | ||
|
||
|
||
def get_build_log_path(): | ||
""" | ||
Return (temporary) directory for build log | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name = 'toy' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What we usually do instead of adding yet another test easyconfig is |
||
version = '0.0' | ||
versionsuffix = '-buggy' | ||
|
||
homepage = 'https://easybuilders.github.io/easybuild' | ||
description = "Toy C program, 100% toy." | ||
|
||
toolchain = SYSTEM | ||
|
||
sources = [SOURCE_TAR_GZ] | ||
checksums = [[ | ||
'be662daa971a640e40be5c804d9d7d10', # default (MD5) | ||
'44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc', # default (SHA256) | ||
('adler32', '0x998410035'), | ||
('crc32', '0x1553842328'), | ||
('md5', 'be662daa971a640e40be5c804d9d7d10'), | ||
('sha1', 'f618096c52244539d0e89867405f573fdb0b55b0'), | ||
('size', 273), | ||
]] | ||
patches = [ | ||
'toy-0.0_add-bug.patch', | ||
('toy-extra.txt', 'toy-0.0'), | ||
] | ||
|
||
sanity_check_paths = { | ||
'files': [('bin/yot', 'bin/toy')], | ||
'dirs': ['bin'], | ||
} | ||
|
||
postinstallcmds = ["echo TOY > %(installdir)s/README"] | ||
|
||
moduleclass = 'tools' | ||
# trailing comment, leave this here, it may trigger bugs with extract_comments() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- a/toy-0.0.orig/toy.source 2014-03-06 18:48:16.000000000 +0100 | ||
+++ b/toy-0.0/toy.source 2020-08-18 12:19:35.000000000 +0200 | ||
@@ -2,6 +2,6 @@ | ||
|
||
int main(int argc, char* argv[]){ | ||
|
||
- printf("I'm a toy, and proud of it.\n"); | ||
+ printf("I'm a toy, and proud of it.\n") | ||
return 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,8 +40,10 @@ | |
import sys | ||
import tempfile | ||
import textwrap | ||
import pathlib | ||
import filecmp | ||
from easybuild.tools import LooseVersion | ||
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered | ||
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered, TempDirectory | ||
from test.framework.package import mock_fpm | ||
from unittest import TextTestRunner | ||
|
||
|
@@ -156,9 +158,9 @@ def check_toy(self, installpath, outtxt, name='toy', version='0.0', versionprefi | |
devel_module_path = os.path.join(software_path, 'easybuild', '%s-%s-easybuild-devel' % (name, full_version)) | ||
self.assertExists(devel_module_path) | ||
|
||
def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True, fails=False, verbose=True, | ||
raise_error=False, test_report=None, name='toy', versionsuffix='', testing=True, | ||
raise_systemexit=False, force=True, test_report_regexs=None, debug=True): | ||
def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, tmp_logdir=None, verify=True, fails=False, | ||
verbose=True, raise_error=False, test_report=None, name='toy', versionsuffix='', testing=True, | ||
raise_systemexit=False, force=True, test_report_regexs=None, debug=True, check_errorlog=None): | ||
"""Perform a toy build.""" | ||
if extra_args is None: | ||
extra_args = [] | ||
|
@@ -172,6 +174,7 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True | |
'--sourcepath=%s' % self.test_sourcepath, | ||
'--buildpath=%s' % self.test_buildpath, | ||
'--installpath=%s' % self.test_installpath, | ||
'--errorlogpath=%s' % self.test_errorlogpath, | ||
'--unittest-file=%s' % self.logfile, | ||
'--robot=%s' % os.pathsep.join([self.test_buildpath, os.path.dirname(__file__)]), | ||
] | ||
|
@@ -183,6 +186,8 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True | |
args.append('--tmpdir=%s' % tmpdir) | ||
if test_report is not None: | ||
args.append('--dump-test-report=%s' % test_report) | ||
if tmp_logdir is not None: | ||
args.append('--tmp-logdir=%s' % tmp_logdir) | ||
args.extend(extra_args) | ||
myerr = None | ||
try: | ||
|
@@ -228,6 +233,9 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True | |
msg = "Pattern %s found in full test report: %s" % (regex.pattern, test_report_txt) | ||
self.assertTrue(regex.search(test_report_txt), msg) | ||
|
||
if check_errorlog is not None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this better done outside? Doesn't need to be in this function, does it? |
||
check_errorlog(outtxt, tmp_logdir, self.test_buildpath, self.test_errorlogpath) | ||
|
||
return outtxt | ||
|
||
def run_test_toy_build_with_output(self, *args, **kwargs): | ||
|
@@ -271,6 +279,71 @@ def test_toy_broken(self): | |
# cleanup | ||
shutil.rmtree(tmpdir) | ||
|
||
def detect_log_file(self, tmp_logpath): | ||
log_files = list(pathlib.Path(tmp_logpath).glob("**/*.log")) | ||
|
||
self.assertTrue(len(log_files) >= 1, 'Log files generated') | ||
log_file = log_files[0] | ||
self.assertTrue(len(log_files) == 1, f"Log file '{log_file}' is unique") | ||
|
||
return log_file | ||
|
||
def assert_build_files_copied(self, buildpath, errorlogpath): | ||
buildir = pathlib.Path(buildpath) | ||
errorlogdir = pathlib.Path(errorlogpath) | ||
iso_date_pattern = r'????????-??????' | ||
for file in buildir.iterdir(): | ||
file_relative_path = file.relative_to(buildir) | ||
file_copies = list(errorlogdir.glob(f"toy-0.0/{iso_date_pattern}/{file_relative_path}")) | ||
self.assertTrue(len(file_copies) == 1, f"Unique copy of toy build file '{file}' made") | ||
file_copy = file_copies[0] | ||
|
||
if file_copy.is_file(): | ||
msg = f"File '{file}' copied succesfully" | ||
self.assertTrue(filecmp.cmp(str(file), str(file_copy), shallow=False), msg) | ||
|
||
def assert_log_files_copied(self, log_file, errorlogpath): | ||
file_name = log_file.name | ||
saved_log_files = list(pathlib.Path(errorlogpath).glob(f"**/{file_name}")) | ||
self.assertTrue(len(saved_log_files) == 1, f"Unique copy of log file '{log_file}' made") | ||
for saved_log_file in saved_log_files: | ||
msg = f"Log file '{log_file}' copied succesfully" | ||
self.assertTrue(filecmp.cmp(str(log_file), str(saved_log_file), shallow=False), msg) | ||
|
||
def assert_error_reported(self, outtxt, output_regexs): | ||
for regex_pattern in output_regexs: | ||
regex = re.compile(regex_pattern, re.M) | ||
msg_stdout = "Pattern %s found in full test report: %s" % (regex.pattern, outtxt) | ||
self.assertTrue(regex.search(outtxt), msg_stdout) | ||
|
||
def check_errorlog(self, output_regexs, outtxt, tmp_logpath, buildpath, errorlogpath): | ||
log_file = self.detect_log_file(tmp_logpath) | ||
|
||
self.assert_build_files_copied(buildpath, errorlogpath) | ||
self.assert_log_files_copied(log_file, errorlogpath) | ||
self.assert_error_reported(outtxt, output_regexs) | ||
|
||
with open(f"{log_file}", 'r') as p_log_file: | ||
self.assert_error_reported(p_log_file.read(), output_regexs) | ||
|
||
def test_toy_broken_compilation(self): | ||
"""Test whether log files and the build directory are copied to a permanent location after a failed | ||
compilation.""" | ||
tmpdir = TempDirectory() | ||
tmp_logdir = TempDirectory() | ||
broken_compilation_ec = os.path.join(os.path.dirname(__file__), | ||
'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0-buggy.eb') | ||
|
||
output_regexs = [r"^\s+toy\.c:5:44: error: expected (;|';') before"] | ||
|
||
def check_errorlog(outtxt, tmp_logpath, buildpath, errorlogpath): | ||
self.check_errorlog(output_regexs, outtxt, tmp_logpath, buildpath, errorlogpath) | ||
|
||
self.run_test_toy_build_with_output( | ||
ec_file=broken_compilation_ec, tmpdir=tmpdir.get_path(), tmp_logdir=tmp_logdir.get_path(), | ||
verify=False, fails=True, verbose=False, raise_error=False, | ||
name='toy', versionsuffix='-buggy', check_errorlog=check_errorlog) | ||
|
||
def test_toy_tweaked(self): | ||
"""Test toy build with tweaked easyconfig, for testing extra easyconfig parameters.""" | ||
test_ecs_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'easyconfigs') | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.