File indexing completed on 2024-04-14 05:32:14

0001 #!/usr/bin/env python3
0002 
0003 import sys
0004 import os
0005 import subprocess
0006 import re
0007 import json
0008 import threading
0009 import multiprocessing
0010 import argparse
0011 import io
0012 import shutil
0013 from threading import Thread
0014 from sys import platform as _platform
0015 import platform
0016 
0017 # cd into the folder containing this script
0018 os.chdir(os.path.realpath(os.path.dirname(sys.argv[0])))
0019 
0020 _verbose = False
0021 _hasStdFileSystem = True
0022 
0023 c_headerpath = False
0024 try:
0025     result = subprocess.run(['clang', '-v'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True)
0026     match = re.search(r'Selected .* installation: (.*)', result.stderr)
0027     if match:
0028         c_headerpath = match.group(1).strip()
0029 except:
0030     pass
0031 
0032 
0033 def isWindows():
0034     return _platform == 'win32'
0035 
0036 def isMacOS():
0037     return _platform == 'darwin'
0038 
0039 def isLinux():
0040     return _platform.startswith('linux')
0041 
0042 class QtInstallation:
0043     def __init__(self):
0044         self.int_version = 000
0045         self.qmake_header_path = "/usr/include/qt/"
0046         self.qmake_lib_path = "/usr/lib"
0047 
0048     def compiler_flags(self, module_includes = False):
0049         extra_includes = ''
0050         if isMacOS():
0051             extra_includes = " -I%s/QtCore.framework/Headers" % self.qmake_lib_path
0052             extra_includes += " -iframework %s" % self.qmake_lib_path
0053 
0054         # Also include the modules folders
0055         qt_modules_includes = []
0056         if module_includes:
0057             qt_modules_includes = ["-isystem " + self.qmake_header_path + "/" + f for f in next(os.walk(self.qmake_header_path))[1]];
0058         c_header_option = ""
0059         if c_headerpath:
0060             c_header_option = "-isystem " + c_headerpath + "/include "
0061 
0062         return c_header_option + "-isystem " + self.qmake_header_path + ("" if isWindows() else " -fPIC") + " -L " + self.qmake_lib_path + ' ' + extra_includes + ' '.join(qt_modules_includes)
0063 
0064 
0065 class Test:
0066     def __init__(self, check):
0067         self.filenames = []
0068         self.minimum_qt_version = 500
0069         self.maximum_qt_version = 69999
0070         self.minimum_clang_version = 380
0071         self.minimum_clang_version_for_fixits = 380
0072         self.compare_everything = False
0073         self.link = False  # If true we also call the linker
0074         self.check = check
0075         self.expects_failure = False
0076         self.qt_major_versions = [5, 6]
0077         self.env = os.environ
0078         self.checks = []
0079         self.flags = ""
0080         self.must_fail = False
0081         self.blacklist_platforms = []
0082         self.qt4compat = False
0083         self.only_qt = False
0084         self.qt_developer = False
0085         self.header_filter = ""
0086         self.ignore_dirs = ""
0087         self.has_fixits = False
0088         self.should_run_fixits_test = False
0089         self.should_run_on_32bit = True
0090         self.cppStandards = ["c++14", "c++17"]
0091         self.requires_std_filesystem = False
0092         self.extra_definitions = False
0093         self.qt_modules_includes = False
0094         self.fixed_file_base = None
0095 
0096     def filename(self):
0097         if len(self.filenames) == 1:
0098             return self.filenames[0]
0099         return ""
0100 
0101     def relativeFilename(self):
0102         # example: "auto-unexpected-qstringbuilder/main.cpp"
0103         return self.check.name + "/" + self.filename()
0104 
0105     def yamlFilename(self, is_standalone):
0106         # The name of the yaml file with fixits
0107         # example: "auto-unexpected-qstringbuilder/main.cpp.clazy.yaml"
0108         if is_standalone:
0109             return self.relativeFilename() + ".clazy-standalone.yaml"
0110         else:
0111             return self.relativeFilename() + ".clazy.yaml"
0112 
0113     def fixedFilename(self, is_standalone):
0114         if is_standalone:
0115             return self.relativeFilename() + ".clazy-standalone.fixed"
0116         else:
0117             return self.relativeFilename() + ".clazy.fixed"
0118 
0119     def expectedFixedFilename(self):
0120         return self.relativeFilename() + ".fixed.expected"
0121 
0122     def isScript(self):
0123         return self.filename().endswith(".sh")
0124 
0125     def dir(self):
0126         return self.check.name
0127 
0128     def setQtMajorVersions(self, major_versions):
0129         self.qt_major_versions = major_versions
0130         if 4 in major_versions:
0131             if self.minimum_qt_version >= 500:
0132                 self.minimum_qt_version = 400
0133 
0134     def envString(self):
0135         result = ""
0136         for key in self.env:
0137             result += key + '="' + self.env[key] + '" '
0138         return result
0139 
0140     def setEnv(self, e):
0141         self.env = os.environ.copy()
0142         for key in e:
0143             if type(key) is bytes:
0144                 key = key.decode('utf-8')
0145 
0146             self.env[key] = e[key]
0147 
0148     def printableName(self, cppStandard, qt_major_version, is_standalone, is_fixits):
0149         name = self.check.name
0150         if len(self.check.tests) > 1:
0151             name += "/" + self.filename()
0152         if len(cppStandard) > 0:
0153             name += " (" + cppStandard + ")"
0154         if qt_major_version > 0:
0155             name += " (Qt " + str(qt_major_version) + ")"
0156         if is_fixits and is_standalone:
0157             name += " (standalone, fixits)"
0158         elif is_standalone:
0159             name += " (standalone)"
0160         elif is_fixits:
0161             name += " (plugin, fixits)"
0162         else:
0163             name += " (plugin)"
0164         return name
0165 
0166     def removeYamlFiles(self):
0167         for f in [self.yamlFilename(False), self.yamlFilename(True)]:
0168             if os.path.exists(f):
0169                 os.remove(f)
0170 
0171 
0172 class Check:
0173     def __init__(self, name):
0174         self.name = name
0175         self.minimum_clang_version = 380  # clang 3.8.0
0176         self.minimum_qt_version = 500
0177         self.maximum_qt_version = 69999
0178         self.enabled = True
0179         self.clazy_standalone_only = False
0180         self.tests = []
0181 # -------------------------------------------------------------------------------
0182 # utility functions #1
0183 
0184 
0185 def get_command_output(cmd, test_env=os.environ, cwd=None, ignore_verbose=False):
0186     success = True
0187 
0188     try:
0189         if _verbose and not ignore_verbose:
0190             print(cmd)
0191 
0192         # Polish up the env to fix "TypeError: environment can only contain strings" exception
0193         str_env = {}
0194         for key in test_env.keys():
0195             str_env[str(key)] = str(test_env[key])
0196 
0197         output = subprocess.check_output(
0198             cmd, stderr=subprocess.STDOUT, shell=True, env=str_env, cwd=cwd)
0199     except subprocess.CalledProcessError as e:
0200         output = e.output
0201         success = False
0202 
0203     if type(output) is bytes:
0204         output = output.decode('utf-8')
0205 
0206     return output, success
0207 
0208 
0209 def load_json(check_name):
0210     check = Check(check_name)
0211     filename = check_name + "/config.json"
0212     if not os.path.exists(filename):
0213         # Ignore this directory
0214         return check
0215 
0216     f = open(filename, 'r')
0217     contents = f.read()
0218     f.close()
0219     decoded = json.loads(contents)
0220     check_blacklist_platforms = []
0221 
0222     if 'minimum_clang_version' in decoded:
0223         check.minimum_clang_version = decoded['minimum_clang_version']
0224 
0225     if 'minimum_qt_version' in decoded:
0226         check.minimum_qt_version = decoded['minimum_qt_version']
0227 
0228     if 'maximum_qt_version' in decoded:
0229         check.maximum_qt_version = decoded['maximum_qt_version']
0230 
0231     if 'enabled' in decoded:
0232         check.enabled = decoded['enabled']
0233 
0234     if 'clazy_standalone_only' in decoded:
0235         check.clazy_standalone_only = decoded['clazy_standalone_only']
0236 
0237     if 'blacklist_platforms' in decoded:
0238         check_blacklist_platforms = decoded['blacklist_platforms']
0239 
0240     if 'tests' in decoded:
0241         for t in decoded['tests']:
0242             test = Test(check)
0243             test.blacklist_platforms = check_blacklist_platforms
0244 
0245             if 'filename' in t:
0246                 test.filenames.append(t['filename'])
0247 
0248             if 'filenames' in t:
0249                 test.filenames += t['filenames']
0250 
0251             if 'minimum_qt_version' in t:
0252                 test.minimum_qt_version = t['minimum_qt_version']
0253             else:
0254                 test.minimum_qt_version = check.minimum_qt_version
0255 
0256             if 'maximum_qt_version' in t:
0257                 test.maximum_qt_version = t['maximum_qt_version']
0258             else:
0259                 test.maximum_qt_version = check.maximum_qt_version
0260 
0261             if 'minimum_clang_version' in t:
0262                 test.minimum_clang_version = t['minimum_clang_version']
0263             else:
0264                 test.minimum_clang_version = check.minimum_clang_version
0265 
0266             if 'minimum_clang_version_for_fixits' in t:
0267                 test.minimum_clang_version_for_fixits = t['minimum_clang_version_for_fixits']
0268 
0269             if 'blacklist_platforms' in t:
0270                 test.blacklist_platforms = t['blacklist_platforms']
0271             if 'compare_everything' in t:
0272                 test.compare_everything = t['compare_everything']
0273             if 'link' in t:
0274                 test.link = t['link']
0275             if 'qt_major_versions' in t:
0276                 test.setQtMajorVersions(t['qt_major_versions'])
0277             if 'env' in t:
0278                 test.setEnv(t['env'])
0279             if 'checks' in t:
0280                 test.checks = t['checks']
0281             if 'flags' in t:
0282                 test.flags = t['flags']
0283             if 'must_fail' in t:
0284                 test.must_fail = t['must_fail']
0285             if 'has_fixits' in t:
0286                 test.has_fixits = t['has_fixits'] and test.minimum_clang_version_for_fixits <= CLANG_VERSION
0287             if 'expects_failure' in t:
0288                 test.expects_failure = t['expects_failure']
0289             if 'qt4compat' in t:
0290                 test.qt4compat = t['qt4compat']
0291             if 'only_qt' in t:
0292                 test.only_qt = t['only_qt']
0293             if 'cppStandards' in t:
0294                 test.cppStandards = t['cppStandards']
0295             if 'qt_developer' in t:
0296                 test.qt_developer = t['qt_developer']
0297             if 'extra_definitions' in t:
0298                 test.extra_definitions = " " + t['extra_definitions'] + " "
0299             if 'header_filter' in t:
0300                 test.header_filter = t['header_filter']
0301             if 'ignore_dirs' in t:
0302                 test.ignore_dirs = t['ignore_dirs']
0303             if 'should_run_on_32bit' in t:
0304                 test.should_run_on_32bit = t['should_run_on_32bit']
0305             if 'requires_std_filesystem' in t:
0306                 test.requires_std_filesystem = t['requires_std_filesystem']
0307             if 'qt_modules_includes' in t:
0308                 test.qt_modules_includes = t['qt_modules_includes']
0309 
0310             if not test.checks:
0311                 test.checks.append(test.check.name)
0312 
0313             check.tests.append(test)
0314 
0315     return check
0316 
0317 
0318 def find_qt_installation(major_version, qmakes):
0319     installation = QtInstallation()
0320 
0321     for qmake in qmakes:
0322         qmake_version_str, success = get_command_output(qmake + " -query QT_VERSION")
0323         if success and qmake_version_str.startswith(str(major_version) + "."):
0324             qmake_header_path = get_command_output(
0325                 qmake + " -query QT_INSTALL_HEADERS")[0].strip()
0326             qmake_lib_path = get_command_output(
0327                 qmake + " -query QT_INSTALL_LIBS")[0].strip()
0328             if qmake_header_path:
0329                 installation.qmake_header_path = qmake_header_path
0330                 if qmake_lib_path:
0331                     installation.qmake_lib_path = qmake_lib_path
0332                 ver = qmake_version_str.split('.')
0333                 installation.int_version = int(
0334                     ver[0]) * 10000 + int(ver[1]) * 100 + int(ver[2])
0335                 if _verbose:
0336                     print("Found Qt " + str(installation.int_version) +
0337                           " using qmake " + qmake)
0338             break
0339 
0340     if installation.int_version == 0 and major_version >= 5:  # Don't warn for missing Qt4 headers
0341         print("Error: Couldn't find a Qt" +
0342               str(major_version) + " installation")
0343     return installation
0344 
0345 
0346 def libraryName():
0347     if 'CLAZYPLUGIN_CXX' in os.environ: # Running tests uninstalled
0348         return os.environ['CLAZYPLUGIN_CXX']
0349     elif _platform == 'win32':
0350         return 'ClazyPlugin.dll'
0351     elif _platform == 'darwin':
0352         return 'ClazyPlugin.dylib'
0353     else:
0354         return 'ClazyPlugin.so'
0355 
0356 
0357 def link_flags(qt:QtInstallation):
0358     is_qt6 = qt.int_version > 60000
0359     major_version = "6" if is_qt6 else "5"
0360     flags = f"-lQt{major_version}Core -lQt{major_version}Gui -lQt{major_version}Widgets"
0361     if _platform.startswith('linux'):
0362         flags += " -lstdc++"
0363         if is_qt6:
0364             flags += " -lQt6StateMachine"
0365     return flags
0366 
0367 
0368 def clazy_cpp_args(cppStandard):
0369     return ' -Wno-unused-value -Qunused-arguments -std=' + cppStandard + ' '
0370 
0371 def clazy_standalone_binary():
0372     if 'CLAZYSTANDALONE_CXX' in os.environ:  # in case we want to use "clazy.AppImage --standalone" instead
0373         return os.environ['CLAZYSTANDALONE_CXX']
0374     return 'clazy-standalone'
0375 
0376 def more_clazy_standalone_args():
0377     if 'CLANG_BUILTIN_INCLUDE_DIR' in os.environ:
0378         return ' -I ' + os.environ['CLANG_BUILTIN_INCLUDE_DIR']
0379     return ''
0380 
0381 def clazy_standalone_command(test, cppStandard, qt):
0382     result = " -- " + clazy_cpp_args(cppStandard) + \
0383         qt.compiler_flags(test.qt_modules_includes) + " " + test.flags + more_clazy_standalone_args()
0384     result = " -checks=" + ','.join(test.checks) + " " + result + suppress_line_numbers_opt
0385 
0386     if test.has_fixits:
0387         result = " -export-fixes=" + \
0388             test.yamlFilename(is_standalone=True) + result
0389 
0390     if test.qt4compat:
0391         result = " -qt4-compat " + result
0392 
0393     if test.only_qt:
0394         result = " -only-qt " + result
0395 
0396     if test.qt_developer:
0397         result = " -qt-developer " + result
0398 
0399     if test.header_filter:
0400         result = " -header-filter " + test.header_filter + " " + result
0401 
0402     if test.ignore_dirs:
0403         result = " -ignore-dirs " + test.ignore_dirs + " " + result
0404 
0405     if test.extra_definitions:
0406         result += test.extra_definitions
0407 
0408     return result
0409 
0410 def clang_name():
0411     return os.getenv('CLANGXX', 'clang')
0412 
0413 def clazy_command(test, cppStandard, qt, filename):
0414     if test.isScript():
0415         return "./" + filename
0416 
0417     if 'CLAZY_CXX' in os.environ:  # In case we want to use clazy.bat
0418         result = os.environ['CLAZY_CXX']
0419     else:
0420         result = clang_name() + " -Xclang -load -Xclang " + libraryName() + " -Xclang -add-plugin -Xclang clazy " 
0421     result += clazy_cpp_args(cppStandard) + qt.compiler_flags(test.qt_modules_includes) + suppress_line_numbers_opt 
0422 
0423     if test.qt4compat:
0424         result = result + " -Xclang -plugin-arg-clazy -Xclang qt4-compat "
0425 
0426     if test.only_qt:
0427         result = result + " -Xclang -plugin-arg-clazy -Xclang only-qt "
0428     if test.qt_developer:
0429         result = result + " -Xclang -plugin-arg-clazy -Xclang qt-developer "
0430     if test.extra_definitions:
0431         result += test.extra_definitions
0432 
0433     # Linking on one platform is enough. Won't waste time on macOS and Windows.
0434     if test.link and _platform.startswith('linux'):
0435         result = result + " " + link_flags(qt)
0436     else:
0437         result = result + " -c "
0438 
0439     result = result + test.flags + \
0440         " -Xclang -plugin-arg-clazy -Xclang " + ','.join(test.checks) + " "
0441     if test.has_fixits:
0442         result += _export_fixes_argument + " "
0443     result += filename
0444 
0445     return result
0446 
0447 
0448 def dump_ast_command(test, cppStandard, qt_major_version):
0449     return "clang -std=" + cppStandard + " -fsyntax-only -Xclang -ast-dump -fno-color-diagnostics -c " + qt_installation(qt_major_version).compiler_flags(test.qt_modules_includes) + " " + test.flags + " " + test.filename()
0450 
0451 def compiler_name():
0452     if 'CLAZY_CXX' in os.environ:
0453         return os.environ['CLAZY_CXX']  # so we can set clazy.bat instead
0454     return os.getenv('CLANGXX', 'clang')
0455 
0456 # -------------------------------------------------------------------------------
0457 # Setup argparse
0458 
0459 
0460 parser = argparse.ArgumentParser()
0461 parser.add_argument("-v", "--verbose", action='store_true')
0462 parser.add_argument("--no-standalone", action='store_true',
0463                     help="Don\'t run clazy-standalone")
0464 parser.add_argument("--no-fixits", action='store_true',
0465                     help='Don\'t run fixits')
0466 parser.add_argument("--only-standalone", action='store_true',
0467                     help='Only run clazy-standalone')
0468 parser.add_argument("--dump-ast", action='store_true',
0469                     help='Dump a unit-test AST to file')
0470 parser.add_argument(
0471     "--exclude", help='Comma separated list of checks to ignore')
0472 parser.add_argument("-j", "--jobs", type=int, default=multiprocessing.cpu_count(),
0473                     help='Parallel jobs to run (defaults to %(default)s)')
0474 parser.add_argument("check_names", nargs='*',
0475                     help="The name of the check whose unit-tests will be run. Defaults to running all checks.")
0476 args = parser.parse_args()
0477 
0478 if args.only_standalone and args.no_standalone:
0479     print("Error: --only-standalone is incompatible with --no-standalone")
0480     sys.exit(1)
0481 
0482 # -------------------------------------------------------------------------------
0483 # Global variables
0484 
0485 _export_fixes_argument = "-Xclang -plugin-arg-clazy -Xclang export-fixes"
0486 _dump_ast = args.dump_ast
0487 _verbose = args.verbose
0488 _no_standalone = args.no_standalone
0489 _no_fixits = args.no_fixits
0490 _only_standalone = args.only_standalone
0491 _num_threads = args.jobs
0492 _lock = threading.Lock()
0493 _was_successful = True
0494 _qt6_installation = find_qt_installation(
0495     6, ["QT_SELECT=6 qmake", "qmake-qt6", "qmake", "qmake6"])
0496 _qt5_installation = find_qt_installation(
0497     5, ["QT_SELECT=5 qmake", "qmake-qt5", "qmake", "qmake5"])
0498 _qt4_installation = find_qt_installation(
0499     4, ["QT_SELECT=4 qmake", "qmake-qt4", "qmake"])
0500 _excluded_checks = args.exclude.split(',') if args.exclude is not None else []
0501 
0502 # -------------------------------------------------------------------------------
0503 # utility functions #2
0504 
0505 version, success = get_command_output(compiler_name() + ' --version')
0506 match = re.search('clang version ([^\s-]+)', version)
0507 try:
0508     version = match.group(1)
0509 except:
0510     # Now try the Clazy.AppImage way
0511     match = re.search('clang version: (.*)', version)
0512 
0513     try:
0514         version = match.group(1)
0515     except:
0516         splitted = version.split()
0517         if len(splitted) > 2:
0518             version = splitted[2]
0519         else:
0520             print("Could not determine clang version, is it in PATH?")
0521             sys.exit(-1)
0522 
0523 if _verbose:
0524     print('Found clang version: ' + str(version))
0525 
0526 CLANG_VERSION = int(version.replace('.', ''))
0527 
0528 suppress_line_numbers_opt = ""
0529 if CLANG_VERSION >= 1700: # See https://releases.llvm.org/17.0.1/tools/clang/docs/ReleaseNotes.html
0530     suppress_line_numbers_opt = " -fno-diagnostics-show-line-numbers"
0531 
0532 def qt_installation(major_version):
0533     if major_version == 6:
0534         return _qt6_installation
0535     elif major_version == 5:
0536         return _qt5_installation
0537     elif major_version == 4:
0538         return _qt4_installation
0539 
0540     return None
0541 
0542 
0543 def run_command(cmd, output_file="", test_env=os.environ, cwd=None, ignore_verbose_command=False):
0544     lines, success = get_command_output(cmd, test_env, cwd=cwd, ignore_verbose=ignore_verbose_command)
0545     # Hack for Windows, we have std::_Vector_base in the expected data
0546     lines = lines.replace("std::_Container_base0", "std::_Vector_base")
0547     lines = lines.replace("std::__1::__vector_base_common",
0548                           "std::_Vector_base")  # Hack for macOS
0549     lines = lines.replace("std::_Vector_alloc", "std::_Vector_base")
0550     if not success and not output_file:
0551         print(lines)
0552         return False
0553 
0554     if _verbose:
0555         print("Running: " + cmd)
0556         print("output_file=" + output_file)
0557 
0558     lines = lines.replace('\r\n', '\n')
0559     if len(lines) > 0 and lines[-1] == "\n":
0560         lines = lines[:-1] # remove trailing empty line, often it's the only output
0561     if output_file:
0562         f = io.open(output_file, 'w', encoding='utf8')
0563         f.writelines(lines)
0564         f.close()
0565     elif len(lines) > 0:
0566         print(lines)
0567 
0568     return success
0569 
0570 
0571 def files_are_equal(file1, file2):
0572     try:
0573         f = io.open(file1, 'r', encoding='utf-8')
0574         lines1 = f.readlines()
0575         f.close()
0576 
0577         f = io.open(file2, 'r', encoding='utf-8')
0578         lines2 = f.readlines()
0579         f.close()
0580 
0581         return lines1 == lines2
0582     except Exception as ex:
0583         print("Error comparing files:" + str(ex))
0584         return False
0585 
0586 
0587 def compare_files(expects_failure, expected_file, result_file, message):
0588     success = files_are_equal(expected_file, result_file)
0589 
0590     if expects_failure:
0591         if success:
0592             print("[XOK]   " + message)
0593             return False
0594         else:
0595             print("[XFAIL] " + message)
0596             print_differences(expected_file, result_file)
0597             return True
0598     else:
0599         if success:
0600             print("[OK]   " + message)
0601             return True
0602         else:
0603             print("[FAIL] " + message)
0604             print_differences(expected_file, result_file)
0605             return False
0606 
0607 
0608 def get_check_names():
0609     return list(filter(lambda entry: os.path.isdir(entry), os.listdir(".")))
0610 
0611 # The yaml file references the test file in our git repo, but we don't want
0612 # to rewrite that one, as we would need to discard git changes afterwards,
0613 # so patch the yaml file and add a ".fixed" suffix to those files
0614 
0615 
0616 def patch_fixit_yaml_file(test, is_standalone):
0617 
0618     yamlfilename = test.yamlFilename(is_standalone)
0619     fixedfilename = test.fixedFilename(is_standalone)
0620 
0621     f = open(yamlfilename, 'r')
0622     lines = f.readlines()
0623     f.close()
0624     f = open(yamlfilename, 'w')
0625 
0626     possible_headerfile = test.relativeFilename().replace(".cpp", ".h")
0627 
0628     for line in lines:
0629         stripped = line.strip()
0630         if stripped.startswith('MainSourceFile') or stripped.startswith("FilePath") or stripped.startswith("- FilePath"):
0631             line = line.replace(test.relativeFilename(), fixedfilename)
0632 
0633             # For Windows:
0634             line = line.replace(test.relativeFilename().replace(
0635                 '/', '\\'), fixedfilename.replace('/', '\\'))
0636 
0637             # Some tests also apply fix their to their headers:
0638             if not test.relativeFilename().endswith(".hh"):
0639                 line = line.replace(possible_headerfile,
0640                                     fixedfilename.replace(".cpp", ".h"))
0641         f.write(line)
0642     f.close()
0643 
0644     shutil.copyfile(test.relativeFilename(), fixedfilename)
0645 
0646     if os.path.exists(possible_headerfile):
0647         shutil.copyfile(possible_headerfile,
0648                         fixedfilename.replace(".cpp", ".h"))
0649 
0650     return True
0651 
0652 
0653 def run_clang_apply_replacements(check):
0654     command = os.getenv('CLAZY_CLANG_APPLY_REPLACEMENTS',
0655                         'clang-apply-replacements')
0656     return run_command(command + ' ' + check.name)
0657 
0658 def cleanup_fixit_files(checks):
0659     for check in checks:
0660         filestodelete = list(filter(lambda entry: entry.endswith(
0661             '.fixed') or entry.endswith('.yaml'), os.listdir(check.name)))
0662         for f in filestodelete:
0663             os.remove(check.name + '/' + f)
0664 
0665 def print_differences(file1, file2):
0666     # Returns true if the the files are equal
0667     return run_command("diff -Naur --strip-trailing-cr {} {}".format(file1, file2))
0668 
0669 
0670 def normalizedCwd():
0671     if _platform.startswith('linux'):
0672         return subprocess.check_output("pwd -L", shell=True, universal_newlines=True).rstrip('\n')
0673     else:
0674         return os.getcwd().replace('\\', '/')
0675 
0676 
0677 def extract_word(word, in_file, out_file):
0678     in_f = io.open(in_file, 'r', encoding='utf-8')
0679     out_f = io.open(out_file, 'w', encoding='utf-8')
0680     for line in in_f:
0681         if '[-Wdeprecated-declarations]' in line:
0682             continue
0683 
0684         if word in line:
0685             line = line.replace('\\', '/')
0686             # clazy-standalone prints the complete cpp file path for some reason. Normalize it so it compares OK with the expected output.
0687             line = line.replace(f"{normalizedCwd()}/", "")
0688             out_f.write(line)
0689     in_f.close()
0690     out_f.close()
0691 
0692 
0693 def print_file(filename):
0694     f = open(filename, 'r')
0695     print(f.read())
0696     f.close()
0697 
0698 
0699 def file_contains(filename, text):
0700     f = io.open(filename, 'r', encoding='utf-8')
0701     contents = f.read()
0702     f.close()
0703     return text in contents
0704 
0705 
0706 def is32Bit():
0707     return platform.architecture()[0] == '32bit'
0708 
0709 
0710 def run_unit_test(test, is_standalone, cppStandard, qt_major_version):
0711     if test.check.clazy_standalone_only and not is_standalone:
0712         return True
0713 
0714     qt = qt_installation(qt_major_version)
0715 
0716     if _verbose:
0717         print("Qt major versions required by the test: " + str(test.qt_major_versions))
0718         print("Currently considering Qt major version: " + str(qt_major_version))
0719         print("Qt versions required by the test: min=" + str(test.minimum_qt_version) + " max=" + str(test.maximum_qt_version))
0720         print("Qt int version: " + str(qt.int_version))
0721         print("Qt headers: " + qt.qmake_header_path)
0722 
0723     printableName = test.printableName(cppStandard, qt_major_version, is_standalone, False)
0724 
0725     if qt.int_version < test.minimum_qt_version or qt.int_version > test.maximum_qt_version or CLANG_VERSION < test.minimum_clang_version:
0726         if (_verbose):
0727             print("Skipping " + printableName +
0728                   " because required version is not available")
0729         return True
0730 
0731     if test.requires_std_filesystem and not _hasStdFileSystem:
0732         if (_verbose):
0733             print("Skipping " + printableName +
0734                   " because it requires std::filesystem")
0735         return True
0736 
0737     if _platform in test.blacklist_platforms:
0738         if (_verbose):
0739             print("Skipping " + printableName +
0740                   " because it is blacklisted for this platform")
0741         return True
0742 
0743     if not test.should_run_on_32bit and is32Bit():
0744         if (_verbose):
0745             print("Skipping " + printableName +
0746                   " because it is blacklisted on 32bit")
0747         return True
0748 
0749     checkname = test.check.name
0750     filename = checkname + "/" + test.filename()
0751 
0752     output_file = filename + ".out"
0753     result_file = filename + ".result"
0754     expected_file = filename + ".expected"
0755     if not os.path.exists(expected_file):
0756         expected_file = filename + ".qt" + str(qt_major_version) + ".expected"
0757 
0758     # Some tests have different output on 32 bit
0759     if is32Bit() and os.path.exists(expected_file + '.x86'):
0760         expected_file = expected_file + '.x86'
0761 
0762     if is_standalone and test.isScript():
0763         return True
0764 
0765     if is_standalone:
0766         cmd_to_run = clazy_standalone_binary() + " " + filename + " " + \
0767             clazy_standalone_command(test, cppStandard, qt)
0768     else:
0769         cmd_to_run = clazy_command(test, cppStandard, qt, filename)
0770 
0771     if test.compare_everything:
0772         result_file = output_file
0773 
0774     must_fail = test.must_fail
0775 
0776     cmd_success = run_command(cmd_to_run, output_file, test.env, ignore_verbose_command=True)
0777 
0778     if file_contains(output_file, 'Invalid check: '):
0779         return True
0780 
0781     if (not cmd_success and not must_fail) or (cmd_success and must_fail):
0782         print("[FAIL] " + printableName +
0783               " (Failed to build test. Check " + output_file + " for details)")
0784         print("-------------------")
0785         print("Contents of %s:" % output_file)
0786         print_file(output_file)
0787         print("-------------------")
0788         return False
0789 
0790     if not test.compare_everything:
0791         word_to_grep = "warning:" if not must_fail else "error:"
0792         extract_word(word_to_grep, output_file, result_file)
0793 
0794     # Check that it printed the expected warnings
0795     if not compare_files(test.expects_failure, expected_file, result_file, printableName):
0796         return False
0797 
0798     if test.has_fixits:
0799         # The normal tests succeeded, we can run the respective fixits then
0800         test.should_run_fixits_test = True
0801 
0802     return True
0803 
0804 def run_unit_test_for_each_configuration(test, is_standalone):
0805     if test.check.clazy_standalone_only and not is_standalone:
0806         return True
0807     result = True
0808     for qt_major_version in test.qt_major_versions:
0809         for cppStandard in test.cppStandards:
0810             if cppStandard == "c++14" and qt_major_version == 6: # Qt6 requires C++17
0811                 continue
0812             if cppStandard == "c++17" and qt_major_version == 5 and len(test.cppStandards) > 1: # valid combination but let's skip it unless it was the only specified standard
0813                 continue
0814             result = result and run_unit_test(test, is_standalone, cppStandard, qt_major_version)
0815     return result
0816 
0817 def run_unit_tests(tests):
0818     result = True
0819     for test in tests:
0820         test_result = True
0821         if not _only_standalone:
0822             test_result = run_unit_test_for_each_configuration(test, False)
0823             result = result and test_result
0824 
0825         if not _no_standalone:
0826             test_result = test_result and run_unit_test_for_each_configuration(test, True)
0827             result = result and test_result
0828 
0829         if not test_result:
0830             test.removeYamlFiles()
0831 
0832     global _was_successful, _lock
0833     with _lock:
0834         _was_successful = _was_successful and result
0835 
0836 
0837 def patch_yaml_files(requested_checks, is_standalone):
0838     if (is_standalone and _no_standalone) or (not is_standalone and _only_standalone):
0839         # Nothing to do
0840         return True
0841 
0842     success = True
0843     for check in requested_checks:
0844         for test in check.tests:
0845             if test.should_run_fixits_test:
0846                 yamlfilename = test.yamlFilename(is_standalone)
0847                 if not os.path.exists(yamlfilename):
0848                     print("[FAIL] " + yamlfilename + " is missing!!")
0849                     success = False
0850                     continue
0851                 if not patch_fixit_yaml_file(test, is_standalone):
0852                     print("[FAIL] Could not patch " + yamlfilename)
0853                     success = False
0854                     continue
0855     return success
0856 
0857 
0858 def compare_fixit_results(test, is_standalone):
0859 
0860     if (is_standalone and _no_standalone) or (not is_standalone and _only_standalone):
0861         # Nothing to do
0862         return True
0863 
0864     # Check that the rewritten file is identical to the expected one
0865     if not compare_files(False, test.expectedFixedFilename(), test.fixedFilename(is_standalone), test.printableName("", 0, is_standalone, True)):
0866         return False
0867 
0868     # Some fixed cpp files have an header that was also fixed. Compare it here too.
0869     possible_headerfile_expected = test.expectedFixedFilename().replace('.cpp', '.h')
0870     if os.path.exists(possible_headerfile_expected):
0871         possible_headerfile = test.fixedFilename(
0872             is_standalone).replace('.cpp', '.h')
0873         if not compare_files(False, possible_headerfile_expected, possible_headerfile, test.printableName("", 0, is_standalone, True).replace('.cpp', '.h')):
0874             return False
0875 
0876     return True
0877 
0878 # This is run sequentially, due to races. As clang-apply-replacements just applies all .yaml files it can find.
0879 # We run a single clang-apply-replacements invocation, which changes all files in the tests/ directory.
0880 
0881 
0882 def run_fixit_tests(requested_checks):
0883 
0884     success = patch_yaml_files(requested_checks, is_standalone=False)
0885     success = patch_yaml_files(
0886         requested_checks, is_standalone=True) and success
0887 
0888     for check in requested_checks:
0889 
0890         if not any(map(lambda test : test.should_run_fixits_test, check.tests)):
0891             continue
0892 
0893         # Call clazy-apply-replacements[.exe]
0894         if not run_clang_apply_replacements(check):
0895             return False
0896 
0897         # Now compare all the *.fixed files with the *.fixed.expected counterparts
0898         for test in check.tests:
0899             if test.should_run_fixits_test:
0900                 # Check that the rewritten file is identical to the expected one
0901                 if not compare_fixit_results(test, is_standalone=False):
0902                     success = False
0903                     continue
0904 
0905                 if not compare_fixit_results(test, is_standalone=True):
0906                     success = False
0907                     continue
0908 
0909     return success
0910 
0911 
0912 def dump_ast(check):
0913     for test in check.tests:
0914         for cppStandard in test.cppStandards:
0915             for version in test.qt_major_versions:
0916                 if version == 6 and cppStandard == "c++14":
0917                     continue # Qt6 requires C++17
0918                 ast_filename = test.filename() + f"_{cppStandard}_{version}.ast"
0919                 run_command(dump_ast_command(test, cppStandard, version) + " > " + ast_filename)
0920                 print("Dumped AST to " + os.getcwd() + "/" + ast_filename)
0921 
0922 # -------------------------------------------------------------------------------
0923 def load_checks(all_check_names):
0924     checks = []
0925     for name in all_check_names:
0926         try:
0927             check = load_json(name)
0928             if check.enabled:
0929                 checks.append(check)
0930         except:
0931             print("Error while loading " + name)
0932             raise
0933     return checks
0934 # -------------------------------------------------------------------------------
0935 def try_compile(filename):
0936     return run_command("%s --std=c++17 -c %s" % (clang_name(), filename))
0937 
0938 # -------------------------------------------------------------------------------
0939 # main
0940 
0941 if isLinux():
0942     # On Windows and macOS we have recent enough toolchains
0943     _hasStdFileSystem = 'CLAZY_HAS_FILESYSTEM' in os.environ or try_compile('../.cmake_has_filesystem_test.cpp')
0944 
0945 if 'CLAZY_NO_WERROR' in os.environ:
0946     del os.environ['CLAZY_NO_WERROR']
0947 
0948 os.environ['CLAZY_CHECKS'] = ''
0949 
0950 all_check_names = get_check_names()
0951 all_checks = load_checks(all_check_names)
0952 requested_check_names = args.check_names
0953 requested_check_names = list(
0954     map(lambda x: x.strip("/\\"), requested_check_names))
0955 
0956 for check_name in requested_check_names:
0957     if check_name not in all_check_names:
0958         print("Unknown check: " + check_name)
0959         sys.exit(-1)
0960 
0961 if not requested_check_names:
0962     requested_check_names = all_check_names
0963 
0964 requested_checks = list(filter(
0965     lambda check: check.name in requested_check_names and check.name not in _excluded_checks, all_checks))
0966 requested_checks = list(filter(
0967     lambda check: check.minimum_clang_version <= CLANG_VERSION, requested_checks))
0968 
0969 threads = []
0970 
0971 if _dump_ast:
0972     for check in requested_checks:
0973         os.chdir(check.name)
0974         dump_ast(check)
0975         os.chdir("..")
0976 else:
0977     cleanup_fixit_files(requested_checks)
0978     # Each list is a list of Test to be worked on by a thread
0979     list_of_chunks = [[] for _ in range(_num_threads)]
0980     i = _num_threads
0981     for check in requested_checks:
0982         for test in check.tests:
0983             i = (i + 1) % _num_threads
0984             list_of_chunks[i].append(test)
0985 
0986     for tests in list_of_chunks:
0987         if not tests:
0988             continue
0989 
0990         t = Thread(target=run_unit_tests, args=(tests,))
0991         t.start()
0992         threads.append(t)
0993 
0994 for thread in threads:
0995     thread.join()
0996 
0997 if not _no_fixits and not run_fixit_tests(requested_checks):
0998     _was_successful = False
0999 
1000 if _was_successful:
1001     print("SUCCESS")
1002     sys.exit(0)
1003 else:
1004     print("FAIL")
1005     sys.exit(-1)