scripts:Initial version of validation stats script
authorTobin Ehlis <tobine@google.com>
Mon, 31 Oct 2016 19:27:36 +0000 (13:27 -0600)
committerTobin Ehlis <tobine@google.com>
Wed, 2 Nov 2016 20:36:08 +0000 (14:36 -0600)
Adding vk_validation_stat.py script to display validation stats.

It parses the database file, unique enum header, source, and testfiles
and reports on the total number of checks and any discrepancies between
the database file and what's actually implemented.

layers/vk_validation_error_database.txt
layers/vk_validation_stats.py [new file with mode: 0755]

index d93408d..404068b 100644 (file)
@@ -930,7 +930,7 @@ VALIDATION_ERROR_00936~^~Y~^~Unknown~^~vkUpdateDescriptorSets~^~For more informa
 VALIDATION_ERROR_00937~^~Y~^~Unknown~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'descriptorType must match the type of dstBinding within dstSet' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
 VALIDATION_ERROR_00938~^~Y~^~WriteDescriptorSetIntegrityCheck~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'The sum of dstArrayElement and descriptorCount must be less than or equal to the number of array elements in the descriptor set binding specified by dstBinding, and all applicable consecutive bindings, as described by consecutive binding updates' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
 VALIDATION_ERROR_00939~^~U~^~Unknown~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'If descriptorType is VK_DESCRIPTOR_TYPE_SAMPLER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, or VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, pImageInfo must be a pointer to an array of descriptorCount valid VkDescriptorImageInfo structures' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
-VALIDATION_ERROR_00940~^~Y~^~InvalidBufferViewObject~Unknown~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'If descriptorType is VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER or VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, pTexelBufferView must be a pointer to an array of descriptorCount valid VkBufferView handles' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
+VALIDATION_ERROR_00940~^~Y~^~InvalidBufferViewObject~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'If descriptorType is VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER or VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, pTexelBufferView must be a pointer to an array of descriptorCount valid VkBufferView handles' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
 VALIDATION_ERROR_00941~^~U~^~Unknown~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'If descriptorType is VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, or VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, pBufferInfo must be a pointer to an array of descriptorCount valid VkDescriptorBufferInfo structures' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
 VALIDATION_ERROR_00942~^~Y~^~SampleDescriptorUpdateError~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'If descriptorType is VK_DESCRIPTOR_TYPE_SAMPLER or VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, and dstSet was not allocated with a layout that included immutable samplers for dstBinding with descriptorType, the sampler member of any given element of pImageInfo must be a valid VkSampler object' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
 VALIDATION_ERROR_00943~^~Y~^~ImageViewDescriptorUpdateError~^~vkUpdateDescriptorSets~^~For more information refer to Vulkan Spec Section '13.2.4. Descriptor Set Updates' which states 'If descriptorType is VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, or VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, the imageView and imageLayout members of any given element of pImageInfo must be a valid VkImageView and VkImageLayout, respectively' (https://www.khronos.org/registry/vulkan/specs/1.0-extensions/xhtml/vkspec.html#descriptorsets-updates-consecutive)~^~
diff --git a/layers/vk_validation_stats.py b/layers/vk_validation_stats.py
new file mode 100755 (executable)
index 0000000..47c593f
--- /dev/null
@@ -0,0 +1,345 @@
+#!/usr/bin/env python3
+# Copyright (c) 2015-2016 The Khronos Group Inc.
+# Copyright (c) 2015-2016 Valve Corporation
+# Copyright (c) 2015-2016 LunarG, Inc.
+# Copyright (c) 2015-2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Author: Tobin Ehlis <tobine@google.com>
+
+import argparse
+import os
+import sys
+import platform
+
+# vk_validation_stats.py overview
+# This script is intended to generate statistics on the state of validation code
+#  based on information parsed from the source files and the database file
+# Here's what it currently does:
+#  1. Parse vk_validation_error_database.txt to store claimed state of validation checks
+#  2. Parse vk_validation_error_messages.h to verify the actual checks in header vs. the
+#     claimed state of the checks
+#  3. Parse source files to identify which checks are implemented and verify that this
+#     exactly matches the list of checks claimed to be implemented in the database
+#  4. Parse test file(s) and verify that reported tests exist
+#  5. Report out stats on number of checks, implemented checks, and duplicated checks
+#
+# TODO:
+#  1. Would also like to report out number of existing checks that don't yet use new, unique enum
+#  2. Could use notes to store custom fields (like TODO) and print those out here
+#  3. Update test code to check if tests use new, unique enums to check for errors instead of strings
+
+db_file = 'vk_validation_error_database.txt'
+layer_source_files = [
+'core_validation.cpp',
+'descriptor_sets.cpp',
+'parameter_validation.cpp',
+'object_tracker.cpp',
+]
+header_file = 'vk_validation_error_messages.h'
+# TODO : Don't hardcode linux path format if we want this to run on windows
+test_file = '../tests/layer_validation_tests.cpp'
+
+
+class ValidationDatabase:
+    def __init__(self, filename=db_file):
+        self.db_file = filename
+        self.delimiter = '~^~'
+        self.db_dict = {} # complete dict of all db values per error enum
+        # specialized data structs with slices of complete dict
+        self.db_implemented_enums = [] # list of all error enums claiming to be implemented in database file
+        self.db_enum_to_tests = {} # dict where enum is key to lookup list of tests implementing the enum
+        #self.src_implemented_enums
+    def read(self):
+        """Read a database file into internal data structures, format of each line is <enum><implemented Y|N?><testname><api><errormsg><notes>"""
+        #db_dict = {} # This is a simple db of just enum->errormsg, the same as is created from spec
+        #max_id = 0
+        with open(self.db_file, "r") as infile:
+            for line in infile:
+                line = line.strip()
+                if line.startswith('#') or '' == line:
+                    continue
+                db_line = line.split(self.delimiter)
+                if len(db_line) != 6:
+                    print "ERROR: Bad database line doesn't have 6 elements: %s" % (line)
+                error_enum = db_line[0]
+                implemented = db_line[1]
+                testname = db_line[2]
+                api = db_line[3]
+                error_str = db_line[4]
+                note = db_line[5]
+                # Read complete database contents into our class var for later use
+                self.db_dict[error_enum] = {}
+                self.db_dict[error_enum]['check_implemented'] = implemented
+                self.db_dict[error_enum]['testname'] = testname
+                self.db_dict[error_enum]['api'] = api
+                self.db_dict[error_enum]['error_string'] = error_str
+                self.db_dict[error_enum]['note'] = note
+                # Now build custom data structs
+                if 'Y' == implemented:
+                    self.db_implemented_enums.append(error_enum)
+                if testname.lower() not in ['unknown', 'none']:
+                    self.db_enum_to_tests[error_enum] = testname.split(',')
+                    #if len(self.db_enum_to_tests[error_enum]) > 1:
+                    #    print "Found check %s that has multiple tests: %s" % (error_enum, self.db_enum_to_tests[error_enum])
+                    #else:
+                    #    print "Check %s has single test: %s" % (error_enum, self.db_enum_to_tests[error_enum])
+                #unique_id = int(db_line[0].split('_')[-1])
+                #if unique_id > max_id:
+                #    max_id = unique_id
+        #print "Found %d total enums in database" % (len(self.db_dict.keys()))
+        #print "Found %d enums claiming to be implemented in source" % (len(self.db_implemented_enums))
+        #print "Found %d enums claiming to have tests implemented" % (len(self.db_enum_to_tests.keys()))
+
+class ValidationHeader:
+    def __init__(self, filename=header_file):
+        self.filename = header_file
+        self.enums = []
+    def read(self):
+        """Read unique error enum header file into internal data structures"""
+        grab_enums = False
+        with open(self.filename, "r") as infile:
+            for line in infile:
+                line = line.strip()
+                if 'enum UNIQUE_VALIDATION_ERROR_CODE {' in line:
+                    grab_enums = True
+                    continue
+                if grab_enums:
+                    if 'VALIDATION_ERROR_MAX_ENUM' in line:
+                        grab_enums = False
+                        break # done
+                    if 'VALIDATION_ERROR_' in line:
+                        enum = line.split(' = ')[0]
+                        self.enums.append(enum)
+        #print "Found %d error enums. First is %s and last is %s." % (len(self.enums), self.enums[0], self.enums[-1])
+
+class ValidationSource:
+    def __init__(self, source_file_list):
+        self.source_files = source_file_list
+        self.enum_count_dict = {} # dict of enum values to the count of how much they're used
+    def parse(self):
+        duplicate_checks = 0
+        for sf in self.source_files:
+            with open(sf) as f:
+                for line in f:
+                    if True in [line.strip().startswith(comment) for comment in ['//', '/*']]:
+                        continue
+                    # Find enums
+                    #if 'VALIDATION_ERROR_' in line and True not in [ignore in line for ignore in ['[VALIDATION_ERROR_', 'UNIQUE_VALIDATION_ERROR_CODE']]:
+                    if 'VALIDATION_ERROR_' in line and 'UNIQUE_VALIDATION_ERROR_CODE' not in line:
+                        # Need to isolate the validation error enum
+                        #print("Line has check:%s" % (line))
+                        line_list = line.split()
+                        enum = ''
+                        for str in line_list:
+                            if 'VALIDATION_ERROR_' in str and '[VALIDATION_ERROR_' not in str:
+                                enum = str.strip(',);')
+                                break
+                        if enum != '':
+                            if enum not in self.enum_count_dict:
+                                self.enum_count_dict[enum] = 1
+                                #print "Found enum %s implemented for first time in file %s" % (enum, sf)
+                            else:
+                                self.enum_count_dict[enum] = self.enum_count_dict[enum] + 1
+                                #print "Found enum %s implemented for %d time in file %s" % (enum, self.enum_count_dict[enum], sf)
+                                duplicate_checks = duplicate_checks + 1
+                        #else:
+                            #print("Didn't find actual check in line:%s" % (line))
+        #print "Found %d unique implemented checks and %d are duplicated at least once" % (len(self.enum_count_dict.keys()), duplicate_checks)
+
+# Class to parse the validation layer test source and store testnames
+# TODO: Enhance class to detect use of unique error enums in the test
+class TestParser:
+    def __init__(self, test_file_list, test_group_name=['VkLayerTest', 'VkPositiveLayerTest', 'VkWsiEnabledLayerTest']):
+        self.test_files = test_file_list
+        self.tests_set = set()
+        self.test_trigger_txt_list = []
+        for tg in test_group_name:
+            self.test_trigger_txt_list.append('TEST_F(%s' % tg)
+            #print('Test trigger test list: %s' % (self.test_trigger_txt_list))
+
+    # Parse test files into internal data struct
+    def parse(self):
+        # For each test file, parse test names into set
+        grab_next_line = False # handle testname on separate line than wildcard
+        for test_file in self.test_files:
+            with open(test_file) as tf:
+                for line in tf:
+                    if True in [line.strip().startswith(comment) for comment in ['//', '/*']]:
+                        continue
+
+                    if True in [ttt in line for ttt in self.test_trigger_txt_list]:
+                        #print('Test wildcard in line: %s' % (line))
+                        testname = line.split(',')[-1]
+                        testname = testname.strip().strip(' {)')
+                        #print('Inserting test: "%s"' % (testname))
+                        if ('' == testname):
+                            grab_next_line = True
+                            continue
+                        self.tests_set.add(testname)
+                    if grab_next_line: # test name on its own line
+                        grab_next_line = False
+                        testname = testname.strip().strip(' {)')
+                        self.tests_set.add(testname)
+
+# Little helper class for coloring cmd line output
+class bcolors:
+
+    def __init__(self):
+        self.GREEN = '\033[0;32m'
+        self.RED = '\033[0;31m'
+        self.YELLOW = '\033[1;33m'
+        self.ENDC = '\033[0m'
+        if 'Linux' != platform.system():
+            self.GREEN = ''
+            self.RED = ''
+            self.YELLOW = ''
+            self.ENDC = ''
+
+    def green(self):
+        return self.GREEN
+
+    def red(self):
+        return self.RED
+
+    def yellow(self):
+        return self.YELLOW
+
+    def endc(self):
+        return self.ENDC
+
+# Class to parse the validation layer test source and store testnames
+class TestParser:
+    def __init__(self, test_file_list, test_group_name=['VkLayerTest', 'VkPositiveLayerTest', 'VkWsiEnabledLayerTest']):
+        self.test_files = test_file_list
+        self.tests_set = set()
+        self.test_trigger_txt_list = []
+        for tg in test_group_name:
+            self.test_trigger_txt_list.append('TEST_F(%s' % tg)
+            #print('Test trigger test list: %s' % (self.test_trigger_txt_list))
+
+    # Parse test files into internal data struct
+    def parse(self):
+        # For each test file, parse test names into set
+        grab_next_line = False # handle testname on separate line than wildcard
+        for test_file in self.test_files:
+            with open(test_file) as tf:
+                for line in tf:
+                    if True in [line.strip().startswith(comment) for comment in ['//', '/*']]:
+                        continue
+
+                    if True in [ttt in line for ttt in self.test_trigger_txt_list]:
+                        #print('Test wildcard in line: %s' % (line))
+                        testname = line.split(',')[-1]
+                        testname = testname.strip().strip(' {)')
+                        #print('Inserting test: "%s"' % (testname))
+                        if ('' == testname):
+                            grab_next_line = True
+                            continue
+                        self.tests_set.add(testname)
+                    if grab_next_line: # test name on its own line
+                        grab_next_line = False
+                        testname = testname.strip().strip(' {)')
+                        self.tests_set.add(testname)
+
+def main(argv=None):
+    # parse db
+    val_db = ValidationDatabase()
+    val_db.read()
+    # parse header
+    val_header = ValidationHeader()
+    val_header.read()
+    # Create parser for layer files
+    val_source = ValidationSource(layer_source_files)
+    val_source.parse()
+    # Parse test files
+    test_parser = TestParser([test_file, ])
+    test_parser.parse()
+
+    # Process stats - Just doing this inline in main, could make a fancy class to handle
+    #   all the processing of data and then get results from that
+    txt_color = bcolors()
+    print("Validation Statistics")
+    # First give number of checks in db & header and report any discrepancies
+    db_enums = len(val_db.db_dict.keys())
+    hdr_enums = len(val_header.enums)
+    print(" Database file includes %d unique checks" % (db_enums))
+    print(" Header file declares %d unique checks" % (hdr_enums))
+    tmp_db_dict = val_db.db_dict
+    db_missing = []
+    for enum in val_header.enums:
+        if not tmp_db_dict.pop(enum, False):
+            db_missing.append(enum)
+    if db_enums == hdr_enums and len(db_missing) == 0 and len(tmp_db_dict.keys()) == 0:
+        print(txt_color.green() + "  Database and Header match, GREAT!" + txt_color.endc())
+    else:
+        print(txt_color.red() + "  Uh oh, Database doesn't match Header :(" + txt_color.endc())
+        if len(db_missing) != 0:
+            print(txt_color.red() + "   The following checks are in header but missing from database:" + txt_color.endc())
+            for missing_enum in db_missing:
+                print(txt_color.red() + "    %s" % (missing_enum) + txt_color.endc())
+        if len(tmp_db_dict.keys()) != 0:
+            print(txt_color.red() + "   The following checks are in database but haven't been declared in the header:" + txt_color.endc())
+            for extra_enum in tmp_db_dict:
+                print(txt_color.red() + "    %s" % (extra_enum) + txt_color.endc())
+    # Report out claimed implemented checks vs. found actual implemented checks
+    imp_not_found = [] # Checks claimed to implemented in DB file but no source found
+    imp_not_claimed = [] # Checks found implemented but not claimed to be in DB
+    multiple_uses = False # Flag if any enums are used multiple times
+    for db_imp in val_db.db_implemented_enums:
+        if db_imp not in val_source.enum_count_dict:
+            imp_not_found.append(db_imp)
+    for src_enum in val_source.enum_count_dict:
+        if val_source.enum_count_dict[src_enum] > 1:
+            multiple_uses = True
+        if src_enum not in val_db.db_implemented_enums:
+            imp_not_claimed.append(src_enum)
+    print(" Database file claims that %d checks (%s) are implemented in source." % (len(val_db.db_implemented_enums), "{0:.0f}%".format(float(len(val_db.db_implemented_enums))/db_enums * 100)))
+    if len(imp_not_found) == 0 and len(imp_not_claimed) == 0:
+        print(txt_color.green() + "  All claimed Database implemented checks have been found in source, and no source checks aren't claimed in Database, GREAT!" + txt_color.endc())
+    else:
+        print(txt_color.red() + "  Uh oh, Database claimed implemented don't match Source :(" + txt_color.endc())
+        if len(imp_not_found) != 0:
+            print(txt_color.red() + "   The following checks are claimed to be implemented in Database, but weren't found in source:" + txt_color.endc())
+            for not_imp_enum in imp_not_found:
+                print(txt_color.red() + "    %s" % (not_imp_enum) + txt_color.endc())
+        if len(imp_not_claimed) != 0:
+            print(txt_color.red() + "   The following checks are implemented in source, but not claimed to be in Database:" + txt_color.endc())
+            for imp_enum in imp_not_claimed:
+                print(txt_color.red() + "    %s" % (imp_enum) + txt_color.endc())
+    if multiple_uses:
+        print(txt_color.yellow() + "  Note that some checks are used multiple times. These may be good candidates for new valid usage spec language." + txt_color.endc())
+        print(txt_color.yellow() + "  Here is a list of each check used multiple times with its number of uses:" + txt_color.endc())
+        for enum in val_source.enum_count_dict:
+            if val_source.enum_count_dict[enum] > 1:
+                print(txt_color.yellow() + "   %s: %d" % (enum, val_source.enum_count_dict[enum]) + txt_color.endc())
+    # Now check that tests claimed to be implemented are actual test names
+    bad_testnames = []
+    for enum in val_db.db_enum_to_tests:
+        for testname in val_db.db_enum_to_tests[enum]:
+            if testname not in test_parser.tests_set:
+                bad_testnames.append(testname)
+    print(" Database file claims that %d checks have tests written." % len(val_db.db_enum_to_tests))
+    if len(bad_testnames) == 0:
+        print(txt_color.green() + "  All claimed tests have valid names. That's good!" + txt_color.endc())
+    else:
+        print(txt_color.red() + "  The following testnames in Database appear to be invalid:")
+        for bt in bad_testnames:
+            print(txt_color.red() + "   %s" % (bt))
+
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main())
+