Skip to content

Commit b9e15b2

Browse files
committedApr 2, 2015
[utils] Add Check Compile Flow Consistency tool (check_cfc.py).
This is a tool for checking consistency of code generation with different compiler options (such as -g or outputting to .s). This tool has found a number of code generation issues. The script acts as a wrapper to clang or clang++ performing 2 (or more) compiles then comparing the object files. Instructions for use are in check_cfc.py including how to use with LNT. Differential Revision: http://reviews.llvm.org/D8723 llvm-svn: 233919
1 parent ad3ec82 commit b9e15b2

File tree

5 files changed

+649
-0
lines changed

5 files changed

+649
-0
lines changed
 

‎clang/utils/check_cfc/check_cfc.cfg

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[Checks]
2+
dash_g_no_change = true
3+
dash_s_no_change = true

‎clang/utils/check_cfc/check_cfc.py

+388
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
#!/usr/bin/env python2.7
2+
3+
"""Check CFC - Check Compile Flow Consistency
4+
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.
8+
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
21+
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)
35+
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++ ...
41+
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+
"""
49+
50+
from __future__ import print_function
51+
52+
import imp
53+
import os
54+
import platform
55+
import shutil
56+
import subprocess
57+
import sys
58+
import tempfile
59+
import ConfigParser
60+
import io
61+
62+
import obj_diff
63+
64+
def is_windows():
65+
"""Returns True if running on Windows."""
66+
return platform.system() == 'Windows'
67+
68+
class WrapperStepException(Exception):
69+
"""Exception type to be used when a step other than the original compile
70+
fails."""
71+
def __init__(self, msg, stdout, stderr):
72+
self.msg = msg
73+
self.stdout = stdout
74+
self.stderr = stderr
75+
76+
class WrapperCheckException(Exception):
77+
"""Exception type to be used when a comparison check fails."""
78+
def __init__(self, msg):
79+
self.msg = msg
80+
81+
def main_is_frozen():
82+
"""Returns True when running as a py2exe executable."""
83+
return (hasattr(sys, "frozen") or # new py2exe
84+
hasattr(sys, "importers") or # old py2exe
85+
imp.is_frozen("__main__")) # tools/freeze
86+
87+
def get_main_dir():
88+
"""Get the directory that the script or executable is located in."""
89+
if main_is_frozen():
90+
return os.path.dirname(sys.executable)
91+
return os.path.dirname(sys.argv[0])
92+
93+
def remove_dir_from_path(path_var, directory):
94+
"""Remove the specified directory from path_var, a string representing
95+
PATH"""
96+
pathlist = path_var.split(os.pathsep)
97+
norm_directory = os.path.normpath(os.path.normcase(directory))
98+
pathlist = filter(lambda x: os.path.normpath(
99+
os.path.normcase(x)) != norm_directory, pathlist)
100+
return os.pathsep.join(pathlist)
101+
102+
def path_without_wrapper():
103+
"""Returns the PATH variable modified to remove the path to this program."""
104+
scriptdir = get_main_dir()
105+
path = os.environ['PATH']
106+
return remove_dir_from_path(path, scriptdir)
107+
108+
def flip_dash_g(args):
109+
"""Search for -g in args. If it exists then return args without. If not then
110+
add it."""
111+
if '-g' in args:
112+
# Return args without any -g
113+
return [x for x in args if x != '-g']
114+
else:
115+
# No -g, add one
116+
return args + ['-g']
117+
118+
def derive_output_file(args):
119+
"""Derive output file from the input file (if just one) or None
120+
otherwise."""
121+
infile = get_input_file(args)
122+
if infile is None:
123+
return None
124+
else:
125+
return '{}.o'.format(os.path.splitext(infile)[0])
126+
127+
def get_output_file(args):
128+
"""Return the output file specified by this command or None if not
129+
specified."""
130+
grabnext = False
131+
for arg in args:
132+
if grabnext:
133+
return arg
134+
if arg == '-o':
135+
# Specified as a separate arg
136+
grabnext = True
137+
elif arg.startswith('-o'):
138+
# Specified conjoined with -o
139+
return arg[2:]
140+
assert grabnext == False
141+
142+
return None
143+
144+
def is_output_specified(args):
145+
"""Return true is output file is specified in args."""
146+
return get_output_file(args) is not None
147+
148+
def replace_output_file(args, new_name):
149+
"""Replaces the specified name of an output file with the specified name.
150+
Assumes that the output file name is specified in the command line args."""
151+
replaceidx = None
152+
attached = False
153+
for idx, val in enumerate(args):
154+
if val == '-o':
155+
replaceidx = idx + 1
156+
attached = False
157+
elif val.startswith('-o'):
158+
replaceidx = idx
159+
attached = True
160+
161+
if replaceidx is None:
162+
raise Exception
163+
replacement = new_name
164+
if attached == True:
165+
replacement = '-o' + new_name
166+
args[replaceidx] = replacement
167+
return args
168+
169+
def add_output_file(args, output_file):
170+
"""Append an output file to args, presuming not already specified."""
171+
return args + ['-o', output_file]
172+
173+
def set_output_file(args, output_file):
174+
"""Set the output file within the arguments. Appends or replaces as
175+
appropriate."""
176+
if is_output_specified(args):
177+
args = replace_output_file(args, output_file)
178+
else:
179+
args = add_output_file(args, output_file)
180+
return args
181+
182+
gSrcFileSuffixes = ('.c', '.cpp', '.cxx', '.c++', '.cp', '.cc')
183+
184+
def get_input_file(args):
185+
"""Return the input file string if it can be found (and there is only
186+
one)."""
187+
inputFiles = list()
188+
for arg in args:
189+
testarg = arg
190+
quotes = ('"', "'")
191+
while testarg.endswith(quotes):
192+
testarg = testarg[:-1]
193+
testarg = os.path.normcase(testarg)
194+
195+
# Test if it is a source file
196+
if testarg.endswith(gSrcFileSuffixes):
197+
inputFiles.append(arg)
198+
if len(inputFiles) == 1:
199+
return inputFiles[0]
200+
else:
201+
return None
202+
203+
def set_input_file(args, input_file):
204+
"""Replaces the input file with that specified."""
205+
infile = get_input_file(args)
206+
if infile:
207+
infile_idx = args.index(infile)
208+
args[infile_idx] = input_file
209+
return args
210+
else:
211+
# Could not find input file
212+
assert False
213+
214+
def is_normal_compile(args):
215+
"""Check if this is a normal compile which will output an object file rather
216+
than a preprocess or link."""
217+
compile_step = '-c' in args
218+
# Bitcode cannot be disassembled in the same way
219+
bitcode = '-flto' in args or '-emit-llvm' in args
220+
# Version and help are queries of the compiler and override -c if specified
221+
query = '--version' in args or '--help' in args
222+
# Check if the input is recognised as a source file (this may be too
223+
# strong a restriction)
224+
input_is_valid = bool(get_input_file(args))
225+
return compile_step and not bitcode and not query and input_is_valid
226+
227+
def run_step(command, my_env, error_on_failure):
228+
"""Runs a step of the compilation. Reports failure as exception."""
229+
# Need to use shell=True on Windows as Popen won't use PATH otherwise.
230+
p = subprocess.Popen(command, stdout=subprocess.PIPE,
231+
stderr=subprocess.PIPE, env=my_env, shell=is_windows())
232+
(stdout, stderr) = p.communicate()
233+
if p.returncode != 0:
234+
raise WrapperStepException(error_on_failure, stdout, stderr)
235+
236+
def get_temp_file_name(suffix):
237+
"""Get a temporary file name with a particular suffix. Let the caller be
238+
reponsible for deleting it."""
239+
tf = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
240+
tf.close()
241+
return tf.name
242+
243+
class WrapperCheck(object):
244+
"""Base class for a check. Subclass this to add a check."""
245+
def __init__(self, output_file_a):
246+
"""Record the base output file that will be compared against."""
247+
self._output_file_a = output_file_a
248+
249+
def perform_check(self, arguments, my_env):
250+
"""Override this to perform the modified compilation and required
251+
checks."""
252+
raise NotImplementedError("Please Implement this method")
253+
254+
class dash_g_no_change(WrapperCheck):
255+
def perform_check(self, arguments, my_env):
256+
"""Check if different code is generated with/without the -g flag."""
257+
output_file_b = get_temp_file_name('.o')
258+
259+
alternate_command = list(arguments)
260+
alternate_command = flip_dash_g(alternate_command)
261+
alternate_command = set_output_file(alternate_command, output_file_b)
262+
run_step(alternate_command, my_env, "Error compiling with -g")
263+
264+
# Compare disassembly (returns first diff if differs)
265+
difference = obj_diff.compare_object_files(self._output_file_a,
266+
output_file_b)
267+
if difference:
268+
raise WrapperCheckException(
269+
"Code difference detected with -g\n{}".format(difference))
270+
271+
# Clean up temp file if comparison okay
272+
os.remove(output_file_b)
273+
274+
class dash_s_no_change(WrapperCheck):
275+
def perform_check(self, arguments, my_env):
276+
"""Check if compiling to asm then assembling in separate steps results
277+
in different code than compiling to object directly."""
278+
output_file_b = get_temp_file_name('.o')
279+
280+
alternate_command = arguments + ['-via-file-asm']
281+
alternate_command = set_output_file(alternate_command, output_file_b)
282+
run_step(alternate_command, my_env,
283+
"Error compiling with -via-file-asm")
284+
285+
# Compare disassembly (returns first diff if differs)
286+
difference = obj_diff.compare_object_files(self._output_file_a,
287+
output_file_b)
288+
if difference:
289+
raise WrapperCheckException(
290+
"Code difference detected with -S\n{}".format(difference))
291+
292+
# Clean up temp file if comparison okay
293+
os.remove(output_file_b)
294+
295+
if __name__ == '__main__':
296+
# Create configuration defaults from list of checks
297+
default_config = """
298+
[Checks]
299+
"""
300+
301+
# Find all subclasses of WrapperCheck
302+
checks = [cls.__name__ for cls in vars()['WrapperCheck'].__subclasses__()]
303+
304+
for c in checks:
305+
default_config += "{} = false\n".format(c)
306+
307+
config = ConfigParser.RawConfigParser()
308+
config.readfp(io.BytesIO(default_config))
309+
scriptdir = get_main_dir()
310+
config_path = os.path.join(scriptdir, 'check_cfc.cfg')
311+
try:
312+
config.read(os.path.join(config_path))
313+
except:
314+
print("Could not read config from {}, "
315+
"using defaults.".format(config_path))
316+
317+
my_env = os.environ.copy()
318+
my_env['PATH'] = path_without_wrapper()
319+
320+
arguments_a = list(sys.argv)
321+
322+
# Prevent infinite loop if called with absolute path.
323+
arguments_a[0] = os.path.basename(arguments_a[0])
324+
325+
# Sanity check
326+
enabled_checks = [check_name
327+
for check_name in checks
328+
if config.getboolean('Checks', check_name)]
329+
checks_comma_separated = ', '.join(enabled_checks)
330+
print("Check CFC, checking: {}".format(checks_comma_separated))
331+
332+
# A - original compilation
333+
output_file_orig = get_output_file(arguments_a)
334+
if output_file_orig is None:
335+
output_file_orig = derive_output_file(arguments_a)
336+
337+
p = subprocess.Popen(arguments_a, env=my_env, shell=is_windows())
338+
p.communicate()
339+
if p.returncode != 0:
340+
sys.exit(p.returncode)
341+
342+
if not is_normal_compile(arguments_a) or output_file_orig is None:
343+
# Bail out here if we can't apply checks in this case.
344+
# Does not indicate an error.
345+
# Maybe not straight compilation (e.g. -S or --version or -flto)
346+
# or maybe > 1 input files.
347+
sys.exit(0)
348+
349+
# Sometimes we generate files which have very long names which can't be
350+
# read/disassembled. This will exit early if we can't find the file we
351+
# expected to be output.
352+
if not os.path.isfile(output_file_orig):
353+
sys.exit(0)
354+
355+
# Copy output file to a temp file
356+
temp_output_file_orig = get_temp_file_name('.o')
357+
shutil.copyfile(output_file_orig, temp_output_file_orig)
358+
359+
# Run checks, if they are enabled in config and if they are appropriate for
360+
# this command line.
361+
current_module = sys.modules[__name__]
362+
for check_name in checks:
363+
if config.getboolean('Checks', check_name):
364+
class_ = getattr(current_module, check_name)
365+
checker = class_(temp_output_file_orig)
366+
try:
367+
checker.perform_check(arguments_a, my_env)
368+
except WrapperCheckException as e:
369+
# Check failure
370+
print(e.msg, file=sys.stderr)
371+
372+
# Remove file to comply with build system expectations (no
373+
# output file if failed)
374+
os.remove(output_file_orig)
375+
sys.exit(1)
376+
377+
except WrapperStepException as e:
378+
# Compile step failure
379+
print(e.msg, file=sys.stderr)
380+
print("*** stdout ***", file=sys.stderr)
381+
print(e.stdout, file=sys.stderr)
382+
print("*** stderr ***", file=sys.stderr)
383+
print(e.stderr, file=sys.stderr)
384+
385+
# Remove file to comply with build system expectations (no
386+
# output file if failed)
387+
os.remove(output_file_orig)
388+
sys.exit(1)

‎clang/utils/check_cfc/obj_diff.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python2.7
2+
3+
from __future__ import print_function
4+
5+
import argparse
6+
import difflib
7+
import os
8+
import subprocess
9+
import sys
10+
11+
disassembler = 'objdump'
12+
13+
def keep_line(line):
14+
"""Returns true for lines that should be compared in the disassembly
15+
output."""
16+
return "file format" not in line
17+
18+
def disassemble(objfile):
19+
"""Disassemble object to a file."""
20+
p = subprocess.Popen([disassembler, '-d', objfile],
21+
stdout=subprocess.PIPE,
22+
stderr=subprocess.PIPE)
23+
(out, err) = p.communicate()
24+
if p.returncode or err:
25+
print("Disassemble failed: {}".format(objfile))
26+
sys.exit(1)
27+
return filter(keep_line, out.split(os.linesep))
28+
29+
def first_diff(a, b, fromfile, tofile):
30+
"""Returns the first few lines of a difference, if there is one. Python
31+
diff can be very slow with large objects and the most interesting changes
32+
are the first ones. Truncate data before sending to difflib. Returns None
33+
is there is no difference."""
34+
35+
# Find first diff
36+
first_diff_idx = None
37+
for idx, val in enumerate(a):
38+
if val != b[idx]:
39+
first_diff_idx = idx
40+
break
41+
42+
if first_diff_idx == None:
43+
# No difference
44+
return None
45+
46+
# Diff to first line of diff plus some lines
47+
context = 3
48+
diff = difflib.unified_diff(a[:first_diff_idx+context],
49+
b[:first_diff_idx+context],
50+
fromfile,
51+
tofile)
52+
difference = "\n".join(diff)
53+
if first_diff_idx + context < len(a):
54+
difference += "\n*** Diff truncated ***"
55+
return difference
56+
57+
def compare_object_files(objfilea, objfileb):
58+
"""Compare disassembly of two different files.
59+
Allowing unavoidable differences, such as filenames.
60+
Return the first difference if the disassembly differs, or None.
61+
"""
62+
disa = disassemble(objfilea)
63+
disb = disassemble(objfileb)
64+
return first_diff(disa, disb, objfilea, objfileb)
65+
66+
if __name__ == '__main__':
67+
parser = argparse.ArgumentParser()
68+
parser.add_argument('objfilea', nargs=1)
69+
parser.add_argument('objfileb', nargs=1)
70+
parser.add_argument('-v', '--verbose', action='store_true')
71+
args = parser.parse_args()
72+
diff = compare_object_files(args.objfilea[0], args.objfileb[0])
73+
if diff:
74+
print("Difference detected")
75+
if args.verbose:
76+
print(diff)
77+
sys.exit(1)
78+
else:
79+
print("The same")

‎clang/utils/check_cfc/setup.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""For use on Windows. Run with:
2+
python.exe setup.py py2exe
3+
"""
4+
from distutils.core import setup
5+
try:
6+
import py2exe
7+
except ImportError:
8+
import platform
9+
import sys
10+
if platform.system() == 'Windows':
11+
print "Could not find py2exe. Please install then run setup.py py2exe."
12+
raise
13+
else:
14+
print "setup.py only required on Windows."
15+
sys.exit(1)
16+
17+
setup(
18+
console=['check_cfc.py'],
19+
name="Check CFC",
20+
description='Check Compile Flow Consistency'
21+
)
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python2.7
2+
3+
"""Test internal functions within check_cfc.py."""
4+
5+
import check_cfc
6+
import os
7+
import platform
8+
import unittest
9+
10+
11+
class TestCheckCFC(unittest.TestCase):
12+
13+
def test_flip_dash_g(self):
14+
self.assertIn('-g', check_cfc.flip_dash_g(['clang', '-c']))
15+
self.assertNotIn('-g', check_cfc.flip_dash_g(['clang', '-c', '-g']))
16+
self.assertNotIn(
17+
'-g', check_cfc.flip_dash_g(['clang', '-g', '-c', '-g']))
18+
19+
def test_remove_dir_from_path(self):
20+
bin_path = r'/usr/bin'
21+
space_path = r'/home/user/space in path'
22+
superstring_path = r'/usr/bin/local'
23+
24+
# Test removing last thing in path
25+
self.assertNotIn(
26+
bin_path, check_cfc.remove_dir_from_path(bin_path, bin_path))
27+
28+
# Test removing one entry and leaving others
29+
# Also tests removing repeated path
30+
path_var = os.pathsep.join(
31+
[superstring_path, bin_path, space_path, bin_path])
32+
stripped_path_var = check_cfc.remove_dir_from_path(path_var, bin_path)
33+
self.assertIn(superstring_path, stripped_path_var)
34+
self.assertNotIn(bin_path, stripped_path_var.split(os.pathsep))
35+
self.assertIn(space_path, stripped_path_var)
36+
37+
# Test removing non-canonical path
38+
self.assertNotIn(r'/usr//bin',
39+
check_cfc.remove_dir_from_path(r'/usr//bin', bin_path))
40+
41+
if platform == 'Windows':
42+
# Windows is case insensitive so should remove a different case
43+
# path
44+
self.assertNotIn(
45+
bin_path, check_cfc.remove_dir_from_path(path_var, r'/USR/BIN'))
46+
else:
47+
# Case sensitive so will not remove different case path
48+
self.assertIn(
49+
bin_path, check_cfc.remove_dir_from_path(path_var, r'/USR/BIN'))
50+
51+
def test_is_output_specified(self):
52+
self.assertTrue(
53+
check_cfc.is_output_specified(['clang', '-o', 'test.o']))
54+
self.assertTrue(check_cfc.is_output_specified(['clang', '-otest.o']))
55+
self.assertFalse(
56+
check_cfc.is_output_specified(['clang', '-gline-tables-only']))
57+
# Not specified for implied output file name
58+
self.assertFalse(check_cfc.is_output_specified(['clang', 'test.c']))
59+
60+
def test_get_output_file(self):
61+
self.assertEqual(
62+
check_cfc.get_output_file(['clang', '-o', 'test.o']), 'test.o')
63+
self.assertEqual(
64+
check_cfc.get_output_file(['clang', '-otest.o']), 'test.o')
65+
self.assertIsNone(
66+
check_cfc.get_output_file(['clang', '-gline-tables-only']))
67+
# Can't get output file if more than one input file
68+
self.assertIsNone(
69+
check_cfc.get_output_file(['clang', '-c', 'test.cpp', 'test2.cpp']))
70+
# No output file specified
71+
self.assertIsNone(check_cfc.get_output_file(['clang', '-c', 'test.c']))
72+
73+
def test_derive_output_file(self):
74+
# Test getting implicit output file
75+
self.assertEqual(
76+
check_cfc.derive_output_file(['clang', '-c', 'test.c']), 'test.o')
77+
self.assertEqual(
78+
check_cfc.derive_output_file(['clang', '-c', 'test.cpp']), 'test.o')
79+
self.assertIsNone(check_cfc.derive_output_file(['clang', '--version']))
80+
81+
def test_is_normal_compile(self):
82+
self.assertTrue(check_cfc.is_normal_compile(
83+
['clang', '-c', 'test.cpp', '-o', 'test2.o']))
84+
self.assertTrue(
85+
check_cfc.is_normal_compile(['clang', '-c', 'test.cpp']))
86+
# Outputting bitcode is not a normal compile
87+
self.assertFalse(
88+
check_cfc.is_normal_compile(['clang', '-c', 'test.cpp', '-flto']))
89+
self.assertFalse(
90+
check_cfc.is_normal_compile(['clang', '-c', 'test.cpp', '-emit-llvm']))
91+
# Outputting preprocessed output or assembly is not a normal compile
92+
self.assertFalse(
93+
check_cfc.is_normal_compile(['clang', '-E', 'test.cpp', '-o', 'test.ii']))
94+
self.assertFalse(
95+
check_cfc.is_normal_compile(['clang', '-S', 'test.cpp', '-o', 'test.s']))
96+
# Input of preprocessed or assembly is not a "normal compile"
97+
self.assertFalse(
98+
check_cfc.is_normal_compile(['clang', '-c', 'test.s', '-o', 'test.o']))
99+
self.assertFalse(
100+
check_cfc.is_normal_compile(['clang', '-c', 'test.ii', '-o', 'test.o']))
101+
# Specifying --version and -c is not a normal compile
102+
self.assertFalse(
103+
check_cfc.is_normal_compile(['clang', '-c', 'test.cpp', '--version']))
104+
self.assertFalse(
105+
check_cfc.is_normal_compile(['clang', '-c', 'test.cpp', '--help']))
106+
107+
def test_replace_output_file(self):
108+
self.assertEqual(check_cfc.replace_output_file(
109+
['clang', '-o', 'test.o'], 'testg.o'), ['clang', '-o', 'testg.o'])
110+
self.assertEqual(check_cfc.replace_output_file(
111+
['clang', '-otest.o'], 'testg.o'), ['clang', '-otestg.o'])
112+
with self.assertRaises(Exception):
113+
check_cfc.replace_output_file(['clang'], 'testg.o')
114+
115+
def test_add_output_file(self):
116+
self.assertEqual(check_cfc.add_output_file(
117+
['clang'], 'testg.o'), ['clang', '-o', 'testg.o'])
118+
119+
def test_set_output_file(self):
120+
# Test output not specified
121+
self.assertEqual(
122+
check_cfc.set_output_file(['clang'], 'test.o'), ['clang', '-o', 'test.o'])
123+
# Test output is specified
124+
self.assertEqual(check_cfc.set_output_file(
125+
['clang', '-o', 'test.o'], 'testb.o'), ['clang', '-o', 'testb.o'])
126+
127+
def test_get_input_file(self):
128+
# No input file
129+
self.assertIsNone(check_cfc.get_input_file(['clang']))
130+
# Input C file
131+
self.assertEqual(
132+
check_cfc.get_input_file(['clang', 'test.c']), 'test.c')
133+
# Input C++ file
134+
self.assertEqual(
135+
check_cfc.get_input_file(['clang', 'test.cpp']), 'test.cpp')
136+
# Multiple input files
137+
self.assertIsNone(
138+
check_cfc.get_input_file(['clang', 'test.c', 'test2.cpp']))
139+
self.assertIsNone(
140+
check_cfc.get_input_file(['clang', 'test.c', 'test2.c']))
141+
# Don't handle preprocessed files
142+
self.assertIsNone(check_cfc.get_input_file(['clang', 'test.i']))
143+
self.assertIsNone(check_cfc.get_input_file(['clang', 'test.ii']))
144+
# Test identifying input file with quotes
145+
self.assertEqual(
146+
check_cfc.get_input_file(['clang', '"test.c"']), '"test.c"')
147+
self.assertEqual(
148+
check_cfc.get_input_file(['clang', "'test.c'"]), "'test.c'")
149+
# Test multiple quotes
150+
self.assertEqual(
151+
check_cfc.get_input_file(['clang', "\"'test.c'\""]), "\"'test.c'\"")
152+
153+
def test_set_input_file(self):
154+
self.assertEqual(check_cfc.set_input_file(
155+
['clang', 'test.c'], 'test.s'), ['clang', 'test.s'])
156+
157+
if __name__ == '__main__':
158+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.