Home | History | Annotate | Download | only in scripts
      1 #!/usr/bin/python -i
      2 
      3 import sys
      4 try:
      5     import urllib.request as urllib2
      6 except ImportError:
      7     import urllib2
      8 from bs4 import BeautifulSoup
      9 import json
     10 import vuid_mapping
     11 
     12 #############################
     13 # spec.py script
     14 #
     15 # Overview - this script is intended to generate validation error codes and message strings from the json spec file
     16 #  that contains all of the valid usage statements. In addition to generating the header file, it provides a number of
     17 #  corrollary services to aid in generating/updating the header.
     18 #
     19 # Ideal flow - Pull the valid usage text and IDs from the spec json, pull the IDs from the validation error database,
     20 #  then update the database with any new IDs from the json file and generate new database and header file.
     21 #
     22 # TODO:
     23 #  1. When VUs go away (in error DB, but not in json) need to report them and remove from DB as deleted
     24 #
     25 #############################
     26 
     27 
     28 out_filename = "../layers/vk_validation_error_messages.h" # can override w/ '-out <filename>' option
     29 db_filename = "../layers/vk_validation_error_database.txt" # can override w/ '-gendb <filename>' option
     30 json_filename = None # con pass in w/ '-json <filename> option
     31 gen_db = False # set to True when '-gendb <filename>' option provided
     32 json_compare = False # compare existing DB to json file input
     33 json_url = "https://www.khronos.org/registry/vulkan/specs/1.0-extensions/validation/validusage.json"
     34 read_json = False
     35 # This is the root spec link that is used in error messages to point users to spec sections
     36 #old_spec_url = "https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html"
     37 spec_url = "https://www.khronos.org/registry/vulkan/specs/1.0-extensions/html/vkspec.html"
     38 core_url = "https://www.khronos.org/registry/vulkan/specs/1.0/html/vkspec.html"
     39 ext_url = "https://www.khronos.org/registry/vulkan/specs/1.0-extensions/html/vkspec.html"
     40 # After the custom validation error message, this is the prefix for the standard message that includes the
     41 #  spec valid usage language as well as the link to nearest section of spec to that language
     42 error_msg_prefix = "The spec valid usage text states "
     43 validation_error_enum_name = "VALIDATION_ERROR_"
     44 
     45 def printHelp():
     46     print ("Usage: python spec.py [-out <headerfile.h>] [-gendb <databasefile.txt>] [-update] [-json <json_file>] [-help]")
     47     print ("\n Default script behavior is to parse the specfile and generate a header of unique error enums and corresponding error messages based on the specfile.\n")
     48     print ("  Default specfile is from online at %s" % (spec_url))
     49     print ("  Default headerfile is %s" % (out_filename))
     50     print ("  Default databasefile is %s" % (db_filename))
     51     print ("\nIf '-gendb' option is specified then a database file is generated to default file or <databasefile.txt> if supplied. The database file stores")
     52     print ("  the list of enums and their error messages.")
     53     print ("\nIf '-update' option is specified this triggers the master flow to automate updating header and database files using default db file as baseline")
     54     print ("  and online spec file as the latest. The default header and database files will be updated in-place for review and commit to the git repo.")
     55     print ("\nIf '-json' option is used trigger the script to load in data from a json file.")
     56     print ("\nIf '-json-file' option is it will point to a local json file, else '%s' is used from the web." % (json_url))
     57 
     58 def get8digithex(dec_num):
     59     """Convert a decimal # into an 8-digit hex"""
     60     if dec_num > 4294967295:
     61         print ("ERROR: Decimal # %d can't be represented in 8 hex digits" % (dec_num))
     62         sys.exit()
     63     hex_num = hex(dec_num)
     64     return hex_num[2:].zfill(8)
     65 
     66 class Specification:
     67     def __init__(self):
     68         self.tree   = None
     69         self.error_db_dict = {} # dict of previous error values read in from database file
     70         self.delimiter = '~^~' # delimiter for db file
     71         # Global dicts used for tracking spec updates from old to new VUs
     72         self.orig_no_link_msg_dict = {} # Pair of API,Original msg w/o spec link to ID list mapping
     73         self.orig_core_msg_dict = {} # Pair of API,Original core msg (no link or section) to ID list mapping
     74         self.last_mapped_id = -10 # start as negative so we don't hit an accidental sequence
     75         self.orig_test_imp_enums = set() # Track old enums w/ tests and/or implementation to flag any that aren't carried fwd
     76         # Dict of data from json DB
     77         # Key is API,<short_msg> which leads to dict w/ following values
     78         #   'ext' -> <core|<ext_name>>
     79         #   'string_vuid' -> <string_vuid>
     80         #   'number_vuid' -> <numerical_vuid>
     81         self.json_db = {}
     82         self.json_missing = 0
     83         self.struct_to_func_map = {} # Map structs to the API func that they fall under in the spec
     84         self.duplicate_json_key_count = 0
     85         self.copyright = """/* THIS FILE IS GENERATED.  DO NOT EDIT. */
     86 
     87 /*
     88  * Vulkan
     89  *
     90  * Copyright (c) 2016 Google Inc.
     91  * Copyright (c) 2016 LunarG, Inc.
     92  *
     93  * Licensed under the Apache License, Version 2.0 (the "License");
     94  * you may not use this file except in compliance with the License.
     95  * You may obtain a copy of the License at
     96  *
     97  *     http://www.apache.org/licenses/LICENSE-2.0
     98  *
     99  * Unless required by applicable law or agreed to in writing, software
    100  * distributed under the License is distributed on an "AS IS" BASIS,
    101  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    102  * See the License for the specific language governing permissions and
    103  * limitations under the License.
    104  *
    105  * Author: Tobin Ehlis <tobine (at] google.com>
    106  */"""
    107 
    108     def readJSON(self):
    109         """Read in JSON file"""
    110         if json_filename is not None:
    111             with open(json_filename) as jsf:
    112                 self.json_data = json.load(jsf, encoding='utf-8')
    113         else:
    114             response = urllib2.urlopen(json_url).read().decode('utf-8')
    115             self.json_data = json.loads(response)
    116 
    117     def parseJSON(self):
    118         """Parse JSON VUIDs into data struct"""
    119         # Format of JSON file is:
    120         # "API": { "core|EXT": [ {"vuid": "<id>", "text": "<VU txt>"}]},
    121         # "VK_KHX_external_memory" & "VK_KHX_device_group" - extension case (vs. "core")
    122         for top_level in sorted(self.json_data):
    123             if "validation" == top_level:
    124                 for api in sorted(self.json_data[top_level]):
    125                     for ext in sorted(self.json_data[top_level][api]):
    126                         for vu_txt_dict in self.json_data[top_level][api][ext]:
    127                             print ("Looking at dict for api:ext entry %s:%s" % (api, ext))
    128                             vuid = vu_txt_dict['vuid']
    129                             vutxt = vu_txt_dict['text']
    130                             #print ("%s:%s:%s:%s" % (api, ext, vuid, vutxt))
    131                             #print ("VUTXT orig:%s" % (vutxt))
    132                             just_txt = BeautifulSoup(vutxt, 'html.parser')
    133                             #print ("VUTXT only:%s" % (just_txt.get_text()))
    134                             num_vuid = vuid_mapping.convertVUID(vuid)
    135                             self.json_db[vuid] = {}
    136                             self.json_db[vuid]['ext'] = ext
    137                             self.json_db[vuid]['number_vuid'] = num_vuid
    138                             self.json_db[vuid]['struct_func'] = api
    139                             just_txt = just_txt.get_text().strip()
    140                             unicode_map = {
    141                             u"\u2019" : "'",
    142                             u"\u201c" : "\"",
    143                             u"\u201d" : "\"",
    144                             u"\u2192" : "->",
    145                             }
    146                             for um in unicode_map:
    147                                 just_txt = just_txt.replace(um, unicode_map[um])
    148                             self.json_db[vuid]['vu_txt'] = just_txt.replace("\\", "")
    149                             print ("Spec vu txt:%s" % (self.json_db[vuid]['vu_txt']))
    150         #sys.exit()
    151 
    152     def compareJSON(self):
    153         """Compare parsed json file with existing data read in from DB file"""
    154         json_db_set = set()
    155         for vuid in self.json_db: # pull entries out and see which fields we're missing from error_db
    156             json_db_set.add(vuid)
    157         for enum in self.error_db_dict:
    158             vuid_string = self.error_db_dict[enum]['vuid_string']
    159             if vuid_string not in self.json_db:
    160                 #print ("Full string for %s is:%s" % (enum, full_error_string))
    161                 print ("WARN: Couldn't find vuid_string in json db:%s" % (vuid_string))
    162                 self.json_missing = self.json_missing + 1
    163                 self.error_db_dict[enum]['ext'] = 'core'
    164                 # TODO: Currently GL843 tracks 2 VUs that are missing from json incorrectly
    165                 #  Fix will land in 1.0.51 spec. After that we should take some alternative
    166                 #  action here to indicate that VUs have gone away.
    167                 #  Can have a removed_enums set that we add to and report to user
    168                 #sys.exit()
    169             else:
    170                 json_db_set.remove(vuid_string)
    171                 self.error_db_dict[enum]['ext'] = self.json_db[vuid_string]['ext']
    172                 if 'core' == self.json_db[vuid_string]['ext'] or '!' in self.json_db[vuid_string]['ext']:
    173                     spec_link = "%s#%s" % (core_url, vuid_string)
    174                 else:
    175                     spec_link = "%s#%s" % (ext_url, vuid_string)
    176                 self.error_db_dict[enum]['error_msg'] = "%s'%s' (%s)" % (error_msg_prefix, self.json_db[vuid_string]['vu_txt'], spec_link)
    177                 print ("Updated error_db error_msg:%s" % (self.error_db_dict[enum]['error_msg']))
    178         #sys.exit()
    179         print ("These json DB entries are not in error DB:")
    180         for extra_vuid in json_db_set:
    181             print ("\t%s" % (extra_vuid))
    182             # Add these missing entries into the error_db
    183             # Create link into core or ext spec as needed
    184             if 'core' == self.json_db[extra_vuid]['ext'] or '!' in self.json_db[extra_vuid]['ext']:
    185                 spec_link = "%s#%s" % (core_url, extra_vuid)
    186             else:
    187                 spec_link = "%s#%s" % (ext_url, extra_vuid)
    188             error_enum = "%s%s" % (validation_error_enum_name, get8digithex(self.json_db[extra_vuid]['number_vuid']))
    189             self.error_db_dict[error_enum] = {}
    190             self.error_db_dict[error_enum]['check_implemented'] = 'N'
    191             self.error_db_dict[error_enum]['testname'] = 'None'
    192             self.error_db_dict[error_enum]['api'] = self.json_db[extra_vuid]['struct_func']
    193             self.error_db_dict[error_enum]['vuid_string'] = extra_vuid
    194             self.error_db_dict[error_enum]['error_msg'] = "%s'%s' (%s)" % (error_msg_prefix, self.json_db[extra_vuid]['vu_txt'], spec_link)
    195             self.error_db_dict[error_enum]['note'] = ''
    196             self.error_db_dict[error_enum]['ext'] = self.json_db[extra_vuid]['ext']
    197             implicit = False
    198             last_segment = extra_vuid.split("-")[-1]
    199             if last_segment in vuid_mapping.implicit_type_map:
    200                 implicit = True
    201             elif not last_segment.isdigit(): # Explicit ids should only have digits in last segment
    202                 print ("ERROR: Found last segment of val error ID that isn't in implicit map and doesn't have numbers in last segment: %s" % (last_segment))
    203                 sys.exit()
    204             self.error_db_dict[error_enum]['implicit'] = implicit
    205 
    206     def genHeader(self, header_file):
    207         """Generate a header file based on the contents of a parsed spec"""
    208         print ("Generating header %s..." % (header_file))
    209         file_contents = []
    210         file_contents.append(self.copyright)
    211         file_contents.append('\n#pragma once')
    212         file_contents.append('\n// Disable auto-formatting for generated file')
    213         file_contents.append('// clang-format off')
    214         file_contents.append('\n#include <unordered_map>')
    215         file_contents.append('\n// enum values for unique validation error codes')
    216         file_contents.append('//  Corresponding validation error message for each enum is given in the mapping table below')
    217         file_contents.append('//  When a given error occurs, these enum values should be passed to the as the messageCode')
    218         file_contents.append('//  parameter to the PFN_vkDebugReportCallbackEXT function')
    219         enum_decl = ['enum UNIQUE_VALIDATION_ERROR_CODE {\n    VALIDATION_ERROR_UNDEFINED = -1,']
    220         error_string_map = ['static std::unordered_map<int, char const *const> validation_error_map{']
    221         enum_value = 0
    222         max_enum_val = 0
    223         for enum in sorted(self.error_db_dict):
    224             #print ("Header enum is %s" % (enum))
    225             # TMP: Use updated value
    226             vuid_str = self.error_db_dict[enum]['vuid_string']
    227             if vuid_str in self.json_db:
    228                 enum_value = self.json_db[vuid_str]['number_vuid']
    229             else:
    230                 enum_value = vuid_mapping.convertVUID(vuid_str)
    231             new_enum = "%s%s" % (validation_error_enum_name, get8digithex(enum_value))
    232             enum_decl.append('    %s = 0x%s,' % (new_enum, get8digithex(enum_value)))
    233             error_string_map.append('    {%s, "%s"},' % (new_enum, self.error_db_dict[enum]['error_msg'].replace('"', '\\"')))
    234             max_enum_val = max(max_enum_val, enum_value)
    235         enum_decl.append('    %sMAX_ENUM = %d,' % (validation_error_enum_name, max_enum_val + 1))
    236         enum_decl.append('};')
    237         error_string_map.append('};\n')
    238         file_contents.extend(enum_decl)
    239         file_contents.append('\n// Mapping from unique validation error enum to the corresponding error message')
    240         file_contents.append('// The error message should be appended to the end of a custom error message that is passed')
    241         file_contents.append('// as the pMessage parameter to the PFN_vkDebugReportCallbackEXT function')
    242         file_contents.extend(error_string_map)
    243         #print ("File contents: %s" % (file_contents))
    244         with open(header_file, "w") as outfile:
    245             outfile.write("\n".join(file_contents))
    246     def genDB(self, db_file):
    247         """Generate a database of check_enum, check_coded?, testname, API, VUID_string, core|ext, error_string, notes"""
    248         db_lines = []
    249         # Write header for database file
    250         db_lines.append("# This is a database file with validation error check information")
    251         db_lines.append("# Comments are denoted with '#' char")
    252         db_lines.append("# The format of the lines is:")
    253         db_lines.append("# <error_enum>%s<check_implemented>%s<testname>%s<api>%s<vuid_string>%s<core|ext>%s<errormsg>%s<note>" % (self.delimiter, self.delimiter, self.delimiter, self.delimiter, self.delimiter, self.delimiter, self.delimiter))
    254         db_lines.append("# error_enum: Unique error enum for this check of format %s<uniqueid>" % validation_error_enum_name)
    255         db_lines.append("# check_implemented: 'Y' if check has been implemented in layers, or 'N' for not implemented")
    256         db_lines.append("# testname: Name of validation test for this check, 'Unknown' for unknown, 'None' if not implemented, or 'NotTestable' if cannot be implemented")
    257         db_lines.append("# api: Vulkan API function that this check is related to")
    258         db_lines.append("# vuid_string: Unique string to identify this check")
    259         db_lines.append("# core|ext: Either 'core' for core spec or some extension string that indicates the extension required for this VU to be relevant")
    260         db_lines.append("# errormsg: The unique error message for this check that includes spec language and link")
    261         db_lines.append("# note: Free txt field with any custom notes related to the check in question")
    262         for enum in sorted(self.error_db_dict):
    263             print ("Gen DB for enum %s" % (enum))
    264             implicit = self.error_db_dict[enum]['implicit']
    265             implemented = self.error_db_dict[enum]['check_implemented']
    266             testname = self.error_db_dict[enum]['testname']
    267             note = self.error_db_dict[enum]['note']
    268             core_ext = self.error_db_dict[enum]['ext']
    269             self.error_db_dict[enum]['vuid_string'] = self.error_db_dict[enum]['vuid_string']
    270             if implicit and 'implicit' not in note: # add implicit note
    271                 if '' != note:
    272                     note = "implicit, %s" % (note)
    273                 else:
    274                     note = "implicit"
    275             db_lines.append("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s" % (enum, self.delimiter, implemented, self.delimiter, testname, self.delimiter, self.error_db_dict[enum]['api'], self.delimiter, self.error_db_dict[enum]['vuid_string'], self.delimiter, core_ext, self.delimiter, self.error_db_dict[enum]['error_msg'], self.delimiter, note))
    276         db_lines.append("\n") # newline at end of file
    277         print ("Generating database file %s" % (db_file))
    278         with open(db_file, "w") as outfile:
    279             outfile.write("\n".join(db_lines))
    280     def readDB(self, db_file):
    281         """Read a db file into a dict, refer to genDB function above for format of each line"""
    282         with open(db_file, "r", encoding='utf-8') as infile:
    283             for line in infile:
    284                 line = line.strip()
    285                 if line.startswith('#') or '' == line:
    286                     continue
    287                 db_line = line.split(self.delimiter)
    288                 if len(db_line) != 8:
    289                     print ("ERROR: Bad database line doesn't have 8 elements: %s" % (line))
    290                 error_enum = db_line[0]
    291                 implemented = db_line[1]
    292                 testname = db_line[2]
    293                 api = db_line[3]
    294                 vuid_str = db_line[4]
    295                 core_ext = db_line[5]
    296                 error_str = db_line[6]
    297                 note = db_line[7]
    298                 # Also read complete database contents into our class var for later use
    299                 self.error_db_dict[error_enum] = {}
    300                 self.error_db_dict[error_enum]['check_implemented'] = implemented
    301                 self.error_db_dict[error_enum]['testname'] = testname
    302                 self.error_db_dict[error_enum]['api'] = api
    303                 self.error_db_dict[error_enum]['vuid_string'] = vuid_str
    304                 self.error_db_dict[error_enum]['ext'] = core_ext
    305                 self.error_db_dict[error_enum]['error_msg'] = error_str
    306                 self.error_db_dict[error_enum]['note'] = note
    307                 implicit = False
    308                 last_segment = vuid_str.split("-")[-1]
    309                 if last_segment in vuid_mapping.implicit_type_map:
    310                     implicit = True
    311                 elif not last_segment.isdigit(): # Explicit ids should only have digits in last segment
    312                     print ("ERROR: Found last segment of val error ID that isn't in implicit map and doesn't have numbers in last segment: %s" % (last_segment))
    313                     sys.exit()
    314                 self.error_db_dict[error_enum]['implicit'] = implicit
    315 if __name__ == "__main__":
    316     i = 1
    317     use_online = True # Attempt to grab spec from online by default
    318     while (i < len(sys.argv)):
    319         arg = sys.argv[i]
    320         i = i + 1
    321         if (arg == '-json-file'):
    322             json_filename = sys.argv[i]
    323             i = i + 1
    324         elif (arg == '-json'):
    325             read_json = True
    326         elif (arg == '-json-compare'):
    327             json_compare = True
    328         elif (arg == '-out'):
    329             out_filename = sys.argv[i]
    330             i = i + 1
    331         elif (arg == '-gendb'):
    332             gen_db = True
    333             # Set filename if supplied, else use default
    334             if i < len(sys.argv) and not sys.argv[i].startswith('-'):
    335                 db_filename = sys.argv[i]
    336                 i = i + 1
    337         elif (arg == '-update'):
    338             read_json = True
    339             json_compare = True
    340             gen_db = True
    341         elif (arg in ['-help', '-h']):
    342             printHelp()
    343             sys.exit()
    344     spec = Specification()
    345     if read_json:
    346         spec.readJSON()
    347         spec.parseJSON()
    348         #sys.exit()
    349     if (json_compare):
    350         # Read in current spec info from db file
    351         (orig_err_msg_dict) = spec.readDB(db_filename)
    352         spec.compareJSON()
    353         print ("Found %d missing db entries in json db" % (spec.json_missing))
    354         print ("Found %d duplicate json entries" % (spec.duplicate_json_key_count))
    355         spec.genDB(db_filename)
    356     if (gen_db):
    357         spec.genDB(db_filename)
    358     print ("Writing out file (-out) to '%s'" % (out_filename))
    359     spec.genHeader(out_filename)
    360