1#! /usr/bin/python3 2# 3# Copyright (c) 2022 Google LLC 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import glob 18import os 19import subprocess 20import sys 21 22# A handful of relevant tests are hand-picked to generate extra unit tests with 23# specific options of spirv-diff. 24IGNORE_SET_BINDING_TESTS = ['different_decorations_vertex'] 25IGNORE_LOCATION_TESTS = ['different_decorations_fragment'] 26IGNORE_DECORATIONS_TESTS = ['different_decorations_vertex', 'different_decorations_fragment'] 27DUMP_IDS_TESTS = ['basic', 'int_vs_uint_constants', 'multiple_same_entry_points', 'small_functions_small_diffs'] 28 29LICENSE = u"""Copyright (c) 2022 Google LLC. 30 31Licensed under the Apache License, Version 2.0 (the "License"); 32you may not use this file except in compliance with the License. 33You may obtain a copy of the License at 34 35 http://www.apache.org/licenses/LICENSE-2.0 36 37Unless required by applicable law or agreed to in writing, software 38distributed under the License is distributed on an "AS IS" BASIS, 39WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 40See the License for the specific language governing permissions and 41limitations under the License. 42""" 43 44TEMPLATE_TEST_FILE = u"""// GENERATED FILE - DO NOT EDIT. 45// Generated by {script_name} 46// 47{license} 48 49#include "../diff_test_utils.h" 50 51#include "gtest/gtest.h" 52 53namespace spvtools {{ 54namespace diff {{ 55namespace {{ 56 57{test_comment} 58constexpr char kSrc[] = R"({src_spirv})"; 59constexpr char kDst[] = R"({dst_spirv})"; 60 61TEST(DiffTest, {test_name}) {{ 62 constexpr char kDiff[] = R"({diff_spirv})"; 63 Options options; 64 DoStringDiffTest(kSrc, kDst, kDiff, options); 65}} 66 67TEST(DiffTest, {test_name}NoDebug) {{ 68 constexpr char kSrcNoDebug[] = R"({src_spirv_no_debug})"; 69 constexpr char kDstNoDebug[] = R"({dst_spirv_no_debug})"; 70 constexpr char kDiff[] = R"({diff_spirv_no_debug})"; 71 Options options; 72 DoStringDiffTest(kSrcNoDebug, kDstNoDebug, kDiff, options); 73}} 74{extra_tests} 75}} // namespace 76}} // namespace diff 77}} // namespace spvtools 78""" 79 80TEMPLATE_TEST_FUNC = u""" 81TEST(DiffTest, {test_name}{test_tag}) {{ 82 constexpr char kDiff[] = R"({diff_spirv})"; 83 Options options; 84 {test_options} 85 DoStringDiffTest(kSrc, kDst, kDiff, options); 86}} 87""" 88 89TEMPLATE_TEST_FILES_CMAKE = u"""# GENERATED FILE - DO NOT EDIT. 90# Generated by {script_name} 91# 92{license} 93 94list(APPEND DIFF_TEST_FILES 95{test_files} 96) 97""" 98 99VARIANT_NONE = 0 100VARIANT_IGNORE_SET_BINDING = 1 101VARIANT_IGNORE_LOCATION = 2 102VARIANT_IGNORE_DECORATIONS = 3 103VARIANT_DUMP_IDS = 4 104 105def print_usage(): 106 print("Usage: {} <path-to-spirv-diff>".format(sys.argv[0])) 107 108def remove_debug_info(in_path): 109 tmp_dir = '.no_dbg' 110 111 if not os.path.exists(tmp_dir): 112 os.makedirs(tmp_dir) 113 114 (in_basename, in_ext) = os.path.splitext(in_path) 115 out_name = in_basename + '_no_dbg' + in_ext 116 out_path = os.path.join(tmp_dir, out_name) 117 118 with open(in_path, 'r') as fin: 119 with open(out_path, 'w') as fout: 120 for line in fin: 121 ops = line.strip().split() 122 op = ops[0] if len(ops) > 0 else '' 123 if (op != ';;' and op != 'OpName' and op != 'OpMemberName' and op != 'OpString' and 124 op != 'OpLine' and op != 'OpNoLine' and op != 'OpModuleProcessed'): 125 fout.write(line) 126 127 return out_path 128 129def make_src_file(test_name): 130 return '{}_src.spvasm'.format(test_name) 131 132def make_dst_file(test_name): 133 return '{}_dst.spvasm'.format(test_name) 134 135def make_cpp_file(test_name): 136 return '{}_autogen.cpp'.format(test_name) 137 138def make_camel_case(test_name): 139 return test_name.replace('_', ' ').title().replace(' ', '') 140 141def make_comment(text, comment_prefix): 142 return '\n'.join([comment_prefix + (' ' if line.strip() else '') + line for line in text.splitlines()]) 143 144def read_file(file_name): 145 with open(file_name, 'r') as f: 146 content = f.read() 147 148 # Use unix line endings. 149 content = content.replace('\r\n', '\n') 150 151 return content 152 153def parse_test_comment(src_spirv_file_name, src_spirv): 154 src_spirv_lines = src_spirv.splitlines() 155 comment_line_count = 0 156 while comment_line_count < len(src_spirv_lines): 157 if not src_spirv_lines[comment_line_count].strip().startswith(';;'): 158 break 159 comment_line_count += 1 160 161 if comment_line_count == 0: 162 print("Expected comment on test file '{}'. See README.md next to this file.".format(src_spirv_file_name)) 163 sys.exit(1) 164 165 comment_block = src_spirv_lines[:comment_line_count] 166 spirv_block = src_spirv_lines[comment_line_count:] 167 168 comment_block = ['// ' + line.replace(';;', '').strip() for line in comment_block] 169 170 return '\n'.join(spirv_block), '\n'.join(comment_block) 171 172def run_diff_tool(diff_tool, src_file, dst_file, variant): 173 args = [diff_tool] 174 175 if variant == VARIANT_IGNORE_SET_BINDING or variant == VARIANT_IGNORE_DECORATIONS: 176 args.append('--ignore-set-binding') 177 178 if variant == VARIANT_IGNORE_LOCATION or variant == VARIANT_IGNORE_DECORATIONS: 179 args.append('--ignore-location') 180 181 if variant == VARIANT_DUMP_IDS: 182 args.append('--with-id-map') 183 184 args.append('--no-color') 185 args.append('--no-indent') 186 187 args.append(src_file) 188 args.append(dst_file) 189 190 success = True 191 print(' '.join(args)) 192 process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) 193 out, err = process.communicate() 194 195 if process.returncode != 0: 196 print(err) 197 sys.exit(process.returncode) 198 199 # Use unix line endings. 200 out = out.replace('\r\n', '\n') 201 202 return out 203 204def generate_extra_test(diff_tool, src_file, dst_file, variant, test_name_camel_case, test_tag, test_options): 205 diff = run_diff_tool(diff_tool, src_file, dst_file, variant) 206 return TEMPLATE_TEST_FUNC.format( 207 test_name = test_name_camel_case, 208 test_tag = test_tag, 209 test_options = test_options, 210 diff_spirv = diff) 211 212def generate_test(diff_tool, test_name): 213 src_file = make_src_file(test_name) 214 dst_file = make_dst_file(test_name) 215 src_file_no_debug = remove_debug_info(src_file) 216 dst_file_no_debug = remove_debug_info(dst_file) 217 218 src_spirv = read_file(src_file) 219 dst_spirv = read_file(dst_file) 220 src_spirv_no_debug = read_file(src_file_no_debug) 221 dst_spirv_no_debug = read_file(dst_file_no_debug) 222 223 test_name_camel_case = make_camel_case(test_name) 224 225 diff_spirv = run_diff_tool(diff_tool, src_file, dst_file, VARIANT_NONE) 226 diff_spirv_no_debug = run_diff_tool(diff_tool, src_file_no_debug, dst_file_no_debug, VARIANT_NONE) 227 228 extra_tests = [] 229 230 if test_name in IGNORE_SET_BINDING_TESTS: 231 extra_tests.append(generate_extra_test(diff_tool, src_file, dst_file, VARIANT_IGNORE_SET_BINDING, 232 test_name_camel_case, 'IgnoreSetBinding', 'options.ignore_set_binding = true;')) 233 234 if test_name in IGNORE_LOCATION_TESTS: 235 extra_tests.append(generate_extra_test(diff_tool, src_file, dst_file, VARIANT_IGNORE_LOCATION, 236 test_name_camel_case, 'IgnoreLocation', 'options.ignore_location = true;')) 237 238 if test_name in IGNORE_DECORATIONS_TESTS: 239 extra_tests.append(generate_extra_test(diff_tool, src_file, dst_file, VARIANT_IGNORE_DECORATIONS, 240 test_name_camel_case, 'IgnoreSetBindingLocation', 241 '\n '.join(['options.ignore_set_binding = true;', 'options.ignore_location = true;']))) 242 243 if test_name in DUMP_IDS_TESTS: 244 extra_tests.append(generate_extra_test(diff_tool, src_file, dst_file, VARIANT_DUMP_IDS, 245 test_name_camel_case, 'DumpIds', 'options.dump_id_map = true;')) 246 247 src_spirv, test_comment = parse_test_comment(src_file, src_spirv) 248 249 test_file = TEMPLATE_TEST_FILE.format( 250 script_name = os.path.basename(__file__), 251 license = make_comment(LICENSE, '//'), 252 test_comment = test_comment, 253 test_name = test_name_camel_case, 254 src_spirv = src_spirv, 255 dst_spirv = dst_spirv, 256 diff_spirv = diff_spirv, 257 src_spirv_no_debug = src_spirv_no_debug, 258 dst_spirv_no_debug = dst_spirv_no_debug, 259 diff_spirv_no_debug = diff_spirv_no_debug, 260 extra_tests = ''.join(extra_tests)) 261 262 test_file_name = make_cpp_file(test_name) 263 with open(test_file_name, 'wb') as fout: 264 fout.write(str.encode(test_file)) 265 266 return test_file_name 267 268def generate_tests(diff_tool, test_names): 269 return [generate_test(diff_tool, test_name) for test_name in test_names] 270 271def generate_cmake(test_files): 272 cmake = TEMPLATE_TEST_FILES_CMAKE.format( 273 script_name = os.path.basename(__file__), 274 license = make_comment(LICENSE, '#'), 275 test_files = '\n'.join(['"diff_files/{}"'.format(f) for f in test_files])) 276 277 with open('diff_test_files_autogen.cmake', 'wb') as fout: 278 fout.write(str.encode(cmake)) 279 280def main(): 281 282 if len(sys.argv) != 2: 283 print_usage() 284 return 1 285 286 diff_tool = sys.argv[1] 287 if not os.path.exists(diff_tool): 288 print("No such file: {}".format(diff_tool)) 289 print_usage() 290 return 1 291 292 diff_tool = os.path.realpath(diff_tool) 293 os.chdir(os.path.dirname(__file__)) 294 295 test_names = sorted([f[:-11] for f in glob.glob("*_src.spvasm")]) 296 297 test_files = generate_tests(diff_tool, test_names) 298 299 generate_cmake(test_files) 300 301 return 0 302 303if __name__ == '__main__': 304 sys.exit(main()) 305