summaryrefslogtreecommitdiff
path: root/tests/scanner/test_ccompiler.py
blob: dd8700d870cedf0fbc689ee5a52503e0c969288d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# GObject-Introspection - a framework for introspecting GObject libraries
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

import distutils
import os
import shlex
import unittest
from contextlib import contextmanager

from giscanner.ccompiler import CCompiler, FLAGS_RETAINING_MACROS


@contextmanager
def Environ(new_environ):
    """Context manager for os.environ."""
    old_environ = os.environ.copy()
    os.environ.clear()
    os.environ.update(new_environ)
    try:
        yield
    finally:
        # Restore previous environment
        os.environ.clear()
        os.environ.update(old_environ)


@unittest.skipIf(os.name != 'posix', 'tests for unix compiler only')
class UnixCCompilerTest(unittest.TestCase):

    def assertListStartsWith(self, seq, prefix):
        """Checks whether seq starts with specified prefix."""
        if not isinstance(seq, list):
            raise self.fail('First argument is not a list: %r' % (seq,))
        if not isinstance(prefix, list):
            raise self.fail('Second argument is not a list: %r' % (prefix,))
        self.assertSequenceEqual(seq[:len(prefix)], prefix, seq_type=list)

    def assertIsSubsequence(self, list1, list2):
        """Checks whether list1 is a subsequence of list2. Not necessarily a contiguous one."""
        if not isinstance(list1, list):
            raise self.fail('First argument is not a list: %r' % (list1,))
        if not isinstance(list2, list):
            raise self.fail('Second argument is not a list: %r' % (list2,))
        start = 0
        for elem in list1:
            try:
                start = list2.index(elem, start) + 1
            except ValueError:
                self.fail('%r is not a subsequence of %r' % (list1, list2))

    def test_link_args_override(self):
        with Environ(dict(CC="foobar")):
            compiler = CCompiler()
            self.assertEqual(compiler.compiler.linker_so[0], "foobar")

    def compile_args(self, environ={}, compiler_name='unix',
                     pkg_config_cflags=[], cpp_includes=[],
                     source='a.c', init_sections=[]):
        """Returns a list of arguments that would be passed to the compiler executing given compilation step."""

        try:
            from unittest.mock import patch
        except ImportError as e:
            raise unittest.SkipTest(e)

        with patch.object(distutils.ccompiler.CCompiler, 'spawn') as spawn:
            with Environ(environ):
                cc = CCompiler(compiler_name=compiler_name)
                # Avoid check if target is newer from source.
                cc.compiler.force = True
                # Don't actually do anything.
                cc.compiler.dry_run = True
                cc.compile(pkg_config_cflags, cpp_includes, [source], init_sections)
        spawn.assert_called_once()
        args, kwargs = spawn.call_args
        return args[0]

    def preprocess_args(self, environ={}, compiler_name=None,
                        source='a.c', output=None, cpp_options=[]):
        """Returns a list of arguments that would be passed to the preprocessor executing given preprocessing step."""

        try:
            from unittest.mock import patch
        except ImportError as e:
            raise unittest.SkipTest(e)

        with patch.object(distutils.ccompiler.CCompiler, 'spawn') as spawn:
            with Environ(environ):
                cc = CCompiler(compiler_name=compiler_name)
                # Avoid check if target is newer from source.
                cc.compiler.force = True
                # Don't actually do anything.
                cc.compiler.dry_run = True
                cc.preprocess(source, output, cpp_options)
        spawn.assert_called_once()
        args, kwargs = spawn.call_args
        return args[0]

    @unittest.skip("Currently a Python build time compiler is used as the default.")
    def test_compile_default(self):
        """Checks that cc is used as the default compiler."""
        args = self.compile_args()
        self.assertListStartsWith(args, ['cc'])

    def test_compile_cc(self):
        """Checks that CC overrides used compiler."""
        args = self.compile_args(environ=dict(CC='supercc'))
        self.assertListStartsWith(args, ['supercc'])

    def test_preprocess_cc(self):
        """Checks that CC overrides used preprocessor when CPP is unspecified."""
        args = self.preprocess_args(environ=dict(CC='clang'))
        self.assertListStartsWith(args, ['clang'])
        self.assertIn('-E', args)

    def test_preprocess_cpp(self):
        """Checks that CPP overrides used preprocessor regardless of CC."""
        args = self.preprocess_args(environ=dict(CC='my-compiler', CPP='my-preprocessor'))
        self.assertListStartsWith(args, ['my-preprocessor'])
        self.assertNotIn('-E', args)

    @unittest.skip("Currently a Python build time preprocessor is used as the default")
    def test_preprocess_default(self):
        """Checks that cpp is used as the default preprocessor."""
        args = self.preprocess_args()
        self.assertListStartsWith(args, ['cpp'])

    def test_multiple_args_in_cc(self):
        """Checks that shell word splitting rules are used to split CC."""
        args = self.compile_args(environ=dict(CC='build-log -m " hello  there  " gcc'))
        self.assertListStartsWith(args, ['build-log', '-m', ' hello  there  ', 'gcc'])

    def test_multiple_args_in_cpp(self):
        """Checks that shell word splitting rules are used to split CPP."""
        args = self.preprocess_args(environ=dict(CPP='build-log -m " hello  there" gcc -E'))
        self.assertListStartsWith(args, ['build-log', '-m', ' hello  there', 'gcc', '-E'])

    def test_deprecation_warnings_are_disabled_during_compilation(self):
        """Checks that deprecation warnings are disabled during compilation."""
        args = self.compile_args()
        self.assertIn('-Wno-deprecated-declarations', args)

    def test_preprocess_includes_cppflags(self):
        """Checks that preprocessing step includes CPPFLAGS."""
        args = self.preprocess_args(environ=dict(CPPFLAGS='-fsecure -Ddebug'))
        self.assertIsSubsequence(['-fsecure', '-Ddebug'], args)

    def test_compile_includes_cppflags(self):
        """Checks that compilation step includes both CFLAGS and CPPFLAGS, in that order."""
        args = self.compile_args(environ=dict(CFLAGS='-lfoo -Da="x y" -Weverything',
                                              CPPFLAGS='-fsecure -Ddebug'))
        self.assertIsSubsequence(['-lfoo', '-Da=x y', '-Weverything', '-fsecure', '-Ddebug'],
                                 args)

    def test_flags_retaining_macros_are_filtered_out(self):
        """Checks that flags that would retain macros after preprocessing step are filtered out."""
        args = self.preprocess_args(cpp_options=list(FLAGS_RETAINING_MACROS))
        for flag in FLAGS_RETAINING_MACROS:
            self.assertNotIn(flag, args)

    @unittest.expectedFailure
    def test_macros(self):
        """"Checks that macros from CFLAGS are defined only once."""
        args = self.compile_args(environ=dict(CFLAGS='-DSECRET_MACRO'))
        self.assertEqual(1, args.count('-DSECRET_MACRO'))

    @unittest.expectedFailure
    def test_flags_retaining_macros_are_filtered_out_from_cppflags(self):
        """Checks that flags that would retain macros after preprocessing step are filtered out from CPPFLAGS."""
        cppflags = ' '.join(shlex.quote(flag) for flag in FLAGS_RETAINING_MACROS)
        args = self.preprocess_args(environ=dict(CPPFLAGS=cppflags))
        for flag in FLAGS_RETAINING_MACROS:
            self.assertNotIn(flag, args)

    def test_preprocess_preserves_comments(self):
        """Checks that preprocessing step includes flag that preserves comments."""
        args = self.preprocess_args()
        self.assertIn('-C', args)

    def test_perprocess_includes_cwd(self):
        """Checks that preprocessing includes current working directory."""
        args = self.preprocess_args()
        self.assertIn('-I.', args)

    def test_preprocess_command(self):
        """"Checks complete preprocessing command."""
        args = self.preprocess_args(environ=dict(CPP='gcc -E'),
                                    source='/tmp/file.c')
        self.assertEqual(['gcc', '-E', '-I.', '-C', '/tmp/file.c'],
                         args)

    def test_compile_command(self):
        """Checks complete compilation command."""
        args = self.compile_args(environ=dict(CC='clang'),
                                 source='/tmp/file.c')
        self.assertEqual(['clang',
                          '-c', '/tmp/file.c',
                          '-o', '/tmp/file.o',
                          '-Wno-deprecated-declarations'],
                         args)


if __name__ == '__main__':
    unittest.main()