# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """CBMC property checking results.""" import json import logging import re from voluptuous import Schema, Any import voluptuous.humanize from cbmc_viewer import filet from cbmc_viewer import parse from cbmc_viewer import propertyt from cbmc_viewer import util from cbmc_viewer.util import flatten, choose JSON_TAG = 'viewer-result' ################################################################ # Validator for property checking results PROGRAM = 'program' STATUS = 'status' WARNING = 'warning' RESULT = 'results' PROVER = 'prover' FAILURE = 'failure' SUCCESS = 'success' VALID_RESULT = Schema({ PROGRAM: Any(str, None), # program version string STATUS: [str], # list of status messages WARNING: [str], # list of warning messages RESULT: { True : [str], # list of proved assertions False: [str] # list of failed assertions }, PROVER: Any(FAILURE, SUCCESS, None) # prover status: success or failure }, required=True) EMPTY_RESULT = { PROGRAM: None, STATUS: [], WARNING: [], RESULT: { True: [], False: [], }, PROVER: None } ################################################################ # CBMC property checking results class Result: """CBMC property checking results.""" def __init__(self, results_list=None): """Merge a list of results into a single result. Each result is given by the dict representation of a result object. """ results_list = results_list or [EMPTY_RESULT] for results in results_list: self.validate(results) self.program = choose([results[PROGRAM] for results in results_list]) self.status = flatten([results[STATUS] for results in results_list]) self.warning = flatten([results[WARNING] for results in results_list]) success = set(flatten([results[RESULT][True] for results in results_list])) failure = set(flatten([results[RESULT][False] for results in results_list])) self.results = { True: sorted(success.difference(failure), key=propertyt.key), False: sorted(failure, key=propertyt.key) } status = [results[PROVER] for results in results_list] or [None] self.prover = None if None in status else FAILURE if FAILURE in status else SUCCESS self.validate() def __repr__(self): """A dict representation of results.""" self.validate() return self.__dict__ def __str__(self): """A string representation of results.""" return json.dumps({JSON_TAG: self.__repr__()}, indent=2, sort_keys=True) def validate(self, results=None): """Validate results.""" return voluptuous.humanize.validate_with_humanized_errors( results or self.__dict__, VALID_RESULT ) def dump(self, filename=None, directory=None): """Write results to a file or stdout.""" util.dump(self, filename, directory) ################################################################ # CBMC property checking results loaded from make-result json data class ResultFromJson(Result): """CBMC property checking results loaded from make-result json data""" def __init__(self, json_files): super().__init__( [self.load_results(json_file) for json_file in json_files] ) @staticmethod def load_results(json_file): """Load CBMC results from json file""" blob = parse.parse_json_file(json_file)[JSON_TAG] # json uses strings "true" and "false" for True and False keys blob[RESULT][True] = blob[RESULT]["true"] blob[RESULT][False] = blob[RESULT]["false"] del blob[RESULT]["true"] del blob[RESULT]["false"] return blob ################################################################ # CBMC property checking results loaded from cbmc text output class ResultFromCbmcText(Result): """CBMC property checking results loaded from cbmc text output""" def __init__(self, text_files): super().__init__( [parse_cbmc_text_results(text_file) for text_file in text_files] ) ################################################################ # CBMC property checking results loaded from cbmc json output class ResultFromCbmcJson(Result): """CBMC property checking results loaded from cbmc json output""" def __init__(self, json_files): super().__init__( [parse_cbmc_json_results(json_file) for json_file in json_files] ) ################################################################ # CBMC property checking results loaded from cbmc xml output class ResultFromCbmcXml(Result): """CBMC property checking results loaded from CBMC xml output""" def __init__(self, xml_files): super().__init__( [parse_cbmc_xml_results(xml_file) for xml_file in xml_files] ) ################################################################ # Parse the text output of cbmc property checking into sections # # This parsing is required only for text output of cbmc. The text # output is just an unstructured sequence of lines with embedded # section headers. The json and xml output of cbmc are already # structured into sections, and sections are easy to find. LOG_SECTION = 'log' RESULTS_SECTION = 'results' TRACES_SECTION = 'traces' SUMMARY_SECTION = 'summary' EMPTY_SECTIONS = { LOG_SECTION: [], RESULTS_SECTION: [], TRACES_SECTION: [], SUMMARY_SECTION: [] } RESULTS_SECTION_HEADER = "** Results" TRACES_SECTION_HEADER = "Trace for" SUMMARY_SECTION_HEADER = "** " # Caution: summary header is a prefix of results header def cbmc_text_sections(text_file): """Parse the text output of cbmc property checking into sections""" sections = EMPTY_SECTIONS with open(text_file, encoding='utf-8') as blob: # The blob is an iterator that iterates through the lines of # the file and throws StopIteration at the end of the file. next_line = lambda: next(blob).rstrip() try: line = next_line() # Log section is from here to results section while not line.startswith(RESULTS_SECTION_HEADER): sections[LOG_SECTION].append(line) line = next_line() # Results section is from here to traces section or summary section # Traces section is optional: generated by `cbmc --trace` line = next_line() # skip results header while (not line.startswith(TRACES_SECTION_HEADER) and not line.startswith(SUMMARY_SECTION_HEADER)): sections[RESULTS_SECTION].append(line) line = next_line() # Traces section is from here to summary section # Traces section is optional: generated by `cbmc --trace` if line.startswith(TRACES_SECTION_HEADER): while not line.startswith(SUMMARY_SECTION_HEADER): sections[TRACES_SECTION].append(line) line = next_line() # Summary section is from here to end of file while True: sections[SUMMARY_SECTION].append(line) line = next_line() except StopIteration: return sections ################################################################ # Parse cbmc property checking output def parse_cbmc_text_results(text_file): """Parse text output of cbmc property checking""" try: sections = cbmc_text_sections(text_file) except OSError as error: raise UserWarning(f"Can't parse {text_file}: {str(error)}") from error results = dict(EMPTY_RESULT) results[PROGRAM] = cbmc_text_program(sections[LOG_SECTION]) results[STATUS] = cbmc_text_status(sections[LOG_SECTION], sections[SUMMARY_SECTION]) results[WARNING] = cbmc_text_warnings(sections[LOG_SECTION]) results[RESULT] = cbmc_text_results(sections[RESULTS_SECTION]) results[PROVER] = cbmc_text_prover(sections[SUMMARY_SECTION]) return results def parse_cbmc_json_results(json_file): """Parse json output of cbmc property checking""" blob = parse.parse_json_file(json_file) if blob is None: return EMPTY_RESULT results = dict(EMPTY_RESULT) results[PROGRAM] = cbmc_json_program(blob) results[STATUS] = cbmc_json_status(blob) results[WARNING] = cbmc_json_warnings(blob) results[RESULT] = cbmc_json_results(blob) results[PROVER] = cbmc_json_prover(blob) return results def parse_cbmc_xml_results(xml_file): """Parse xml output of cbmc property checking""" blob = parse.parse_xml_file(xml_file) if blob is None: return EMPTY_RESULT results = dict(EMPTY_RESULT) results[PROGRAM] = cbmc_xml_program(blob) results[STATUS] = cbmc_xml_status(blob) results[WARNING] = cbmc_xml_warnings(blob) results[RESULT] = cbmc_xml_results(blob) results[PROVER] = cbmc_xml_prover(blob) return results # Keys used in cbmc json output JSON_DESCRIPTION_KEY = 'description' JSON_MESSAGE_TEXT_KEY = 'messageText' JSON_MESSAGE_TYPE_KEY = 'messageType' JSON_PROGRAM_KEY = 'program' JSON_PROPERTY_KEY = 'property' JSON_PROVER_STATUS_KEY = 'cProverStatus' JSON_RESULT_KEY = 'result' JSON_STATUS_KEY = 'status' JSON_STATUS_MESSAGE = 'STATUS-MESSAGE' JSON_SUCCESS = 'SUCCESS' JSON_WARNING = 'WARNING' JSON_PROVER_STATUS_SUCCESS = 'success' # Keys used in cbmc xml output XML_FAILURE_TAG = 'failure' XML_GOTO_TRACE_TAG = 'goto_trace' XML_LOCATION_TAG = 'location' XML_MESSAGE_TAG = 'message' XML_MESSAGE_TEXT_TAG = 'text' XML_PROGRAM_TAG = 'program' XML_PROVER_STATUS_TAG = 'cprover-status' XML_RESULT_TAG = 'result' XML_FAILURE_PROPERTY_ATTR = 'property' XML_FAILURE_REASON_ATTR = 'reason' XML_MESSAGE_TYPE_ATTR = 'type' XML_RESULT_PROPERTY_ATTR = 'property' XML_RESULT_STATUS_ATTR = 'status' XML_STATUS_MESSAGE = 'STATUS-MESSAGE' XML_SUCCESS_STATUS = 'SUCCESS' XML_WARNING_MESSAGE = 'WARNING' ################################################################ # Find PROGRAM in cbmc property checking output def cbmc_text_program(log_section): """Find program in cbmc text output""" for line in log_section: if line.startswith('CBMC version'): return line return None def cbmc_json_program(blobs): """Find program in cbmc json output""" for blob in blobs: program = blob.get(JSON_PROGRAM_KEY) if program: return program return None def cbmc_xml_program(blob): """Find program in cbmc xml output""" program = blob.find(XML_PROGRAM_TAG) if program is not None: return program.text return None ################################################################ # Find STATUS messages in cbmc property checking output def cbmc_text_status(log_section, summary_section): """Find status messages in cbmc text output""" status = [] for line in log_section: if line and not line.startswith('**** WARNING:'): status.append(line) for line in summary_section: if line and line.startswith('VERIFICATION '): status.append(line) return status def cbmc_json_status(blobs): """Find status messages in cbmc json output""" status = [] for blob in blobs: kind = blob.get(JSON_MESSAGE_TYPE_KEY) text = blob.get(JSON_MESSAGE_TEXT_KEY) if kind == JSON_STATUS_MESSAGE: status.append(text) return status def cbmc_xml_status(blob): """Find status messages in cbmc xml output""" status = [] for msg in blob.iter(XML_MESSAGE_TAG): if msg.get(XML_MESSAGE_TYPE_ATTR) == XML_STATUS_MESSAGE: status.append(msg.find(XML_MESSAGE_TEXT_TAG).text) return status ################################################################ # Find WARNING messages in cbmc property checking output def cbmc_text_warnings(log_section): """Find warning messages in cbmc text output""" warnings = [] for line in log_section: if line.startswith('**** WARNING:'): warnings.append(line) return warnings def cbmc_json_warnings(blobs): """Find warning messages in cbmc json output""" warnings = [] for blob in blobs: kind = blob.get(JSON_MESSAGE_TYPE_KEY) text = blob.get(JSON_MESSAGE_TEXT_KEY) if kind == JSON_WARNING: warnings.append(text) return warnings def cbmc_xml_warnings(blob): """Find warning messages in cbmc xml output""" warnings = [] for msg in blob.iter(XML_MESSAGE_TAG): if msg.get(XML_MESSAGE_TYPE_ATTR) == XML_WARNING_MESSAGE: warnings.append(msg.find(XML_MESSAGE_TEXT_TAG).text) return warnings ################################################################ # Find RESULTS in cbmc property checking output EMPTY_RESULT_RESULTS = {True: [], False: []} def cbmc_text_results(results_section): """Find results in cbmc text output""" results = EMPTY_RESULT_RESULTS for line in results_section: # Lines given property checking results have the form # [name] srcloc description: SUCCESS|FAILURE match = re.match(r'\[([^ ]*)\].*: ((FAILURE)|(SUCCESS))', line) if match: name, status = match.groups()[:2] results[status == 'SUCCESS'].append(name) return results def cbmc_json_results(blobs): """Find results in cbmc json output""" results = EMPTY_RESULT_RESULTS for blob in blobs: for result in blob.get(JSON_RESULT_KEY) or []: name, status = result[JSON_PROPERTY_KEY], result[JSON_STATUS_KEY] results[status == JSON_SUCCESS].append(name) return results def cbmc_xml_results(blob): """Find results in cbmc xml output""" results = EMPTY_RESULT_RESULTS # cbmc normal output produces property checking results for all properties for result in blob.iter(XML_RESULT_TAG): name = result.get(XML_RESULT_PROPERTY_ATTR) status = result.get(XML_RESULT_STATUS_ATTR) results[status == XML_SUCCESS_STATUS].append(name) # cbmc --stop-on-fail output produces one trace with one failure if not (results[True] or results[False]): trace = blob.find(XML_GOTO_TRACE_TAG) if trace is not None: results[False] = [trace.find(XML_FAILURE_TAG).get(XML_FAILURE_PROPERTY_ATTR)] return results ################################################################ # Find PROVER status in cbmc property checking output def cbmc_text_prover(summary_section): """Find prover status in cbmc text output""" # Infer prover status from the lines 'VERIFICATION SUCCESSFUL' or # 'VERIFICATION FAILED' in the summary section. # In rare cases, cbmc can solve the constraint problem without # invoking the constraint solver, and the string '** X of Y # failed` that we use as the summary header is not printed. The # string 'VERIFICATION SUCCESSFUL' that we look for in the summary # section is absorbed into the log section, and the summary # section is empty. if not summary_section: return SUCCESS for line in summary_section: if line == 'VERIFICATION FAILED': return FAILURE if line == 'VERIFICATION SUCCESSFUL': return SUCCESS return None def cbmc_json_prover(blobs): """Find prover status in cbmc json output""" for blob in blobs: prover = blob.get(JSON_PROVER_STATUS_KEY) if prover: return SUCCESS if prover == JSON_PROVER_STATUS_SUCCESS else FAILURE return None def cbmc_xml_prover(blob): """Find prover status in cbmc xml output""" prover = blob.find(XML_PROVER_STATUS_TAG) if prover is not None: return SUCCESS if prover.text == XML_SUCCESS_STATUS else FAILURE return None ################################################################ # make-result def fail(msg): """Log failure and raise exception.""" logging.info(msg) raise UserWarning(msg) def make_result(args): """Implementation of make-result.""" viewer_result, cbmc_result = args.viewer_result, args.result if viewer_result: if filet.all_json_files(viewer_result): return ResultFromJson(viewer_result) fail(f"Expected a list of json files: {viewer_result}") if cbmc_result: if filet.all_text_files(cbmc_result): return ResultFromCbmcText(cbmc_result) if filet.all_json_files(cbmc_result): return ResultFromCbmcJson(cbmc_result) if filet.all_xml_files(cbmc_result): return ResultFromCbmcXml(cbmc_result) fail(f"Expected a list of text files, json files, or xml files: {cbmc_result}") logging.info("make-result: nothing to do: need " "cbmc property checking results or " "--viewer-result") return Result() def make_and_save_result(args, path=None): """Make result object and write to file or stdout""" obj = make_result(args) util.save(obj, path) return obj ################################################################