1 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 # Licensed under the Apache License, Version 2.0 (the "License").
4 # You may not use this file except in compliance with the License.
5 # A copy of the License is located at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # or in the "license" file accompanying this file. This file is distributed
10 # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11 # express or implied. See the License for the specific language governing
12 # permissions and limitations under the License.
21 ################################################################################
23 # ``````````````````````````````````````````````````````````````````````````````
24 # Each of these classes has get_job_fields method. The class evaluates what
25 # the result of a single Litani job is and returns that result in the dict.
26 ################################################################################
30 @dataclasses.dataclass
31 class OutcomeTableDecider:
32 """Decide what the result of a job is based on an outcome table.
34 An 'outcome table' is a mapping from 'outcomes'---like return codes,
35 timeouts, or a wildcard---to whether or not the job is successful. This
36 class takes a user-specified or default outcome table, and decides what the
37 result of a single Litani job was by iterating through the table.
43 loaded_from_file: bool
46 def _get_wildcard_outcome(self):
47 for outcome in self.table["outcomes"]:
48 if outcome["type"] == "wildcard":
49 return outcome["action"]
51 "Outcome table contains no wildcard rule: %s" % json.dumps(
52 self.table, indent=2))
55 def _get_timeout_outcome(self):
56 for outcome in self.table["outcomes"]:
57 if outcome["type"] == "timeout":
58 return outcome["action"]
62 def _get_return_code_outcome(self, return_code):
63 for outcome in self.table["outcomes"]:
64 if outcome["type"] == "return-code" and \
65 outcome["value"] == return_code:
66 return outcome["action"]
70 def get_job_fields(self):
72 "outcome": self.get_outcome(),
73 "loaded_outcome_dict":
74 self.table if self.loaded_from_file else None
78 def get_outcome(self):
79 timeout_outcome = self._get_timeout_outcome()
80 if self.timeout_reached:
82 return timeout_outcome
83 return self._get_wildcard_outcome()
85 rc_outcome = self._get_return_code_outcome(self.return_code)
89 return self._get_wildcard_outcome()
93 ################################################################################
95 ################################################################################
98 def _get_default_outcome_dict(args):
99 """Litani's default behavior if the user does not specify an outcome table.
101 This is not a constant dict as it also depends on whether the user passed in
102 command-line flags that affect how the result is decided, like
103 --ignore-returns etc.
112 elif args.timeout_ignore:
115 "action": "fail_ignored",
119 for rc in args.ok_returns:
121 "type": "return-code",
126 if args.ignore_returns:
127 for rc in args.ignore_returns:
129 "type": "return-code",
131 "action": "fail_ignored",
135 "type": "return-code",
143 return {"outcomes": outcomes}
146 def validate_outcome_table(table):
150 logging.debug("Skipping outcome table validation as voluptuous is not installed")
153 actions = voluptuous.Any("success", "fail", "fail_ignored")
154 schema = voluptuous.Schema({
155 # A description of the outcome table as a whole.
156 voluptuous.Optional("comment"): str,
158 # We use the first item in this list that matches the job
159 "outcomes": [voluptuous.Any({
160 "type": "return-code",
163 voluptuous.Optional("comment"): str,
167 voluptuous.Optional("comment"): str,
171 voluptuous.Optional("comment"): str,
174 voluptuous.humanize.validate_with_humanized_errors(table, schema)
177 def _get_outcome_table_job_decider(args, return_code, timeout_reached):
178 if args.outcome_table:
179 _, ext = os.path.splitext(args.outcome_table)
180 with open(args.outcome_table) as handle:
182 outcome_table = json.load(handle)
185 outcome_table = yaml.safe_load(handle)
187 raise UserWarning("Unsupported outcome table format (%s)" % ext)
188 loaded_from_file = True
190 loaded_from_file = False
191 outcome_table = _get_default_outcome_dict(args)
193 logging.debug("Using outcome table: %s", json.dumps(outcome_table, indent=2))
194 validate_outcome_table(outcome_table)
196 return OutcomeTableDecider(
197 outcome_table, return_code, timeout_reached,
198 loaded_from_file=loaded_from_file)
201 ################################################################################
203 ################################################################################
206 def fill_in_result(runner, job_data, args):
207 """Add fields pertaining to job result to job_data dict
209 The 'result' of a job can be evaluated in several ways. The most simple
210 mechanism, where a return code of 0 means success and anything else is a
211 failure, is encoded by the "default outcome table". Users can also supply
212 their own outcome table as a JSON file, and other mechanisms could be
213 available in the future.
215 Depending on how we are to evaluate the result, we construct an instance of
216 one of the Decider classes in this module, and use the Decider to evaluate
217 the result of the job. The result is a dict, whose keys and values we add to
221 job_data["complete"] = True
222 job_data["timeout_reached"] = runner.reached_timeout()
223 job_data["command_return_code"] = runner.get_return_code()
224 job_data["memory_trace"] = runner.get_memory_trace()
226 # These get set by the deciders
227 job_data["loaded_outcome_dict"] = None
229 decider = _get_outcome_table_job_decider(
230 args, runner.get_return_code(), runner.reached_timeout())
232 fields = decider.get_job_fields()
233 for k, v in fields.items():
235 job_data["wrapper_return_code"] = 1 if job_data["outcome"] == "fail" else 0