comparison planemo/lib/python3.7/site-packages/galaxy/tool_util/output_checker.py @ 1:56ad4e20f292 draft

"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author guerler
date Fri, 31 Jul 2020 00:32:28 -0400
parents
children
comparison
equal deleted inserted replaced
0:d30785e31577 1:56ad4e20f292
1 import re
2 from logging import getLogger
3
4 from galaxy.tool_util.parser.stdio import StdioErrorLevel
5 from galaxy.util import unicodify
6 from galaxy.util.bunch import Bunch
7
8 log = getLogger(__name__)
9
10 DETECTED_JOB_STATE = Bunch(
11 OK='ok',
12 OUT_OF_MEMORY_ERROR='oom_error',
13 GENERIC_ERROR='generic_error',
14 )
15
16 ERROR_PEAK = 2000
17
18
19 def check_output_regex(job_id_tag, regex, stream, stream_name, job_messages, max_error_level):
20 """
21 check a single regex against a stream
22
23 regex the regex to check
24 stream the stream to search in
25 job_messages a list where the descriptions of the detected regexes can be appended
26 max_error_level the maximum error level that has been detected so far
27 returns the max of the error_level of the regex and the given max_error_level
28 """
29 regex_match = re.search(regex.match, stream, re.IGNORECASE)
30 if regex_match:
31 reason = __regex_err_msg(regex_match, stream_name, regex)
32 job_messages.append(reason)
33 return max(max_error_level, regex.error_level)
34 return max_error_level
35
36
37 def check_output(stdio_regexes, stdio_exit_codes, stdout, stderr, tool_exit_code, job_id_tag):
38 """
39 Check the output of a tool - given the stdout, stderr, and the tool's
40 exit code, return DETECTED_JOB_STATE.OK if the tool exited succesfully or
41 error type otherwise. No exceptions should be thrown. If this code encounters
42 an exception, it returns OK so that the workflow can continue;
43 otherwise, a bug in this code could halt workflow progress.
44
45 Note that, if the tool did not define any exit code handling or
46 any stdio/stderr handling, then it reverts back to previous behavior:
47 if stderr contains anything, then False is returned.
48 """
49 # By default, the tool succeeded. This covers the case where the code
50 # has a bug but the tool was ok, and it lets a workflow continue.
51 state = DETECTED_JOB_STATE.OK
52
53 stdout = unicodify(stdout, strip_null=True)
54 stderr = unicodify(stderr, strip_null=True)
55
56 # messages (descriptions of the detected exit_code and regexes)
57 # to be prepended to the stdout/stderr after all exit code and regex tests
58 # are done (otherwise added messages are searched again).
59 # messages are added it the order of detection
60
61 # If job is failed, track why.
62 job_messages = []
63
64 try:
65 # Check exit codes and match regular expressions against stdout and
66 # stderr if this tool was configured to do so.
67 # If there is a regular expression for scanning stdout/stderr,
68 # then we assume that the tool writer overwrote the default
69 # behavior of just setting an error if there is *anything* on
70 # stderr.
71 if len(stdio_regexes) > 0 or len(stdio_exit_codes) > 0:
72 # Check the exit code ranges in the order in which
73 # they were specified. Each exit_code is a StdioExitCode
74 # that includes an applicable range. If the exit code was in
75 # that range, then apply the error level and add a message.
76 # If we've reached a fatal error rule, then stop.
77 max_error_level = StdioErrorLevel.NO_ERROR
78 if tool_exit_code is not None:
79 for stdio_exit_code in stdio_exit_codes:
80 if (tool_exit_code >= stdio_exit_code.range_start and
81 tool_exit_code <= stdio_exit_code.range_end):
82 # Tack on a generic description of the code
83 # plus a specific code description. For example,
84 # this might prepend "Job 42: Warning (Out of Memory)\n".
85 code_desc = stdio_exit_code.desc
86 if None is code_desc:
87 code_desc = ""
88 desc = "%s: Exit code %d (%s)" % (
89 StdioErrorLevel.desc(stdio_exit_code.error_level),
90 tool_exit_code,
91 code_desc)
92 reason = {
93 'type': 'exit_code',
94 'desc': desc,
95 'exit_code': tool_exit_code,
96 'code_desc': code_desc,
97 'error_level': stdio_exit_code.error_level,
98 }
99 log.info("Job %s: %s" % (job_id_tag, reason))
100 job_messages.append(reason)
101 max_error_level = max(max_error_level,
102 stdio_exit_code.error_level)
103 if max_error_level >= StdioErrorLevel.MAX:
104 break
105
106 if max_error_level < StdioErrorLevel.FATAL_OOM:
107 # We'll examine every regex. Each regex specifies whether
108 # it is to be run on stdout, stderr, or both. (It is
109 # possible for neither stdout nor stderr to be scanned,
110 # but those regexes won't be used.) We record the highest
111 # error level, which are currently "warning" and "fatal".
112 # If fatal, then we set the job's state to ERROR.
113 # If warning, then we still set the job's state to OK
114 # but include a message. We'll do this if we haven't seen
115 # a fatal error yet
116 for regex in stdio_regexes:
117 # If ( this regex should be matched against stdout )
118 # - Run the regex's match pattern against stdout
119 # - If it matched, then determine the error level.
120 # o If it was fatal, then we're done - break.
121 if regex.stderr_match:
122 max_error_level = check_output_regex(job_id_tag, regex, stderr, 'stderr', job_messages, max_error_level)
123 if max_error_level >= StdioErrorLevel.MAX:
124 break
125
126 if regex.stdout_match:
127 max_error_level = check_output_regex(job_id_tag, regex, stdout, 'stdout', job_messages, max_error_level)
128 if max_error_level >= StdioErrorLevel.MAX:
129 break
130
131 # If we encountered a fatal error, then we'll need to set the
132 # job state accordingly. Otherwise the job is ok:
133 if max_error_level == StdioErrorLevel.FATAL_OOM:
134 state = DETECTED_JOB_STATE.OUT_OF_MEMORY_ERROR
135 elif max_error_level >= StdioErrorLevel.FATAL:
136 log.debug("Tool exit code indicates an error, failing job.")
137 state = DETECTED_JOB_STATE.GENERIC_ERROR
138
139 # When there are no regular expressions and no exit codes to check,
140 # default to the previous behavior: when there's anything on stderr
141 # the job has an error, and the job is ok otherwise.
142 else:
143 # TODO: Add in the tool and job id:
144 # log.debug( "Tool did not define exit code or stdio handling; "
145 # + "checking stderr for success" )
146 if stderr:
147 state = DETECTED_JOB_STATE.GENERIC_ERROR
148
149 if state != DETECTED_JOB_STATE.OK:
150 peak = stderr[0:ERROR_PEAK] if stderr else ""
151 log.debug("job failed, detected state %s, standard error is - [%s]" % (state, peak))
152 except Exception:
153 log.exception("Job state check encountered unexpected exception; assuming execution successful")
154
155 return state, stdout, stderr, job_messages
156
157
158 def __regex_err_msg(match, stream, regex):
159 """
160 Return a message about the match on tool output using the given
161 ToolStdioRegex regex object. The regex_match is a MatchObject
162 that will contain the string matched on.
163 """
164 # Get the description for the error level:
165 desc = StdioErrorLevel.desc(regex.error_level) + ": "
166 mstart = match.start()
167 mend = match.end()
168 if mend - mstart > 256:
169 match_str = match.string[mstart : mstart + 256] + "..."
170 else:
171 match_str = match.string[mstart: mend]
172
173 # If there's a description for the regular expression, then use it.
174 # Otherwise, we'll take the first 256 characters of the match.
175 if regex.desc is not None:
176 desc += regex.desc
177 else:
178 desc += "Matched on %s" % match_str
179 return {
180 "type": "regex",
181 "stream": stream,
182 "desc": desc,
183 "code_desc": regex.desc,
184 "match": match_str,
185 "error_level": regex.error_level,
186 }