[memprof] Upgrade a unit test to MemProf Version 3 (#117063)
[llvm-project.git] / clang / utils / check_cfc / check_cfc.py
blob8d42ec532bbb751c31193976c1f5a81a99b5f371
1 #!/usr/bin/env python
3 """Check CFC - Check Compile Flow Consistency
5 This is a compiler wrapper for testing that code generation is consistent with
6 different compilation processes. It checks that code is not unduly affected by
7 compiler options or other changes which should not have side effects.
9 To use:
10 -Ensure that the compiler under test (i.e. clang, clang++) is on the PATH
11 -On Linux copy this script to the name of the compiler
12 e.g. cp check_cfc.py clang && cp check_cfc.py clang++
13 -On Windows use setup.py to generate check_cfc.exe and copy that to clang.exe
14 and clang++.exe
15 -Enable the desired checks in check_cfc.cfg (in the same directory as the
16 wrapper)
17 e.g.
18 [Checks]
19 dash_g_no_change = true
20 dash_s_no_change = false
22 -The wrapper can be run using its absolute path or added to PATH before the
23 compiler under test
24 e.g. export PATH=<path to check_cfc>:$PATH
25 -Compile as normal. The wrapper intercepts normal -c compiles and will return
26 non-zero if the check fails.
27 e.g.
28 $ clang -c test.cpp
29 Code difference detected with -g
30 --- /tmp/tmp5nv893.o
31 +++ /tmp/tmp6Vwjnc.o
32 @@ -1 +1 @@
33 - 0: 48 8b 05 51 0b 20 00 mov 0x200b51(%rip),%rax
34 + 0: 48 39 3d 51 0b 20 00 cmp %rdi,0x200b51(%rip)
36 -To run LNT with Check CFC specify the absolute path to the wrapper to the --cc
37 and --cxx options
38 e.g.
39 lnt runtest nt --cc <path to check_cfc>/clang \\
40 --cxx <path to check_cfc>/clang++ ...
42 To add a new check:
43 -Create a new subclass of WrapperCheck
44 -Implement the perform_check() method. This should perform the alternate compile
45 and do the comparison.
46 -Add the new check to check_cfc.cfg. The check has the same name as the
47 subclass.
48 """
50 from __future__ import absolute_import, division, print_function
52 import imp
53 import os
54 import platform
55 import shutil
56 import subprocess
57 import sys
58 import tempfile
60 try:
61 import configparser
62 except ImportError:
63 import ConfigParser as configparser
64 import io
66 import obj_diff
69 def is_windows():
70 """Returns True if running on Windows."""
71 return platform.system() == "Windows"
74 class WrapperStepException(Exception):
75 """Exception type to be used when a step other than the original compile
76 fails."""
78 def __init__(self, msg, stdout, stderr):
79 self.msg = msg
80 self.stdout = stdout
81 self.stderr = stderr
84 class WrapperCheckException(Exception):
85 """Exception type to be used when a comparison check fails."""
87 def __init__(self, msg):
88 self.msg = msg
91 def main_is_frozen():
92 """Returns True when running as a py2exe executable."""
93 return (
94 hasattr(sys, "frozen")
95 or hasattr(sys, "importers") # new py2exe
96 or imp.is_frozen("__main__") # old py2exe
97 ) # tools/freeze
100 def get_main_dir():
101 """Get the directory that the script or executable is located in."""
102 if main_is_frozen():
103 return os.path.dirname(sys.executable)
104 return os.path.dirname(sys.argv[0])
107 def remove_dir_from_path(path_var, directory):
108 """Remove the specified directory from path_var, a string representing
109 PATH"""
110 pathlist = path_var.split(os.pathsep)
111 norm_directory = os.path.normpath(os.path.normcase(directory))
112 pathlist = [
113 x for x in pathlist if os.path.normpath(os.path.normcase(x)) != norm_directory
115 return os.pathsep.join(pathlist)
118 def path_without_wrapper():
119 """Returns the PATH variable modified to remove the path to this program."""
120 scriptdir = get_main_dir()
121 path = os.environ["PATH"]
122 return remove_dir_from_path(path, scriptdir)
125 def flip_dash_g(args):
126 """Search for -g in args. If it exists then return args without. If not then
127 add it."""
128 if "-g" in args:
129 # Return args without any -g
130 return [x for x in args if x != "-g"]
131 else:
132 # No -g, add one
133 return args + ["-g"]
136 def derive_output_file(args):
137 """Derive output file from the input file (if just one) or None
138 otherwise."""
139 infile = get_input_file(args)
140 if infile is None:
141 return None
142 else:
143 return "{}.o".format(os.path.splitext(infile)[0])
146 def get_output_file(args):
147 """Return the output file specified by this command or None if not
148 specified."""
149 grabnext = False
150 for arg in args:
151 if grabnext:
152 return arg
153 if arg == "-o":
154 # Specified as a separate arg
155 grabnext = True
156 elif arg.startswith("-o"):
157 # Specified conjoined with -o
158 return arg[2:]
159 assert not grabnext
161 return None
164 def is_output_specified(args):
165 """Return true is output file is specified in args."""
166 return get_output_file(args) is not None
169 def replace_output_file(args, new_name):
170 """Replaces the specified name of an output file with the specified name.
171 Assumes that the output file name is specified in the command line args."""
172 replaceidx = None
173 attached = False
174 for idx, val in enumerate(args):
175 if val == "-o":
176 replaceidx = idx + 1
177 attached = False
178 elif val.startswith("-o"):
179 replaceidx = idx
180 attached = True
182 if replaceidx is None:
183 raise Exception
184 replacement = new_name
185 if attached:
186 replacement = "-o" + new_name
187 args[replaceidx] = replacement
188 return args
191 def add_output_file(args, output_file):
192 """Append an output file to args, presuming not already specified."""
193 return args + ["-o", output_file]
196 def set_output_file(args, output_file):
197 """Set the output file within the arguments. Appends or replaces as
198 appropriate."""
199 if is_output_specified(args):
200 args = replace_output_file(args, output_file)
201 else:
202 args = add_output_file(args, output_file)
203 return args
206 gSrcFileSuffixes = (".c", ".cpp", ".cxx", ".c++", ".cp", ".cc")
209 def get_input_file(args):
210 """Return the input file string if it can be found (and there is only
211 one)."""
212 inputFiles = list()
213 for arg in args:
214 testarg = arg
215 quotes = ('"', "'")
216 while testarg.endswith(quotes):
217 testarg = testarg[:-1]
218 testarg = os.path.normcase(testarg)
220 # Test if it is a source file
221 if testarg.endswith(gSrcFileSuffixes):
222 inputFiles.append(arg)
223 if len(inputFiles) == 1:
224 return inputFiles[0]
225 else:
226 return None
229 def set_input_file(args, input_file):
230 """Replaces the input file with that specified."""
231 infile = get_input_file(args)
232 if infile:
233 infile_idx = args.index(infile)
234 args[infile_idx] = input_file
235 return args
236 else:
237 # Could not find input file
238 assert False
241 def is_normal_compile(args):
242 """Check if this is a normal compile which will output an object file rather
243 than a preprocess or link. args is a list of command line arguments."""
244 compile_step = "-c" in args
245 # Bitcode cannot be disassembled in the same way
246 bitcode = "-flto" in args or "-emit-llvm" in args
247 # Version and help are queries of the compiler and override -c if specified
248 query = "--version" in args or "--help" in args
249 # Options to output dependency files for make
250 dependency = "-M" in args or "-MM" in args
251 # Check if the input is recognised as a source file (this may be too
252 # strong a restriction)
253 input_is_valid = bool(get_input_file(args))
254 return (
255 compile_step and not bitcode and not query and not dependency and input_is_valid
259 def run_step(command, my_env, error_on_failure):
260 """Runs a step of the compilation. Reports failure as exception."""
261 # Need to use shell=True on Windows as Popen won't use PATH otherwise.
262 p = subprocess.Popen(
263 command,
264 stdout=subprocess.PIPE,
265 stderr=subprocess.PIPE,
266 env=my_env,
267 shell=is_windows(),
269 (stdout, stderr) = p.communicate()
270 if p.returncode != 0:
271 raise WrapperStepException(error_on_failure, stdout, stderr)
274 def get_temp_file_name(suffix):
275 """Get a temporary file name with a particular suffix. Let the caller be
276 responsible for deleting it."""
277 tf = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
278 tf.close()
279 return tf.name
282 class WrapperCheck(object):
283 """Base class for a check. Subclass this to add a check."""
285 def __init__(self, output_file_a):
286 """Record the base output file that will be compared against."""
287 self._output_file_a = output_file_a
289 def perform_check(self, arguments, my_env):
290 """Override this to perform the modified compilation and required
291 checks."""
292 raise NotImplementedError("Please Implement this method")
295 class dash_g_no_change(WrapperCheck):
296 def perform_check(self, arguments, my_env):
297 """Check if different code is generated with/without the -g flag."""
298 output_file_b = get_temp_file_name(".o")
300 alternate_command = list(arguments)
301 alternate_command = flip_dash_g(alternate_command)
302 alternate_command = set_output_file(alternate_command, output_file_b)
303 run_step(alternate_command, my_env, "Error compiling with -g")
305 # Compare disassembly (returns first diff if differs)
306 difference = obj_diff.compare_object_files(self._output_file_a, output_file_b)
307 if difference:
308 raise WrapperCheckException(
309 "Code difference detected with -g\n{}".format(difference)
312 # Clean up temp file if comparison okay
313 os.remove(output_file_b)
316 class dash_s_no_change(WrapperCheck):
317 def perform_check(self, arguments, my_env):
318 """Check if compiling to asm then assembling in separate steps results
319 in different code than compiling to object directly."""
320 output_file_b = get_temp_file_name(".o")
322 alternate_command = arguments + ["-via-file-asm"]
323 alternate_command = set_output_file(alternate_command, output_file_b)
324 run_step(alternate_command, my_env, "Error compiling with -via-file-asm")
326 # Compare if object files are exactly the same
327 exactly_equal = obj_diff.compare_exact(self._output_file_a, output_file_b)
328 if not exactly_equal:
329 # Compare disassembly (returns first diff if differs)
330 difference = obj_diff.compare_object_files(
331 self._output_file_a, output_file_b
333 if difference:
334 raise WrapperCheckException(
335 "Code difference detected with -S\n{}".format(difference)
338 # Code is identical, compare debug info
339 dbgdifference = obj_diff.compare_debug_info(
340 self._output_file_a, output_file_b
342 if dbgdifference:
343 raise WrapperCheckException(
344 "Debug info difference detected with -S\n{}".format(dbgdifference)
347 raise WrapperCheckException("Object files not identical with -S\n")
349 # Clean up temp file if comparison okay
350 os.remove(output_file_b)
353 if __name__ == "__main__":
354 # Create configuration defaults from list of checks
355 default_config = """
356 [Checks]
359 # Find all subclasses of WrapperCheck
360 checks = [cls.__name__ for cls in vars()["WrapperCheck"].__subclasses__()]
362 for c in checks:
363 default_config += "{} = false\n".format(c)
365 config = configparser.RawConfigParser()
366 config.readfp(io.BytesIO(default_config))
367 scriptdir = get_main_dir()
368 config_path = os.path.join(scriptdir, "check_cfc.cfg")
369 try:
370 config.read(os.path.join(config_path))
371 except:
372 print("Could not read config from {}, " "using defaults.".format(config_path))
374 my_env = os.environ.copy()
375 my_env["PATH"] = path_without_wrapper()
377 arguments_a = list(sys.argv)
379 # Prevent infinite loop if called with absolute path.
380 arguments_a[0] = os.path.basename(arguments_a[0])
382 # Basic correctness check
383 enabled_checks = [
384 check_name for check_name in checks if config.getboolean("Checks", check_name)
386 checks_comma_separated = ", ".join(enabled_checks)
387 print("Check CFC, checking: {}".format(checks_comma_separated))
389 # A - original compilation
390 output_file_orig = get_output_file(arguments_a)
391 if output_file_orig is None:
392 output_file_orig = derive_output_file(arguments_a)
394 p = subprocess.Popen(arguments_a, env=my_env, shell=is_windows())
395 p.communicate()
396 if p.returncode != 0:
397 sys.exit(p.returncode)
399 if not is_normal_compile(arguments_a) or output_file_orig is None:
400 # Bail out here if we can't apply checks in this case.
401 # Does not indicate an error.
402 # Maybe not straight compilation (e.g. -S or --version or -flto)
403 # or maybe > 1 input files.
404 sys.exit(0)
406 # Sometimes we generate files which have very long names which can't be
407 # read/disassembled. This will exit early if we can't find the file we
408 # expected to be output.
409 if not os.path.isfile(output_file_orig):
410 sys.exit(0)
412 # Copy output file to a temp file
413 temp_output_file_orig = get_temp_file_name(".o")
414 shutil.copyfile(output_file_orig, temp_output_file_orig)
416 # Run checks, if they are enabled in config and if they are appropriate for
417 # this command line.
418 current_module = sys.modules[__name__]
419 for check_name in checks:
420 if config.getboolean("Checks", check_name):
421 class_ = getattr(current_module, check_name)
422 checker = class_(temp_output_file_orig)
423 try:
424 checker.perform_check(arguments_a, my_env)
425 except WrapperCheckException as e:
426 # Check failure
427 print(
428 "{} {}".format(get_input_file(arguments_a), e.msg), file=sys.stderr
431 # Remove file to comply with build system expectations (no
432 # output file if failed)
433 os.remove(output_file_orig)
434 sys.exit(1)
436 except WrapperStepException as e:
437 # Compile step failure
438 print(e.msg, file=sys.stderr)
439 print("*** stdout ***", file=sys.stderr)
440 print(e.stdout, file=sys.stderr)
441 print("*** stderr ***", file=sys.stderr)
442 print(e.stderr, file=sys.stderr)
444 # Remove file to comply with build system expectations (no
445 # output file if failed)
446 os.remove(output_file_orig)
447 sys.exit(1)