Skip to content

Commit 5a0cf13

Browse files
authored
Merge pull request #3118 from Flamefire/build_errors
Add check_log_for_errors to detect and handle multiple errors
2 parents e7a37c5 + 0859617 commit 5a0cf13

File tree

2 files changed

+126
-2
lines changed

2 files changed

+126
-2
lines changed

easybuild/tools/run.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ def parse_cmd_output(cmd, stdouterr, ec, simple, log_all, log_ok, regexp):
539539
if use_regexp or regexp:
540540
res = parse_log_for_error(stdouterr, regexp, msg="Command used: %s" % cmd)
541541
if len(res) > 0:
542-
message = "Found %s errors in command output (output: %s)" % (len(res), ", ".join([r[0] for r in res]))
542+
message = "Found %s errors in command output (output: %s)" % (len(res), "\n\t".join([r[0] for r in res]))
543543
if use_regexp:
544544
raise EasyBuildError(message)
545545
else:
@@ -589,3 +589,70 @@ def parse_log_for_error(txt, regExp=None, stdout=True, msg=None):
589589
(regExp, '\n'.join([x[0] for x in res])))
590590

591591
return res
592+
593+
594+
def extract_errors_from_log(log_txt, reg_exps):
595+
"""
596+
Check provided string (command output) for messages matching specified regular expressions,
597+
and return 2-tuple with list of warnings and errors.
598+
:param log_txt: String containing the log, will be split into individual lines
599+
:param reg_exps: List of: regular expressions (as strings) to error on,
600+
or tuple of regular expression and action (any of [IGNORE, WARN, ERROR])
601+
:return (warnings, errors) as lists of lines containing a match
602+
"""
603+
actions = (IGNORE, WARN, ERROR)
604+
605+
# promote single string value to list, since code below expects a list
606+
if isinstance(reg_exps, string_type):
607+
reg_exps = [reg_exps]
608+
609+
re_tuples = []
610+
for cur in reg_exps:
611+
try:
612+
if isinstance(cur, str):
613+
# use ERROR as default action if only regexp pattern is specified
614+
reg_exp, action = cur, ERROR
615+
elif isinstance(cur, tuple) and len(cur) == 2:
616+
reg_exp, action = cur
617+
else:
618+
raise TypeError("Incorrect type of value, expected string or 2-tuple")
619+
620+
if not isinstance(reg_exp, str):
621+
raise TypeError("Regular expressions must be passed as string, got %s" % type(reg_exp))
622+
if action not in actions:
623+
raise TypeError("action must be one of %s, got %s" % (actions, action))
624+
625+
re_tuples.append((re.compile(reg_exp), action))
626+
except Exception as err:
627+
raise EasyBuildError("Invalid input: No regexp or tuple of regexp and action '%s': %s", str(cur), err)
628+
629+
warnings = []
630+
errors = []
631+
for line in log_txt.split('\n'):
632+
for reg_exp, action in re_tuples:
633+
if reg_exp.search(line):
634+
if action == ERROR:
635+
errors.append(line)
636+
elif action == WARN:
637+
warnings.append(line)
638+
break
639+
return warnings, errors
640+
641+
642+
def check_log_for_errors(log_txt, reg_exps):
643+
"""
644+
Check log_txt for messages matching regExps in order and do appropriate action
645+
:param log_txt: String containing the log, will be split into individual lines
646+
:param reg_exps: List of: regular expressions (as strings) to error on,
647+
or tuple of regular expression and action (any of [IGNORE, WARN, ERROR])
648+
"""
649+
global errors_found_in_log
650+
warnings, errors = extract_errors_from_log(log_txt, reg_exps)
651+
652+
errors_found_in_log += len(warnings) + len(errors)
653+
if warnings:
654+
_log.warning("Found %s potential error(s) in command output (output: %s)",
655+
len(warnings), "\n\t".join(warnings))
656+
if errors:
657+
raise EasyBuildError("Found %s error(s) in command output (output: %s)",
658+
len(errors), "\n\t".join(errors))

test/framework/run.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@
4646
import easybuild.tools.utilities
4747
from easybuild.tools.build_log import EasyBuildError, init_logging, stop_logging
4848
from easybuild.tools.filetools import adjust_permissions, read_file, write_file
49-
from easybuild.tools.run import get_output_from_process, run_cmd, run_cmd_qa, parse_log_for_error
49+
from easybuild.tools.run import (
50+
check_log_for_errors,
51+
get_output_from_process,
52+
run_cmd,
53+
run_cmd_qa,
54+
parse_log_for_error,
55+
)
56+
from easybuild.tools.config import ERROR, IGNORE, WARN
5057

5158

5259
class RunTest(EnhancedTestCase):
@@ -520,6 +527,56 @@ def test_run_cmd_stream(self):
520527
])
521528
self.assertEqual(stdout, expected)
522529

530+
def test_check_log_for_errors(self):
531+
fd, logfile = tempfile.mkstemp(suffix='.log', prefix='eb-test-')
532+
os.close(fd)
533+
534+
self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [42])
535+
self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [(42, IGNORE)])
536+
self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [("42", "invalid-mode")])
537+
self.assertErrorRegex(EasyBuildError, "Invalid input:", check_log_for_errors, "", [("42", IGNORE, "")])
538+
539+
input_text = "\n".join([
540+
"OK",
541+
"error found",
542+
"test failed",
543+
"msg: allowed-test failed",
544+
"enabling -Werror",
545+
"the process crashed with 0"
546+
])
547+
expected_msg = r"Found 2 error\(s\) in command output "\
548+
r"\(output: error found\n\tthe process crashed with 0\)"
549+
550+
# String promoted to list
551+
self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
552+
r"\b(error|crashed)\b")
553+
# List of string(s)
554+
self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
555+
[r"\b(error|crashed)\b"])
556+
# List of tuple(s)
557+
self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text,
558+
[(r"\b(error|crashed)\b", ERROR)])
559+
560+
expected_msg = "Found 2 potential error(s) in command output " \
561+
"(output: error found\n\tthe process crashed with 0)"
562+
init_logging(logfile, silent=True)
563+
check_log_for_errors(input_text, [(r"\b(error|crashed)\b", WARN)])
564+
stop_logging(logfile)
565+
self.assertTrue(expected_msg in read_file(logfile))
566+
567+
expected_msg = r"Found 2 error\(s\) in command output \(output: error found\n\ttest failed\)"
568+
write_file(logfile, '')
569+
init_logging(logfile, silent=True)
570+
self.assertErrorRegex(EasyBuildError, expected_msg, check_log_for_errors, input_text, [
571+
r"\berror\b",
572+
(r"\ballowed-test failed\b", IGNORE),
573+
(r"(?i)\bCRASHED\b", WARN),
574+
"fail"
575+
])
576+
stop_logging(logfile)
577+
expected_msg = "Found 1 potential error(s) in command output (output: the process crashed with 0)"
578+
self.assertTrue(expected_msg in read_file(logfile))
579+
523580

524581
def suite():
525582
""" returns all the testcases in this module """

0 commit comments

Comments
 (0)