1# GObject-Introspection - a framework for introspecting GObject libraries
2#
3# This library is free software; you can redistribute it and/or
4# modify it under the terms of the GNU Lesser General Public
5# License as published by the Free Software Foundation; either
6# version 2 of the License, or (at your option) any later version.
7#
8# This library is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11# Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public
14# License along with this library; if not, write to the
15# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
16# Boston, MA 02111-1307, USA.
17
18import distutils
19import os
20import shlex
21import unittest
22from contextlib import contextmanager
23
24from giscanner.ccompiler import CCompiler, FLAGS_RETAINING_MACROS
25
26
27@contextmanager
28def Environ(new_environ):
29    """Context manager for os.environ."""
30    old_environ = os.environ.copy()
31    os.environ.clear()
32    os.environ.update(new_environ)
33    try:
34        yield
35    finally:
36        # Restore previous environment
37        os.environ.clear()
38        os.environ.update(old_environ)
39
40
41@unittest.skipIf(os.name != 'posix', 'tests for unix compiler only')
42class UnixCCompilerTest(unittest.TestCase):
43
44    def assertListStartsWith(self, seq, prefix):
45        """Checks whether seq starts with specified prefix."""
46        if not isinstance(seq, list):
47            raise self.fail('First argument is not a list: %r' % (seq,))
48        if not isinstance(prefix, list):
49            raise self.fail('Second argument is not a list: %r' % (prefix,))
50        self.assertSequenceEqual(seq[:len(prefix)], prefix, seq_type=list)
51
52    def assertIsSubsequence(self, list1, list2):
53        """Checks whether list1 is a subsequence of list2. Not necessarily a contiguous one."""
54        if not isinstance(list1, list):
55            raise self.fail('First argument is not a list: %r' % (list1,))
56        if not isinstance(list2, list):
57            raise self.fail('Second argument is not a list: %r' % (list2,))
58        start = 0
59        for elem in list1:
60            try:
61                start = list2.index(elem, start) + 1
62            except ValueError:
63                self.fail('%r is not a subsequence of %r' % (list1, list2))
64
65    def test_link_cmd(self):
66        with Environ(dict(CC="foobar")):
67            compiler = CCompiler()
68            self.assertEqual(compiler.linker_cmd[0], "foobar")
69
70    def test_link_args_override(self):
71        with Environ(dict(CC="foobar")):
72            compiler = CCompiler()
73            self.assertEqual(compiler.compiler.linker_so[0], "foobar")
74
75    def compile_args(self, environ={}, compiler_name='unix',
76                     pkg_config_cflags=[], cpp_includes=[],
77                     source='a.c', init_sections=[]):
78        """Returns a list of arguments that would be passed to the compiler executing given compilation step."""
79
80        try:
81            from unittest.mock import patch
82        except ImportError as e:
83            raise unittest.SkipTest(e)
84
85        with patch.object(distutils.ccompiler.CCompiler, 'spawn') as spawn:
86            with Environ(environ):
87                cc = CCompiler(compiler_name=compiler_name)
88                # Avoid check if target is newer from source.
89                cc.compiler.force = True
90                # Don't actually do anything.
91                cc.compiler.dry_run = True
92                cc.compile(pkg_config_cflags, cpp_includes, [source], init_sections)
93        self.assertEqual(1, spawn.call_count)
94        args, kwargs = spawn.call_args
95        return args[0]
96
97    def preprocess_args(self, environ={}, compiler_name=None,
98                        source='a.c', output=None, cpp_options=[]):
99        """Returns a list of arguments that would be passed to the preprocessor executing given preprocessing step."""
100
101        try:
102            from unittest.mock import patch
103        except ImportError as e:
104            raise unittest.SkipTest(e)
105
106        with patch.object(distutils.ccompiler.CCompiler, 'spawn') as spawn:
107            with Environ(environ):
108                cc = CCompiler(compiler_name=compiler_name)
109                # Avoid check if target is newer from source.
110                cc.compiler.force = True
111                # Don't actually do anything.
112                cc.compiler.dry_run = True
113                cc.preprocess(source, output, cpp_options)
114        self.assertEqual(1, spawn.call_count)
115        args, kwargs = spawn.call_args
116        return args[0]
117
118    @unittest.skip("Currently a Python build time compiler is used as the default.")
119    def test_compile_default(self):
120        """Checks that cc is used as the default compiler."""
121        args = self.compile_args()
122        self.assertListStartsWith(args, ['cc'])
123
124    def test_compile_cc(self):
125        """Checks that CC overrides used compiler."""
126        args = self.compile_args(environ=dict(CC='supercc'))
127        self.assertListStartsWith(args, ['supercc'])
128
129    def test_preprocess_cc(self):
130        """Checks that CC overrides used preprocessor when CPP is unspecified."""
131        args = self.preprocess_args(environ=dict(CC='clang'))
132        self.assertListStartsWith(args, ['clang'])
133        self.assertIn('-E', args)
134
135    def test_preprocess_cpp(self):
136        """Checks that CPP overrides used preprocessor regardless of CC."""
137        args = self.preprocess_args(environ=dict(CC='my-compiler', CPP='my-preprocessor'))
138        self.assertListStartsWith(args, ['my-preprocessor'])
139        self.assertNotIn('-E', args)
140
141    @unittest.skip("Currently a Python build time preprocessor is used as the default")
142    def test_preprocess_default(self):
143        """Checks that cpp is used as the default preprocessor."""
144        args = self.preprocess_args()
145        self.assertListStartsWith(args, ['cpp'])
146
147    def test_multiple_args_in_cc(self):
148        """Checks that shell word splitting rules are used to split CC."""
149        args = self.compile_args(environ=dict(CC='build-log -m " hello  there  " gcc'))
150        self.assertListStartsWith(args, ['build-log', '-m', ' hello  there  ', 'gcc'])
151
152    def test_multiple_args_in_cpp(self):
153        """Checks that shell word splitting rules are used to split CPP."""
154        args = self.preprocess_args(environ=dict(CPP='build-log -m " hello  there" gcc -E'))
155        self.assertListStartsWith(args, ['build-log', '-m', ' hello  there', 'gcc', '-E'])
156
157    def test_deprecation_warnings_are_disabled_during_compilation(self):
158        """Checks that deprecation warnings are disabled during compilation."""
159        args = self.compile_args()
160        self.assertIn('-Wno-deprecated-declarations', args)
161
162    def test_preprocess_includes_cppflags(self):
163        """Checks that preprocessing step includes CPPFLAGS."""
164        args = self.preprocess_args(environ=dict(CPPFLAGS='-fsecure -Ddebug'))
165        self.assertIsSubsequence(['-fsecure', '-Ddebug'], args)
166
167    def test_compile_includes_cppflags(self):
168        """Checks that compilation step includes both CFLAGS and CPPFLAGS, in that order."""
169        args = self.compile_args(environ=dict(CFLAGS='-lfoo -Da="x y" -Weverything',
170                                              CPPFLAGS='-fsecure -Ddebug'))
171        self.assertIsSubsequence(['-lfoo', '-Da=x y', '-Weverything', '-fsecure', '-Ddebug'],
172                                 args)
173
174    def test_flags_retaining_macros_are_filtered_out(self):
175        """Checks that flags that would retain macros after preprocessing step are filtered out."""
176        args = self.preprocess_args(cpp_options=list(FLAGS_RETAINING_MACROS))
177        for flag in FLAGS_RETAINING_MACROS:
178            self.assertNotIn(flag, args)
179
180    @unittest.expectedFailure
181    def test_macros(self):
182        """"Checks that macros from CFLAGS are defined only once."""
183        args = self.compile_args(environ=dict(CFLAGS='-DSECRET_MACRO'))
184        self.assertEqual(1, args.count('-DSECRET_MACRO'))
185
186    @unittest.expectedFailure
187    def test_flags_retaining_macros_are_filtered_out_from_cppflags(self):
188        """Checks that flags that would retain macros after preprocessing step are filtered out from CPPFLAGS."""
189        cppflags = ' '.join(shlex.quote(flag) for flag in FLAGS_RETAINING_MACROS)
190        args = self.preprocess_args(environ=dict(CPPFLAGS=cppflags))
191        for flag in FLAGS_RETAINING_MACROS:
192            self.assertNotIn(flag, args)
193
194    def test_preprocess_preserves_comments(self):
195        """Checks that preprocessing step includes flag that preserves comments."""
196        args = self.preprocess_args()
197        self.assertIn('-C', args)
198
199    def test_perprocess_includes_cwd(self):
200        """Checks that preprocessing includes current working directory."""
201        args = self.preprocess_args()
202        self.assertIn('-I.', args)
203
204    def test_preprocess_command(self):
205        """"Checks complete preprocessing command."""
206        args = self.preprocess_args(environ=dict(CPP='gcc -E'),
207                                    source='/tmp/file.c')
208        self.assertEqual(['gcc', '-E', '-I.', '-C', '/tmp/file.c'],
209                         args)
210
211    def test_compile_command(self):
212        """Checks complete compilation command."""
213        args = self.compile_args(environ=dict(CC='clang'),
214                                 source='/tmp/file.c')
215        self.assertEqual(['clang',
216                          '-c', '/tmp/file.c',
217                          '-o', '/tmp/file.o',
218                          '-Wno-deprecated-declarations'],
219                         args)
220
221
222if __name__ == '__main__':
223    unittest.main()
224