1# Copyright 2016-2021 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import subprocess
16import re
17import json
18import tempfile
19import textwrap
20import os
21import shutil
22import platform
23import pickle
24import zipfile, tarfile
25import sys
26from unittest import mock, SkipTest, skipIf, skipUnless
27from contextlib import contextmanager
28from glob import glob
29from pathlib import (PurePath, Path)
30import typing as T
31
32import mesonbuild.mlog
33import mesonbuild.depfile
34import mesonbuild.dependencies.base
35import mesonbuild.dependencies.factory
36import mesonbuild.envconfig
37import mesonbuild.environment
38import mesonbuild.coredata
39import mesonbuild.modules.gnome
40from mesonbuild.mesonlib import (
41    BuildDirLock, MachineChoice, is_windows, is_osx, is_cygwin, is_dragonflybsd,
42    is_sunos, windows_proof_rmtree, python_command, version_compare, split_args, quote_arg,
43    relpath, is_linux, git, search_version, do_conf_file, do_conf_str, default_prefix,
44    MesonException, EnvironmentException, OptionKey
45)
46
47from mesonbuild.compilers import (
48    GnuCompiler, ClangCompiler, IntelGnuLikeCompiler, VisualStudioCCompiler,
49    VisualStudioCPPCompiler, ClangClCCompiler, ClangClCPPCompiler,
50    detect_static_linker, detect_c_compiler, compiler_from_language,
51    detect_compiler_for
52)
53
54from mesonbuild.dependencies import PkgConfigDependency
55from mesonbuild.build import Target, ConfigurationData, Executable, SharedLibrary, StaticLibrary
56import mesonbuild.modules.pkgconfig
57from mesonbuild.scripts import destdir_join
58
59from mesonbuild.wrap.wrap import PackageDefinition, WrapException
60
61from run_tests import (
62    Backend, exe_suffix, get_fake_env
63)
64
65from .baseplatformtests import BasePlatformTests
66from .helpers import *
67
68@contextmanager
69def temp_filename():
70    '''A context manager which provides a filename to an empty temporary file.
71
72    On exit the file will be deleted.
73    '''
74
75    fd, filename = tempfile.mkstemp()
76    os.close(fd)
77    try:
78        yield filename
79    finally:
80        try:
81            os.remove(filename)
82        except OSError:
83            pass
84
85def _git_init(project_dir):
86    # If a user has git configuration init.defaultBranch set we want to override that
87    with tempfile.TemporaryDirectory() as d:
88        out = git(['--version'], str(d))[1]
89    if version_compare(search_version(out), '>= 2.28'):
90        extra_cmd = ['--initial-branch', 'master']
91    else:
92        extra_cmd = []
93
94    subprocess.check_call(['git', 'init'] + extra_cmd, cwd=project_dir, stdout=subprocess.DEVNULL)
95    subprocess.check_call(['git', 'config',
96                           'user.name', 'Author Person'], cwd=project_dir)
97    subprocess.check_call(['git', 'config',
98                           'user.email', 'teh_coderz@example.com'], cwd=project_dir)
99    _git_add_all(project_dir)
100
101def _git_add_all(project_dir):
102    subprocess.check_call('git add *', cwd=project_dir, shell=True,
103                          stdout=subprocess.DEVNULL)
104    subprocess.check_call(['git', 'commit', '--no-gpg-sign', '-a', '-m', 'I am a project'], cwd=project_dir,
105                          stdout=subprocess.DEVNULL)
106
107class AllPlatformTests(BasePlatformTests):
108    '''
109    Tests that should run on all platforms
110    '''
111
112    def test_default_options_prefix(self):
113        '''
114        Tests that setting a prefix in default_options in project() works.
115        Can't be an ordinary test because we pass --prefix to meson there.
116        https://github.com/mesonbuild/meson/issues/1349
117        '''
118        testdir = os.path.join(self.common_test_dir, '87 default options')
119        self.init(testdir, default_args=False, inprocess=True)
120        opts = self.introspect('--buildoptions')
121        for opt in opts:
122            if opt['name'] == 'prefix':
123                prefix = opt['value']
124                break
125        else:
126            raise self.fail('Did not find option "prefix"')
127        self.assertEqual(prefix, '/absoluteprefix')
128
129    def test_do_conf_file_preserve_newlines(self):
130
131        def conf_file(in_data, confdata):
132            with temp_filename() as fin:
133                with open(fin, 'wb') as fobj:
134                    fobj.write(in_data.encode('utf-8'))
135                with temp_filename() as fout:
136                    do_conf_file(fin, fout, confdata, 'meson')
137                    with open(fout, 'rb') as fobj:
138                        return fobj.read().decode('utf-8')
139
140        confdata = {'VAR': ('foo', 'bar')}
141        self.assertEqual(conf_file('@VAR@\n@VAR@\n', confdata), 'foo\nfoo\n')
142        self.assertEqual(conf_file('@VAR@\r\n@VAR@\r\n', confdata), 'foo\r\nfoo\r\n')
143
144    def test_do_conf_file_by_format(self):
145        def conf_str(in_data, confdata, vformat):
146            (result, missing_variables, confdata_useless) = do_conf_str('configuration_file', in_data, confdata, variable_format = vformat)
147            return '\n'.join(result)
148
149        def check_formats(confdata, result):
150            self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result)
151            self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result)
152            self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result)
153
154        confdata = ConfigurationData()
155        # Key error as they do not exists
156        check_formats(confdata, '/* #undef VAR */\n')
157
158        # Check boolean
159        confdata.values = {'VAR': (False, 'description')}
160        check_formats(confdata, '#undef VAR\n')
161        confdata.values = {'VAR': (True, 'description')}
162        check_formats(confdata, '#define VAR\n')
163
164        # Check string
165        confdata.values = {'VAR': ('value', 'description')}
166        check_formats(confdata, '#define VAR value\n')
167
168        # Check integer
169        confdata.values = {'VAR': (10, 'description')}
170        check_formats(confdata, '#define VAR 10\n')
171
172        # Check multiple string with cmake formats
173        confdata.values = {'VAR': ('value', 'description')}
174        self.assertEqual(conf_str(['#cmakedefine VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value\n')
175        self.assertEqual(conf_str(['#define VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value')
176        self.assertEqual(conf_str(['#cmakedefine VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value\n')
177        self.assertEqual(conf_str(['#define VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value')
178
179        # Handles meson format exceptions
180        #   Unknown format
181        self.assertRaises(MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'unknown_format')
182        #   More than 2 params in mesondefine
183        self.assertRaises(MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'meson')
184        #   Mismatched line with format
185        self.assertRaises(MesonException, conf_str, ['#cmakedefine VAR'], confdata, 'meson')
186        self.assertRaises(MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake')
187        self.assertRaises(MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake@')
188        #   Dict value in confdata
189        confdata.values = {'VAR': (['value'], 'description')}
190        self.assertRaises(MesonException, conf_str, ['#mesondefine VAR'], confdata, 'meson')
191
192    def test_absolute_prefix_libdir(self):
193        '''
194        Tests that setting absolute paths for --prefix and --libdir work. Can't
195        be an ordinary test because these are set via the command-line.
196        https://github.com/mesonbuild/meson/issues/1341
197        https://github.com/mesonbuild/meson/issues/1345
198        '''
199        testdir = os.path.join(self.common_test_dir, '87 default options')
200        # on Windows, /someabs is *not* an absolute path
201        prefix = 'x:/someabs' if is_windows() else '/someabs'
202        libdir = 'libdir'
203        extra_args = ['--prefix=' + prefix,
204                      # This can just be a relative path, but we want to test
205                      # that passing this as an absolute path also works
206                      '--libdir=' + prefix + '/' + libdir]
207        self.init(testdir, extra_args=extra_args, default_args=False)
208        opts = self.introspect('--buildoptions')
209        for opt in opts:
210            if opt['name'] == 'prefix':
211                self.assertEqual(prefix, opt['value'])
212            elif opt['name'] == 'libdir':
213                self.assertEqual(libdir, opt['value'])
214
215    def test_libdir_must_be_inside_prefix(self):
216        '''
217        Tests that libdir is forced to be inside prefix no matter how it is set.
218        Must be a unit test for obvious reasons.
219        '''
220        testdir = os.path.join(self.common_test_dir, '1 trivial')
221        # libdir being inside prefix is ok
222        if is_windows():
223            args = ['--prefix', 'x:/opt', '--libdir', 'x:/opt/lib32']
224        else:
225            args = ['--prefix', '/opt', '--libdir', '/opt/lib32']
226        self.init(testdir, extra_args=args)
227        self.wipe()
228        # libdir not being inside prefix is not ok
229        if is_windows():
230            args = ['--prefix', 'x:/usr', '--libdir', 'x:/opt/lib32']
231        else:
232            args = ['--prefix', '/usr', '--libdir', '/opt/lib32']
233        self.assertRaises(subprocess.CalledProcessError, self.init, testdir, extra_args=args)
234        self.wipe()
235        # libdir must be inside prefix even when set via mesonconf
236        self.init(testdir)
237        if is_windows():
238            self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=x:/opt', False)
239        else:
240            self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt', False)
241
242    def test_prefix_dependent_defaults(self):
243        '''
244        Tests that configured directory paths are set to prefix dependent
245        defaults.
246        '''
247        testdir = os.path.join(self.common_test_dir, '1 trivial')
248        expected = {
249            '/opt': {'prefix': '/opt',
250                     'bindir': 'bin', 'datadir': 'share', 'includedir': 'include',
251                     'infodir': 'share/info',
252                     'libexecdir': 'libexec', 'localedir': 'share/locale',
253                     'localstatedir': 'var', 'mandir': 'share/man',
254                     'sbindir': 'sbin', 'sharedstatedir': 'com',
255                     'sysconfdir': 'etc'},
256            '/usr': {'prefix': '/usr',
257                     'bindir': 'bin', 'datadir': 'share', 'includedir': 'include',
258                     'infodir': 'share/info',
259                     'libexecdir': 'libexec', 'localedir': 'share/locale',
260                     'localstatedir': '/var', 'mandir': 'share/man',
261                     'sbindir': 'sbin', 'sharedstatedir': '/var/lib',
262                     'sysconfdir': '/etc'},
263            '/usr/local': {'prefix': '/usr/local',
264                           'bindir': 'bin', 'datadir': 'share',
265                           'includedir': 'include', 'infodir': 'share/info',
266                           'libexecdir': 'libexec',
267                           'localedir': 'share/locale',
268                           'localstatedir': '/var/local', 'mandir': 'share/man',
269                           'sbindir': 'sbin', 'sharedstatedir': '/var/local/lib',
270                           'sysconfdir': 'etc'},
271            # N.B. We don't check 'libdir' as it's platform dependent, see
272            # default_libdir():
273        }
274
275        if default_prefix() == '/usr/local':
276            expected[None] = expected['/usr/local']
277
278        for prefix in expected:
279            args = []
280            if prefix:
281                args += ['--prefix', prefix]
282            self.init(testdir, extra_args=args, default_args=False)
283            opts = self.introspect('--buildoptions')
284            for opt in opts:
285                name = opt['name']
286                value = opt['value']
287                if name in expected[prefix]:
288                    self.assertEqual(value, expected[prefix][name])
289            self.wipe()
290
291    def test_default_options_prefix_dependent_defaults(self):
292        '''
293        Tests that setting a prefix in default_options in project() sets prefix
294        dependent defaults for other options, and that those defaults can
295        be overridden in default_options or by the command line.
296        '''
297        testdir = os.path.join(self.common_test_dir, '163 default options prefix dependent defaults')
298        expected = {
299            '':
300            {'prefix':         '/usr',
301             'sysconfdir':     '/etc',
302             'localstatedir':  '/var',
303             'sharedstatedir': '/sharedstate'},
304            '--prefix=/usr':
305            {'prefix':         '/usr',
306             'sysconfdir':     '/etc',
307             'localstatedir':  '/var',
308             'sharedstatedir': '/sharedstate'},
309            '--sharedstatedir=/var/state':
310            {'prefix':         '/usr',
311             'sysconfdir':     '/etc',
312             'localstatedir':  '/var',
313             'sharedstatedir': '/var/state'},
314            '--sharedstatedir=/var/state --prefix=/usr --sysconfdir=sysconf':
315            {'prefix':         '/usr',
316             'sysconfdir':     'sysconf',
317             'localstatedir':  '/var',
318             'sharedstatedir': '/var/state'},
319        }
320        for args in expected:
321            self.init(testdir, extra_args=args.split(), default_args=False)
322            opts = self.introspect('--buildoptions')
323            for opt in opts:
324                name = opt['name']
325                value = opt['value']
326                if name in expected[args]:
327                    self.assertEqual(value, expected[args][name])
328            self.wipe()
329
330    def test_clike_get_library_dirs(self):
331        env = get_fake_env()
332        cc = detect_c_compiler(env, MachineChoice.HOST)
333        for d in cc.get_library_dirs(env):
334            self.assertTrue(os.path.exists(d))
335            self.assertTrue(os.path.isdir(d))
336            self.assertTrue(os.path.isabs(d))
337
338    def test_static_library_overwrite(self):
339        '''
340        Tests that static libraries are never appended to, always overwritten.
341        Has to be a unit test because this involves building a project,
342        reconfiguring, and building it again so that `ar` is run twice on the
343        same static library.
344        https://github.com/mesonbuild/meson/issues/1355
345        '''
346        testdir = os.path.join(self.common_test_dir, '3 static')
347        env = get_fake_env(testdir, self.builddir, self.prefix)
348        cc = detect_c_compiler(env, MachineChoice.HOST)
349        static_linker = detect_static_linker(env, cc)
350        if is_windows():
351            raise SkipTest('https://github.com/mesonbuild/meson/issues/1526')
352        if not isinstance(static_linker, mesonbuild.linkers.ArLinker):
353            raise SkipTest('static linker is not `ar`')
354        # Configure
355        self.init(testdir)
356        # Get name of static library
357        targets = self.introspect('--targets')
358        self.assertEqual(len(targets), 1)
359        libname = targets[0]['filename'][0]
360        # Build and get contents of static library
361        self.build()
362        before = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split()
363        # Filter out non-object-file contents
364        before = [f for f in before if f.endswith(('.o', '.obj'))]
365        # Static library should contain only one object
366        self.assertEqual(len(before), 1, msg=before)
367        # Change the source to be built into the static library
368        self.setconf('-Dsource=libfile2.c')
369        self.build()
370        after = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split()
371        # Filter out non-object-file contents
372        after = [f for f in after if f.endswith(('.o', '.obj'))]
373        # Static library should contain only one object
374        self.assertEqual(len(after), 1, msg=after)
375        # and the object must have changed
376        self.assertNotEqual(before, after)
377
378    def test_static_compile_order(self):
379        '''
380        Test that the order of files in a compiler command-line while compiling
381        and linking statically is deterministic. This can't be an ordinary test
382        case because we need to inspect the compiler database.
383        https://github.com/mesonbuild/meson/pull/951
384        '''
385        testdir = os.path.join(self.common_test_dir, '5 linkstatic')
386        self.init(testdir)
387        compdb = self.get_compdb()
388        # Rules will get written out in this order
389        self.assertTrue(compdb[0]['file'].endswith("libfile.c"))
390        self.assertTrue(compdb[1]['file'].endswith("libfile2.c"))
391        self.assertTrue(compdb[2]['file'].endswith("libfile3.c"))
392        self.assertTrue(compdb[3]['file'].endswith("libfile4.c"))
393        # FIXME: We don't have access to the linker command
394
395    def test_run_target_files_path(self):
396        '''
397        Test that run_targets are run from the correct directory
398        https://github.com/mesonbuild/meson/issues/957
399        '''
400        testdir = os.path.join(self.common_test_dir, '51 run target')
401        self.init(testdir)
402        self.run_target('check_exists')
403        self.run_target('check-env')
404        self.run_target('check-env-ct')
405
406    def test_run_target_subdir(self):
407        '''
408        Test that run_targets are run from the correct directory
409        https://github.com/mesonbuild/meson/issues/957
410        '''
411        testdir = os.path.join(self.common_test_dir, '51 run target')
412        self.init(testdir)
413        self.run_target('textprinter')
414
415    def test_install_introspection(self):
416        '''
417        Tests that the Meson introspection API exposes install filenames correctly
418        https://github.com/mesonbuild/meson/issues/829
419        '''
420        if self.backend is not Backend.ninja:
421            raise SkipTest(f'{self.backend.name!r} backend can\'t install files')
422        testdir = os.path.join(self.common_test_dir, '8 install')
423        self.init(testdir)
424        intro = self.introspect('--targets')
425        if intro[0]['type'] == 'executable':
426            intro = intro[::-1]
427        self.assertPathListEqual(intro[0]['install_filename'], ['/usr/lib/libstat.a'])
428        self.assertPathListEqual(intro[1]['install_filename'], ['/usr/bin/prog' + exe_suffix])
429
430    def test_install_subdir_introspection(self):
431        '''
432        Test that the Meson introspection API also contains subdir install information
433        https://github.com/mesonbuild/meson/issues/5556
434        '''
435        testdir = os.path.join(self.common_test_dir, '59 install subdir')
436        self.init(testdir)
437        intro = self.introspect('--installed')
438        expected = {
439            'sub2': 'share/sub2',
440            'subdir/sub1': 'share/sub1',
441            'subdir/sub_elided': 'share',
442            'sub1': 'share/sub1',
443            'sub/sub1': 'share/sub1',
444            'sub_elided': 'share',
445            'nested_elided/sub': 'share',
446            'new_directory': 'share/new_directory',
447        }
448
449        self.assertEqual(len(intro), len(expected))
450
451        # Convert expected to PurePath
452        expected_converted = {PurePath(os.path.join(testdir, key)): PurePath(os.path.join(self.prefix, val)) for key, val in expected.items()}
453        intro_converted = {PurePath(key): PurePath(val) for key, val in intro.items()}
454
455        for src, dst in expected_converted.items():
456            self.assertIn(src, intro_converted)
457            self.assertEqual(dst, intro_converted[src])
458
459    def test_install_introspection_multiple_outputs(self):
460        '''
461        Tests that the Meson introspection API exposes multiple install filenames correctly without crashing
462        https://github.com/mesonbuild/meson/pull/4555
463
464        Reverted to the first file only because of https://github.com/mesonbuild/meson/pull/4547#discussion_r244173438
465        TODO Change the format to a list officially in a followup PR
466        '''
467        if self.backend is not Backend.ninja:
468            raise SkipTest(f'{self.backend.name!r} backend can\'t install files')
469        testdir = os.path.join(self.common_test_dir, '140 custom target multiple outputs')
470        self.init(testdir)
471        intro = self.introspect('--targets')
472        if intro[0]['type'] == 'executable':
473            intro = intro[::-1]
474        self.assertPathListEqual(intro[0]['install_filename'], ['/usr/include/diff.h', '/usr/bin/diff.sh'])
475        self.assertPathListEqual(intro[1]['install_filename'], ['/opt/same.h', '/opt/same.sh'])
476        self.assertPathListEqual(intro[2]['install_filename'], ['/usr/include/first.h', None])
477        self.assertPathListEqual(intro[3]['install_filename'], [None, '/usr/bin/second.sh'])
478
479    def read_install_logs(self):
480        # Find logged files and directories
481        with Path(self.builddir, 'meson-logs', 'install-log.txt').open(encoding='utf-8') as f:
482            return list(map(lambda l: Path(l.strip()),
483                              filter(lambda l: not l.startswith('#'),
484                                     f.readlines())))
485
486    def test_install_log_content(self):
487        '''
488        Tests that the install-log.txt is consistent with the installed files and directories.
489        Specifically checks that the log file only contains one entry per file/directory.
490        https://github.com/mesonbuild/meson/issues/4499
491        '''
492        testdir = os.path.join(self.common_test_dir, '59 install subdir')
493        self.init(testdir)
494        self.install()
495        installpath = Path(self.installdir)
496        # Find installed files and directories
497        expected = {installpath: 0}
498        for name in installpath.rglob('*'):
499            expected[name] = 0
500        logged = self.read_install_logs()
501        for name in logged:
502            self.assertTrue(name in expected, f'Log contains extra entry {name}')
503            expected[name] += 1
504
505        for name, count in expected.items():
506            self.assertGreater(count, 0, f'Log is missing entry for {name}')
507            self.assertLess(count, 2, f'Log has multiple entries for {name}')
508
509        # Verify that with --dry-run we obtain the same logs but with nothing
510        # actually installed
511        windows_proof_rmtree(self.installdir)
512        self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir], workdir=self.builddir)
513        self.assertEqual(logged, self.read_install_logs())
514        self.assertFalse(os.path.exists(self.installdir))
515
516        # If destdir is relative to build directory it should install
517        # exactly the same files.
518        rel_installpath = os.path.relpath(self.installdir, self.builddir)
519        self._run(self.meson_command + ['install', '--dry-run', '--destdir', rel_installpath, '-C', self.builddir])
520        self.assertEqual(logged, self.read_install_logs())
521
522    def test_uninstall(self):
523        exename = os.path.join(self.installdir, 'usr/bin/prog' + exe_suffix)
524        dirname = os.path.join(self.installdir, 'usr/share/dir')
525        testdir = os.path.join(self.common_test_dir, '8 install')
526        self.init(testdir)
527        self.assertPathDoesNotExist(exename)
528        self.install()
529        self.assertPathExists(exename)
530        self.uninstall()
531        self.assertPathDoesNotExist(exename)
532        self.assertPathDoesNotExist(dirname)
533
534    def test_forcefallback(self):
535        testdir = os.path.join(self.unit_test_dir, '31 forcefallback')
536        self.init(testdir, extra_args=['--wrap-mode=forcefallback'])
537        self.build()
538        self.run_tests()
539
540    def test_implicit_forcefallback(self):
541        testdir = os.path.join(self.unit_test_dir, '96 implicit force fallback')
542        with self.assertRaises(subprocess.CalledProcessError):
543            self.init(testdir)
544        self.init(testdir, extra_args=['--wrap-mode=forcefallback'])
545        self.new_builddir()
546        self.init(testdir, extra_args=['--force-fallback-for=something'])
547
548    def test_nopromote(self):
549        testdir = os.path.join(self.common_test_dir, '98 subproject subdir')
550        with self.assertRaises(subprocess.CalledProcessError) as cm:
551            self.init(testdir, extra_args=['--wrap-mode=nopromote'])
552        self.assertIn('dependency subsub found: NO', cm.exception.stdout)
553
554    def test_force_fallback_for(self):
555        testdir = os.path.join(self.unit_test_dir, '31 forcefallback')
556        self.init(testdir, extra_args=['--force-fallback-for=zlib,foo'])
557        self.build()
558        self.run_tests()
559
560    def test_force_fallback_for_nofallback(self):
561        testdir = os.path.join(self.unit_test_dir, '31 forcefallback')
562        self.init(testdir, extra_args=['--force-fallback-for=zlib,foo', '--wrap-mode=nofallback'])
563        self.build()
564        self.run_tests()
565
566    def test_testrepeat(self):
567        testdir = os.path.join(self.common_test_dir, '206 tap tests')
568        self.init(testdir)
569        self.build()
570        self._run(self.mtest_command + ['--repeat=2'])
571
572    def test_testsetups(self):
573        if not shutil.which('valgrind'):
574            raise SkipTest('Valgrind not installed.')
575        testdir = os.path.join(self.unit_test_dir, '2 testsetups')
576        self.init(testdir)
577        self.build()
578        # Run tests without setup
579        self.run_tests()
580        with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f:
581            basic_log = f.read()
582        # Run buggy test with setup that has env that will make it fail
583        self.assertRaises(subprocess.CalledProcessError,
584                          self._run, self.mtest_command + ['--setup=valgrind'])
585        with open(os.path.join(self.logdir, 'testlog-valgrind.txt'), encoding='utf-8') as f:
586            vg_log = f.read()
587        self.assertFalse('TEST_ENV is set' in basic_log)
588        self.assertFalse('Memcheck' in basic_log)
589        self.assertTrue('TEST_ENV is set' in vg_log)
590        self.assertTrue('Memcheck' in vg_log)
591        # Run buggy test with setup without env that will pass
592        self._run(self.mtest_command + ['--setup=wrapper'])
593        # Setup with no properties works
594        self._run(self.mtest_command + ['--setup=empty'])
595        # Setup with only env works
596        self._run(self.mtest_command + ['--setup=onlyenv'])
597        self._run(self.mtest_command + ['--setup=onlyenv2'])
598        self._run(self.mtest_command + ['--setup=onlyenv3'])
599        # Setup with only a timeout works
600        self._run(self.mtest_command + ['--setup=timeout'])
601        # Setup that does not define a wrapper works with --wrapper
602        self._run(self.mtest_command + ['--setup=timeout', '--wrapper', shutil.which('valgrind')])
603        # Setup that skips test works
604        self._run(self.mtest_command + ['--setup=good'])
605        with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f:
606            exclude_suites_log = f.read()
607        self.assertFalse('buggy' in exclude_suites_log)
608        # --suite overrides add_test_setup(xclude_suites)
609        self._run(self.mtest_command + ['--setup=good', '--suite', 'buggy'])
610        with open(os.path.join(self.logdir, 'testlog-good.txt'), encoding='utf-8') as f:
611            include_suites_log = f.read()
612        self.assertTrue('buggy' in include_suites_log)
613
614    def test_testsetup_selection(self):
615        testdir = os.path.join(self.unit_test_dir, '14 testsetup selection')
616        self.init(testdir)
617        self.build()
618
619        # Run tests without setup
620        self.run_tests()
621
622        self.assertRaises(subprocess.CalledProcessError, self._run, self.mtest_command + ['--setup=missingfromfoo'])
623        self._run(self.mtest_command + ['--setup=missingfromfoo', '--no-suite=foo:'])
624
625        self._run(self.mtest_command + ['--setup=worksforall'])
626        self._run(self.mtest_command + ['--setup=main:worksforall'])
627
628        self.assertRaises(subprocess.CalledProcessError, self._run,
629                          self.mtest_command + ['--setup=onlyinbar'])
630        self.assertRaises(subprocess.CalledProcessError, self._run,
631                          self.mtest_command + ['--setup=onlyinbar', '--no-suite=main:'])
632        self._run(self.mtest_command + ['--setup=onlyinbar', '--no-suite=main:', '--no-suite=foo:'])
633        self._run(self.mtest_command + ['--setup=bar:onlyinbar'])
634        self.assertRaises(subprocess.CalledProcessError, self._run,
635                          self.mtest_command + ['--setup=foo:onlyinbar'])
636        self.assertRaises(subprocess.CalledProcessError, self._run,
637                          self.mtest_command + ['--setup=main:onlyinbar'])
638
639    def test_testsetup_default(self):
640        testdir = os.path.join(self.unit_test_dir, '49 testsetup default')
641        self.init(testdir)
642        self.build()
643
644        # Run tests without --setup will cause the default setup to be used
645        self.run_tests()
646        with open(os.path.join(self.logdir, 'testlog.txt'), encoding='utf-8') as f:
647            default_log = f.read()
648
649        # Run tests with explicitly using the same setup that is set as default
650        self._run(self.mtest_command + ['--setup=mydefault'])
651        with open(os.path.join(self.logdir, 'testlog-mydefault.txt'), encoding='utf-8') as f:
652            mydefault_log = f.read()
653
654        # Run tests with another setup
655        self._run(self.mtest_command + ['--setup=other'])
656        with open(os.path.join(self.logdir, 'testlog-other.txt'), encoding='utf-8') as f:
657            other_log = f.read()
658
659        self.assertTrue('ENV_A is 1' in default_log)
660        self.assertTrue('ENV_B is 2' in default_log)
661        self.assertTrue('ENV_C is 2' in default_log)
662
663        self.assertTrue('ENV_A is 1' in mydefault_log)
664        self.assertTrue('ENV_B is 2' in mydefault_log)
665        self.assertTrue('ENV_C is 2' in mydefault_log)
666
667        self.assertTrue('ENV_A is 1' in other_log)
668        self.assertTrue('ENV_B is 3' in other_log)
669        self.assertTrue('ENV_C is 2' in other_log)
670
671    def assertFailedTestCount(self, failure_count, command):
672        try:
673            self._run(command)
674            self.assertEqual(0, failure_count, 'Expected %d tests to fail.' % failure_count)
675        except subprocess.CalledProcessError as e:
676            self.assertEqual(e.returncode, failure_count)
677
678    def test_suite_selection(self):
679        testdir = os.path.join(self.unit_test_dir, '4 suite selection')
680        self.init(testdir)
681        self.build()
682
683        self.assertFailedTestCount(4, self.mtest_command)
684
685        self.assertFailedTestCount(0, self.mtest_command + ['--suite', ':success'])
686        self.assertFailedTestCount(3, self.mtest_command + ['--suite', ':fail'])
687        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', ':success'])
688        self.assertFailedTestCount(1, self.mtest_command + ['--no-suite', ':fail'])
689
690        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'mainprj'])
691        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc'])
692        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail'])
693        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjmix'])
694        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'mainprj'])
695        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc'])
696        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjfail'])
697        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjmix'])
698
699        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'mainprj:fail'])
700        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'mainprj:success'])
701        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'mainprj:fail'])
702        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'mainprj:success'])
703
704        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail:fail'])
705        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjfail:success'])
706        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjfail:fail'])
707        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjfail:success'])
708
709        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc:fail'])
710        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc:success'])
711        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc:fail'])
712        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc:success'])
713
714        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjmix:fail'])
715        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjmix:success'])
716        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjmix:fail'])
717        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjmix:success'])
718
719        self.assertFailedTestCount(2, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix:fail'])
720        self.assertFailedTestCount(3, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj'])
721        self.assertFailedTestCount(2, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj', '--no-suite', 'subprjmix:fail'])
722        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj', '--no-suite', 'subprjmix:fail', 'mainprj-failing_test'])
723
724        self.assertFailedTestCount(2, self.mtest_command + ['--no-suite', 'subprjfail:fail', '--no-suite', 'subprjmix:fail'])
725
726    def test_build_by_default(self):
727        testdir = os.path.join(self.common_test_dir, '129 build by default')
728        self.init(testdir)
729        self.build()
730        genfile1 = os.path.join(self.builddir, 'generated1.dat')
731        genfile2 = os.path.join(self.builddir, 'generated2.dat')
732        exe1 = os.path.join(self.builddir, 'fooprog' + exe_suffix)
733        exe2 = os.path.join(self.builddir, 'barprog' + exe_suffix)
734        self.assertPathExists(genfile1)
735        self.assertPathExists(genfile2)
736        self.assertPathDoesNotExist(exe1)
737        self.assertPathDoesNotExist(exe2)
738        self.build(target=('fooprog' + exe_suffix))
739        self.assertPathExists(exe1)
740        self.build(target=('barprog' + exe_suffix))
741        self.assertPathExists(exe2)
742
743    def test_internal_include_order(self):
744        if mesonbuild.environment.detect_msys2_arch() and ('MESON_RSP_THRESHOLD' in os.environ):
745            raise SkipTest('Test does not yet support gcc rsp files on msys2')
746
747        testdir = os.path.join(self.common_test_dir, '130 include order')
748        self.init(testdir)
749        execmd = fxecmd = None
750        for cmd in self.get_compdb():
751            if 'someexe' in cmd['command']:
752                execmd = cmd['command']
753                continue
754            if 'somefxe' in cmd['command']:
755                fxecmd = cmd['command']
756                continue
757        if not execmd or not fxecmd:
758            raise Exception('Could not find someexe and somfxe commands')
759        # Check include order for 'someexe'
760        incs = [a for a in split_args(execmd) if a.startswith("-I")]
761        self.assertEqual(len(incs), 9)
762        # Need to run the build so the private dir is created.
763        self.build()
764        pdirs = glob(os.path.join(self.builddir, 'sub4/someexe*.p'))
765        self.assertEqual(len(pdirs), 1)
766        privdir = pdirs[0][len(self.builddir)+1:]
767        self.assertPathEqual(incs[0], "-I" + privdir)
768        # target build subdir
769        self.assertPathEqual(incs[1], "-Isub4")
770        # target source subdir
771        self.assertPathBasenameEqual(incs[2], 'sub4')
772        # include paths added via per-target c_args: ['-I'...]
773        self.assertPathBasenameEqual(incs[3], 'sub3')
774        # target include_directories: build dir
775        self.assertPathEqual(incs[4], "-Isub2")
776        # target include_directories: source dir
777        self.assertPathBasenameEqual(incs[5], 'sub2')
778        # target internal dependency include_directories: build dir
779        self.assertPathEqual(incs[6], "-Isub1")
780        # target internal dependency include_directories: source dir
781        self.assertPathBasenameEqual(incs[7], 'sub1')
782        # custom target include dir
783        self.assertPathEqual(incs[8], '-Ictsub')
784        # Check include order for 'somefxe'
785        incs = [a for a in split_args(fxecmd) if a.startswith('-I')]
786        self.assertEqual(len(incs), 9)
787        # target private dir
788        pdirs = glob(os.path.join(self.builddir, 'somefxe*.p'))
789        self.assertEqual(len(pdirs), 1)
790        privdir = pdirs[0][len(self.builddir)+1:]
791        self.assertPathEqual(incs[0], '-I' + privdir)
792        # target build dir
793        self.assertPathEqual(incs[1], '-I.')
794        # target source dir
795        self.assertPathBasenameEqual(incs[2], os.path.basename(testdir))
796        # target internal dependency correct include_directories: build dir
797        self.assertPathEqual(incs[3], "-Isub4")
798        # target internal dependency correct include_directories: source dir
799        self.assertPathBasenameEqual(incs[4], 'sub4')
800        # target internal dependency dep include_directories: build dir
801        self.assertPathEqual(incs[5], "-Isub1")
802        # target internal dependency dep include_directories: source dir
803        self.assertPathBasenameEqual(incs[6], 'sub1')
804        # target internal dependency wrong include_directories: build dir
805        self.assertPathEqual(incs[7], "-Isub2")
806        # target internal dependency wrong include_directories: source dir
807        self.assertPathBasenameEqual(incs[8], 'sub2')
808
809    def test_compiler_detection(self):
810        '''
811        Test that automatic compiler detection and setting from the environment
812        both work just fine. This is needed because while running project tests
813        and other unit tests, we always read CC/CXX/etc from the environment.
814        '''
815        gnu = GnuCompiler
816        clang = ClangCompiler
817        intel = IntelGnuLikeCompiler
818        msvc = (VisualStudioCCompiler, VisualStudioCPPCompiler)
819        clangcl = (ClangClCCompiler, ClangClCPPCompiler)
820        ar = mesonbuild.linkers.ArLinker
821        lib = mesonbuild.linkers.VisualStudioLinker
822        langs = [('c', 'CC'), ('cpp', 'CXX')]
823        if not is_windows() and platform.machine().lower() != 'e2k':
824            langs += [('objc', 'OBJC'), ('objcpp', 'OBJCXX')]
825        testdir = os.path.join(self.unit_test_dir, '5 compiler detection')
826        env = get_fake_env(testdir, self.builddir, self.prefix)
827        for lang, evar in langs:
828            # Detect with evar and do sanity checks on that
829            if evar in os.environ:
830                ecc = compiler_from_language(env, lang, MachineChoice.HOST)
831                self.assertTrue(ecc.version)
832                elinker = detect_static_linker(env, ecc)
833                # Pop it so we don't use it for the next detection
834                evalue = os.environ.pop(evar)
835                # Very rough/strict heuristics. Would never work for actual
836                # compiler detection, but should be ok for the tests.
837                ebase = os.path.basename(evalue)
838                if ebase.startswith('g') or ebase.endswith(('-gcc', '-g++')):
839                    self.assertIsInstance(ecc, gnu)
840                    self.assertIsInstance(elinker, ar)
841                elif 'clang-cl' in ebase:
842                    self.assertIsInstance(ecc, clangcl)
843                    self.assertIsInstance(elinker, lib)
844                elif 'clang' in ebase:
845                    self.assertIsInstance(ecc, clang)
846                    self.assertIsInstance(elinker, ar)
847                elif ebase.startswith('ic'):
848                    self.assertIsInstance(ecc, intel)
849                    self.assertIsInstance(elinker, ar)
850                elif ebase.startswith('cl'):
851                    self.assertIsInstance(ecc, msvc)
852                    self.assertIsInstance(elinker, lib)
853                else:
854                    raise AssertionError(f'Unknown compiler {evalue!r}')
855                # Check that we actually used the evalue correctly as the compiler
856                self.assertEqual(ecc.get_exelist(), split_args(evalue))
857            # Do auto-detection of compiler based on platform, PATH, etc.
858            cc = compiler_from_language(env, lang, MachineChoice.HOST)
859            self.assertTrue(cc.version)
860            linker = detect_static_linker(env, cc)
861            # Check compiler type
862            if isinstance(cc, gnu):
863                self.assertIsInstance(linker, ar)
864                if is_osx():
865                    self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
866                elif is_sunos():
867                    self.assertIsInstance(cc.linker, (mesonbuild.linkers.SolarisDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin))
868                else:
869                    self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)
870            if isinstance(cc, clangcl):
871                self.assertIsInstance(linker, lib)
872                self.assertIsInstance(cc.linker, mesonbuild.linkers.ClangClDynamicLinker)
873            if isinstance(cc, clang):
874                self.assertIsInstance(linker, ar)
875                if is_osx():
876                    self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
877                elif is_windows():
878                    # This is clang, not clang-cl. This can be either an
879                    # ld-like linker of link.exe-like linker (usually the
880                    # former for msys2, the latter otherwise)
881                    self.assertIsInstance(cc.linker, (mesonbuild.linkers.MSVCDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin))
882                else:
883                    self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)
884            if isinstance(cc, intel):
885                self.assertIsInstance(linker, ar)
886                if is_osx():
887                    self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
888                elif is_windows():
889                    self.assertIsInstance(cc.linker, mesonbuild.linkers.XilinkDynamicLinker)
890                else:
891                    self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuDynamicLinker)
892            if isinstance(cc, msvc):
893                self.assertTrue(is_windows())
894                self.assertIsInstance(linker, lib)
895                self.assertEqual(cc.id, 'msvc')
896                self.assertTrue(hasattr(cc, 'is_64'))
897                self.assertIsInstance(cc.linker, mesonbuild.linkers.MSVCDynamicLinker)
898                # If we're on Windows CI, we know what the compiler will be
899                if 'arch' in os.environ:
900                    if os.environ['arch'] == 'x64':
901                        self.assertTrue(cc.is_64)
902                    else:
903                        self.assertFalse(cc.is_64)
904            # Set evar ourselves to a wrapper script that just calls the same
905            # exelist + some argument. This is meant to test that setting
906            # something like `ccache gcc -pipe` or `distcc ccache gcc` works.
907            wrapper = os.path.join(testdir, 'compiler wrapper.py')
908            wrappercc = python_command + [wrapper] + cc.get_exelist() + ['-DSOME_ARG']
909            os.environ[evar] = ' '.join(quote_arg(w) for w in wrappercc)
910
911            # Check static linker too
912            wrapperlinker = python_command + [wrapper] + linker.get_exelist() + linker.get_always_args()
913            os.environ['AR'] = ' '.join(quote_arg(w) for w in wrapperlinker)
914
915            # Need a new env to re-run environment loading
916            env = get_fake_env(testdir, self.builddir, self.prefix)
917
918            wcc = compiler_from_language(env, lang, MachineChoice.HOST)
919            wlinker = detect_static_linker(env, wcc)
920            # Pop it so we don't use it for the next detection
921            evalue = os.environ.pop('AR')
922            # Must be the same type since it's a wrapper around the same exelist
923            self.assertIs(type(cc), type(wcc))
924            self.assertIs(type(linker), type(wlinker))
925            # Ensure that the exelist is correct
926            self.assertEqual(wcc.get_exelist(), wrappercc)
927            self.assertEqual(wlinker.get_exelist(), wrapperlinker)
928            # Ensure that the version detection worked correctly
929            self.assertEqual(cc.version, wcc.version)
930            if hasattr(cc, 'is_64'):
931                self.assertEqual(cc.is_64, wcc.is_64)
932
933    def test_always_prefer_c_compiler_for_asm(self):
934        testdir = os.path.join(self.common_test_dir, '133 c cpp and asm')
935        # Skip if building with MSVC
936        env = get_fake_env(testdir, self.builddir, self.prefix)
937        if detect_c_compiler(env, MachineChoice.HOST).get_id() == 'msvc':
938            raise SkipTest('MSVC can\'t compile assembly')
939        self.init(testdir)
940        commands = {'c-asm': {}, 'cpp-asm': {}, 'cpp-c-asm': {}, 'c-cpp-asm': {}}
941        for cmd in self.get_compdb():
942            # Get compiler
943            split = split_args(cmd['command'])
944            if split[0] == 'ccache':
945                compiler = split[1]
946            else:
947                compiler = split[0]
948            # Classify commands
949            if 'Ic-asm' in cmd['command']:
950                if cmd['file'].endswith('.S'):
951                    commands['c-asm']['asm'] = compiler
952                elif cmd['file'].endswith('.c'):
953                    commands['c-asm']['c'] = compiler
954                else:
955                    raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command']))
956            elif 'Icpp-asm' in cmd['command']:
957                if cmd['file'].endswith('.S'):
958                    commands['cpp-asm']['asm'] = compiler
959                elif cmd['file'].endswith('.cpp'):
960                    commands['cpp-asm']['cpp'] = compiler
961                else:
962                    raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command']))
963            elif 'Ic-cpp-asm' in cmd['command']:
964                if cmd['file'].endswith('.S'):
965                    commands['c-cpp-asm']['asm'] = compiler
966                elif cmd['file'].endswith('.c'):
967                    commands['c-cpp-asm']['c'] = compiler
968                elif cmd['file'].endswith('.cpp'):
969                    commands['c-cpp-asm']['cpp'] = compiler
970                else:
971                    raise AssertionError('{!r} found in c-cpp-asm?'.format(cmd['command']))
972            elif 'Icpp-c-asm' in cmd['command']:
973                if cmd['file'].endswith('.S'):
974                    commands['cpp-c-asm']['asm'] = compiler
975                elif cmd['file'].endswith('.c'):
976                    commands['cpp-c-asm']['c'] = compiler
977                elif cmd['file'].endswith('.cpp'):
978                    commands['cpp-c-asm']['cpp'] = compiler
979                else:
980                    raise AssertionError('{!r} found in cpp-c-asm?'.format(cmd['command']))
981            else:
982                raise AssertionError('Unknown command {!r} found'.format(cmd['command']))
983        # Check that .S files are always built with the C compiler
984        self.assertEqual(commands['c-asm']['asm'], commands['c-asm']['c'])
985        self.assertEqual(commands['c-asm']['asm'], commands['cpp-asm']['asm'])
986        self.assertEqual(commands['cpp-asm']['asm'], commands['c-cpp-asm']['c'])
987        self.assertEqual(commands['c-cpp-asm']['asm'], commands['c-cpp-asm']['c'])
988        self.assertEqual(commands['cpp-c-asm']['asm'], commands['cpp-c-asm']['c'])
989        self.assertNotEqual(commands['cpp-asm']['asm'], commands['cpp-asm']['cpp'])
990        self.assertNotEqual(commands['c-cpp-asm']['c'], commands['c-cpp-asm']['cpp'])
991        self.assertNotEqual(commands['cpp-c-asm']['c'], commands['cpp-c-asm']['cpp'])
992        # Check that the c-asm target is always linked with the C linker
993        build_ninja = os.path.join(self.builddir, 'build.ninja')
994        with open(build_ninja, encoding='utf-8') as f:
995            contents = f.read()
996            m = re.search('build c-asm.*: c_LINKER', contents)
997        self.assertIsNotNone(m, msg=contents)
998
999    def test_preprocessor_checks_CPPFLAGS(self):
1000        '''
1001        Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS but
1002        not LDFLAGS.
1003        '''
1004        testdir = os.path.join(self.common_test_dir, '132 get define')
1005        define = 'MESON_TEST_DEFINE_VALUE'
1006        # NOTE: this list can't have \n, ' or "
1007        # \n is never substituted by the GNU pre-processor via a -D define
1008        # ' and " confuse split_args() even when they are escaped
1009        # % and # confuse the MSVC preprocessor
1010        # !, ^, *, and < confuse lcc preprocessor
1011        value = 'spaces and fun@$&()-=_+{}[]:;>?,./~`'
1012        for env_var in ['CPPFLAGS', 'CFLAGS']:
1013            env = {}
1014            env[env_var] = f'-D{define}="{value}"'
1015            env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read'
1016            self.init(testdir, extra_args=[f'-D{define}={value}'], override_envvars=env)
1017
1018    def test_custom_target_exe_data_deterministic(self):
1019        testdir = os.path.join(self.common_test_dir, '109 custom target capture')
1020        self.init(testdir)
1021        meson_exe_dat1 = glob(os.path.join(self.privatedir, 'meson_exe*.dat'))
1022        self.wipe()
1023        self.init(testdir)
1024        meson_exe_dat2 = glob(os.path.join(self.privatedir, 'meson_exe*.dat'))
1025        self.assertListEqual(meson_exe_dat1, meson_exe_dat2)
1026
1027    def test_noop_changes_cause_no_rebuilds(self):
1028        '''
1029        Test that no-op changes to the build files such as mtime do not cause
1030        a rebuild of anything.
1031        '''
1032        testdir = os.path.join(self.common_test_dir, '6 linkshared')
1033        self.init(testdir)
1034        self.build()
1035        # Immediately rebuilding should not do anything
1036        self.assertBuildIsNoop()
1037        # Changing mtime of meson.build should not rebuild anything
1038        self.utime(os.path.join(testdir, 'meson.build'))
1039        self.assertReconfiguredBuildIsNoop()
1040        # Changing mtime of libefile.c should rebuild the library, but not relink the executable
1041        self.utime(os.path.join(testdir, 'libfile.c'))
1042        self.assertBuildRelinkedOnlyTarget('mylib')
1043
1044    def test_source_changes_cause_rebuild(self):
1045        '''
1046        Test that changes to sources and headers cause rebuilds, but not
1047        changes to unused files (as determined by the dependency file) in the
1048        input files list.
1049        '''
1050        testdir = os.path.join(self.common_test_dir, '19 header in file list')
1051        self.init(testdir)
1052        self.build()
1053        # Immediately rebuilding should not do anything
1054        self.assertBuildIsNoop()
1055        # Changing mtime of header.h should rebuild everything
1056        self.utime(os.path.join(testdir, 'header.h'))
1057        self.assertBuildRelinkedOnlyTarget('prog')
1058
1059    def test_custom_target_changes_cause_rebuild(self):
1060        '''
1061        Test that in a custom target, changes to the input files, the
1062        ExternalProgram, and any File objects on the command-line cause
1063        a rebuild.
1064        '''
1065        testdir = os.path.join(self.common_test_dir, '57 custom header generator')
1066        self.init(testdir)
1067        self.build()
1068        # Immediately rebuilding should not do anything
1069        self.assertBuildIsNoop()
1070        # Changing mtime of these should rebuild everything
1071        for f in ('input.def', 'makeheader.py', 'somefile.txt'):
1072            self.utime(os.path.join(testdir, f))
1073            self.assertBuildRelinkedOnlyTarget('prog')
1074
1075    def test_source_generator_program_cause_rebuild(self):
1076        '''
1077        Test that changes to generator programs in the source tree cause
1078        a rebuild.
1079        '''
1080        testdir = os.path.join(self.common_test_dir, '90 gen extra')
1081        self.init(testdir)
1082        self.build()
1083        # Immediately rebuilding should not do anything
1084        self.assertBuildIsNoop()
1085        # Changing mtime of generator should rebuild the executable
1086        self.utime(os.path.join(testdir, 'srcgen.py'))
1087        self.assertRebuiltTarget('basic')
1088
1089    def test_static_library_lto(self):
1090        '''
1091        Test that static libraries can be built with LTO and linked to
1092        executables. On Linux, this requires the use of gcc-ar.
1093        https://github.com/mesonbuild/meson/issues/1646
1094        '''
1095        testdir = os.path.join(self.common_test_dir, '5 linkstatic')
1096
1097        env = get_fake_env(testdir, self.builddir, self.prefix)
1098        if detect_c_compiler(env, MachineChoice.HOST).get_id() == 'clang' and is_windows():
1099            raise SkipTest('LTO not (yet) supported by windows clang')
1100
1101        self.init(testdir, extra_args='-Db_lto=true')
1102        self.build()
1103        self.run_tests()
1104
1105    @skip_if_not_base_option('b_lto_threads')
1106    def test_lto_threads(self):
1107        testdir = os.path.join(self.common_test_dir, '6 linkshared')
1108
1109        env = get_fake_env(testdir, self.builddir, self.prefix)
1110        cc = detect_c_compiler(env, MachineChoice.HOST)
1111        extra_args: T.List[str] = []
1112        if cc.get_id() == 'clang':
1113            if is_windows():
1114                raise SkipTest('LTO not (yet) supported by windows clang')
1115
1116        self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_threads=8'] + extra_args)
1117        self.build()
1118        self.run_tests()
1119
1120        expected = set(cc.get_lto_compile_args(threads=8))
1121        targets = self.introspect('--targets')
1122        # This assumes all of the targets support lto
1123        for t in targets:
1124            for s in t['target_sources']:
1125                for e in expected:
1126                    self.assertIn(e, s['parameters'])
1127
1128    @skip_if_not_base_option('b_lto_mode')
1129    @skip_if_not_base_option('b_lto_threads')
1130    def test_lto_mode(self):
1131        testdir = os.path.join(self.common_test_dir, '6 linkshared')
1132
1133        env = get_fake_env(testdir, self.builddir, self.prefix)
1134        cc = detect_c_compiler(env, MachineChoice.HOST)
1135        if cc.get_id() != 'clang':
1136            raise SkipTest('Only clang currently supports thinLTO')
1137        if cc.linker.id not in {'ld.lld', 'ld.gold', 'ld64', 'lld-link'}:
1138            raise SkipTest('thinLTO requires ld.lld, ld.gold, ld64, or lld-link')
1139        elif is_windows():
1140            raise SkipTest('LTO not (yet) supported by windows clang')
1141
1142        self.init(testdir, extra_args=['-Db_lto=true', '-Db_lto_mode=thin', '-Db_lto_threads=8', '-Dc_args=-Werror=unused-command-line-argument'])
1143        self.build()
1144        self.run_tests()
1145
1146        expected = set(cc.get_lto_compile_args(threads=8, mode='thin'))
1147        targets = self.introspect('--targets')
1148        # This assumes all of the targets support lto
1149        for t in targets:
1150            for s in t['target_sources']:
1151                self.assertTrue(expected.issubset(set(s['parameters'])), f'Incorrect values for {t["name"]}')
1152
1153    def test_dist_git(self):
1154        if not shutil.which('git'):
1155            raise SkipTest('Git not found')
1156        if self.backend is not Backend.ninja:
1157            raise SkipTest('Dist is only supported with Ninja')
1158
1159        try:
1160            self.dist_impl(_git_init, _git_add_all)
1161        except PermissionError:
1162            # When run under Windows CI, something (virus scanner?)
1163            # holds on to the git files so cleaning up the dir
1164            # fails sometimes.
1165            pass
1166
1167    def has_working_hg(self):
1168        if not shutil.which('hg'):
1169            return False
1170        try:
1171            # This check should not be necessary, but
1172            # CI under macOS passes the above test even
1173            # though Mercurial is not installed.
1174            if subprocess.call(['hg', '--version'],
1175                               stdout=subprocess.DEVNULL,
1176                               stderr=subprocess.DEVNULL) != 0:
1177                return False
1178            return True
1179        except FileNotFoundError:
1180            return False
1181
1182    def test_dist_hg(self):
1183        if not self.has_working_hg():
1184            raise SkipTest('Mercurial not found or broken.')
1185        if self.backend is not Backend.ninja:
1186            raise SkipTest('Dist is only supported with Ninja')
1187
1188        def hg_init(project_dir):
1189            subprocess.check_call(['hg', 'init'], cwd=project_dir)
1190            with open(os.path.join(project_dir, '.hg', 'hgrc'), 'w', encoding='utf-8') as f:
1191                print('[ui]', file=f)
1192                print('username=Author Person <teh_coderz@example.com>', file=f)
1193            subprocess.check_call(['hg', 'add', 'meson.build', 'distexe.c'], cwd=project_dir)
1194            subprocess.check_call(['hg', 'commit', '-m', 'I am a project'], cwd=project_dir)
1195
1196        try:
1197            self.dist_impl(hg_init, include_subprojects=False)
1198        except PermissionError:
1199            # When run under Windows CI, something (virus scanner?)
1200            # holds on to the hg files so cleaning up the dir
1201            # fails sometimes.
1202            pass
1203
1204    def test_dist_git_script(self):
1205        if not shutil.which('git'):
1206            raise SkipTest('Git not found')
1207        if self.backend is not Backend.ninja:
1208            raise SkipTest('Dist is only supported with Ninja')
1209
1210        try:
1211            with tempfile.TemporaryDirectory() as tmpdir:
1212                project_dir = os.path.join(tmpdir, 'a')
1213                shutil.copytree(os.path.join(self.unit_test_dir, '35 dist script'),
1214                                project_dir)
1215                _git_init(project_dir)
1216                self.init(project_dir)
1217                self.build('dist')
1218
1219                self.new_builddir()
1220                self.init(project_dir, extra_args=['-Dsub:broken_dist_script=false'])
1221                self._run(self.meson_command + ['dist', '--include-subprojects'], workdir=self.builddir)
1222        except PermissionError:
1223            # When run under Windows CI, something (virus scanner?)
1224            # holds on to the git files so cleaning up the dir
1225            # fails sometimes.
1226            pass
1227
1228    def create_dummy_subproject(self, project_dir, name):
1229        path = os.path.join(project_dir, 'subprojects', name)
1230        os.makedirs(path)
1231        with open(os.path.join(path, 'meson.build'), 'w', encoding='utf-8') as ofile:
1232            ofile.write(f"project('{name}', version: '1.0')")
1233        return path
1234
1235    def dist_impl(self, vcs_init, vcs_add_all=None, include_subprojects=True):
1236        # Create this on the fly because having rogue .git directories inside
1237        # the source tree leads to all kinds of trouble.
1238        with tempfile.TemporaryDirectory() as project_dir:
1239            with open(os.path.join(project_dir, 'meson.build'), 'w', encoding='utf-8') as ofile:
1240                ofile.write(textwrap.dedent('''\
1241                    project('disttest', 'c', version : '1.4.3')
1242                    e = executable('distexe', 'distexe.c')
1243                    test('dist test', e)
1244                    subproject('vcssub', required : false)
1245                    subproject('tarballsub', required : false)
1246                    subproject('samerepo', required : false)
1247                    '''))
1248            with open(os.path.join(project_dir, 'distexe.c'), 'w', encoding='utf-8') as ofile:
1249                ofile.write(textwrap.dedent('''\
1250                    #include<stdio.h>
1251
1252                    int main(int argc, char **argv) {
1253                        printf("I am a distribution test.\\n");
1254                        return 0;
1255                    }
1256                    '''))
1257            xz_distfile = os.path.join(self.distdir, 'disttest-1.4.3.tar.xz')
1258            xz_checksumfile = xz_distfile + '.sha256sum'
1259            gz_distfile = os.path.join(self.distdir, 'disttest-1.4.3.tar.gz')
1260            gz_checksumfile = gz_distfile + '.sha256sum'
1261            zip_distfile = os.path.join(self.distdir, 'disttest-1.4.3.zip')
1262            zip_checksumfile = zip_distfile + '.sha256sum'
1263            vcs_init(project_dir)
1264            if include_subprojects:
1265                vcs_init(self.create_dummy_subproject(project_dir, 'vcssub'))
1266                self.create_dummy_subproject(project_dir, 'tarballsub')
1267                self.create_dummy_subproject(project_dir, 'unusedsub')
1268            if vcs_add_all:
1269                vcs_add_all(self.create_dummy_subproject(project_dir, 'samerepo'))
1270            self.init(project_dir)
1271            self.build('dist')
1272            self.assertPathExists(xz_distfile)
1273            self.assertPathExists(xz_checksumfile)
1274            self.assertPathDoesNotExist(gz_distfile)
1275            self.assertPathDoesNotExist(gz_checksumfile)
1276            self.assertPathDoesNotExist(zip_distfile)
1277            self.assertPathDoesNotExist(zip_checksumfile)
1278            self._run(self.meson_command + ['dist', '--formats', 'gztar'],
1279                      workdir=self.builddir)
1280            self.assertPathExists(gz_distfile)
1281            self.assertPathExists(gz_checksumfile)
1282            self._run(self.meson_command + ['dist', '--formats', 'zip'],
1283                      workdir=self.builddir)
1284            self.assertPathExists(zip_distfile)
1285            self.assertPathExists(zip_checksumfile)
1286            os.remove(xz_distfile)
1287            os.remove(xz_checksumfile)
1288            os.remove(gz_distfile)
1289            os.remove(gz_checksumfile)
1290            os.remove(zip_distfile)
1291            os.remove(zip_checksumfile)
1292            self._run(self.meson_command + ['dist', '--formats', 'xztar,gztar,zip'],
1293                      workdir=self.builddir)
1294            self.assertPathExists(xz_distfile)
1295            self.assertPathExists(xz_checksumfile)
1296            self.assertPathExists(gz_distfile)
1297            self.assertPathExists(gz_checksumfile)
1298            self.assertPathExists(zip_distfile)
1299            self.assertPathExists(zip_checksumfile)
1300
1301            if include_subprojects:
1302                # Verify that without --include-subprojects we have files from
1303                # the main project and also files from subprojects part of the
1304                # main vcs repository.
1305                z = zipfile.ZipFile(zip_distfile)
1306                expected = ['disttest-1.4.3/',
1307                            'disttest-1.4.3/meson.build',
1308                            'disttest-1.4.3/distexe.c']
1309                if vcs_add_all:
1310                    expected += ['disttest-1.4.3/subprojects/',
1311                                 'disttest-1.4.3/subprojects/samerepo/',
1312                                 'disttest-1.4.3/subprojects/samerepo/meson.build']
1313                self.assertEqual(sorted(expected),
1314                                 sorted(z.namelist()))
1315                # Verify that with --include-subprojects we now also have files
1316                # from tarball and separate vcs subprojects. But not files from
1317                # unused subprojects.
1318                self._run(self.meson_command + ['dist', '--formats', 'zip', '--include-subprojects'],
1319                          workdir=self.builddir)
1320                z = zipfile.ZipFile(zip_distfile)
1321                expected += ['disttest-1.4.3/subprojects/tarballsub/',
1322                             'disttest-1.4.3/subprojects/tarballsub/meson.build',
1323                             'disttest-1.4.3/subprojects/vcssub/',
1324                             'disttest-1.4.3/subprojects/vcssub/meson.build']
1325                self.assertEqual(sorted(expected),
1326                                 sorted(z.namelist()))
1327            if vcs_add_all:
1328                # Verify we can distribute separately subprojects in the same vcs
1329                # repository as the main project.
1330                subproject_dir = os.path.join(project_dir, 'subprojects', 'samerepo')
1331                self.new_builddir()
1332                self.init(subproject_dir)
1333                self.build('dist')
1334                xz_distfile = os.path.join(self.distdir, 'samerepo-1.0.tar.xz')
1335                xz_checksumfile = xz_distfile + '.sha256sum'
1336                self.assertPathExists(xz_distfile)
1337                self.assertPathExists(xz_checksumfile)
1338                tar = tarfile.open(xz_distfile, "r:xz")  # [ignore encoding]
1339                self.assertEqual(sorted(['samerepo-1.0',
1340                                         'samerepo-1.0/meson.build']),
1341                                 sorted([i.name for i in tar]))
1342
1343    def test_rpath_uses_ORIGIN(self):
1344        '''
1345        Test that built targets use $ORIGIN in rpath, which ensures that they
1346        are relocatable and ensures that builds are reproducible since the
1347        build directory won't get embedded into the built binaries.
1348        '''
1349        if is_windows() or is_cygwin():
1350            raise SkipTest('Windows PE/COFF binaries do not use RPATH')
1351        testdir = os.path.join(self.common_test_dir, '39 library chain')
1352        self.init(testdir)
1353        self.build()
1354        for each in ('prog', 'subdir/liblib1.so', ):
1355            rpath = get_rpath(os.path.join(self.builddir, each))
1356            self.assertTrue(rpath, f'Rpath could not be determined for {each}.')
1357            if is_dragonflybsd():
1358                # DragonflyBSD will prepend /usr/lib/gccVERSION to the rpath,
1359                # so ignore that.
1360                self.assertTrue(rpath.startswith('/usr/lib/gcc'))
1361                rpaths = rpath.split(':')[1:]
1362            else:
1363                rpaths = rpath.split(':')
1364            for path in rpaths:
1365                self.assertTrue(path.startswith('$ORIGIN'), msg=(each, path))
1366        # These two don't link to anything else, so they do not need an rpath entry.
1367        for each in ('subdir/subdir2/liblib2.so', 'subdir/subdir3/liblib3.so'):
1368            rpath = get_rpath(os.path.join(self.builddir, each))
1369            if is_dragonflybsd():
1370                # The rpath should be equal to /usr/lib/gccVERSION
1371                self.assertTrue(rpath.startswith('/usr/lib/gcc'))
1372                self.assertEqual(len(rpath.split(':')), 1)
1373            else:
1374                self.assertTrue(rpath is None)
1375
1376    def test_dash_d_dedup(self):
1377        testdir = os.path.join(self.unit_test_dir, '9 d dedup')
1378        self.init(testdir)
1379        cmd = self.get_compdb()[0]['command']
1380        self.assertTrue('-D FOO -D BAR' in cmd or
1381                        '"-D" "FOO" "-D" "BAR"' in cmd or
1382                        '/D FOO /D BAR' in cmd or
1383                        '"/D" "FOO" "/D" "BAR"' in cmd)
1384
1385    def test_all_forbidden_targets_tested(self):
1386        '''
1387        Test that all forbidden targets are tested in the '150 reserved targets'
1388        test. Needs to be a unit test because it accesses Meson internals.
1389        '''
1390        testdir = os.path.join(self.common_test_dir, '150 reserved targets')
1391        targets = mesonbuild.coredata.FORBIDDEN_TARGET_NAMES
1392        # We don't actually define a target with this name
1393        targets.pop('build.ninja')
1394        # Remove this to avoid multiple entries with the same name
1395        # but different case.
1396        targets.pop('PHONY')
1397        for i in targets:
1398            self.assertPathExists(os.path.join(testdir, i))
1399
1400    def detect_prebuild_env(self):
1401        env = get_fake_env()
1402        cc = detect_c_compiler(env, MachineChoice.HOST)
1403        stlinker = detect_static_linker(env, cc)
1404        if is_windows():
1405            object_suffix = 'obj'
1406            shared_suffix = 'dll'
1407        elif is_cygwin():
1408            object_suffix = 'o'
1409            shared_suffix = 'dll'
1410        elif is_osx():
1411            object_suffix = 'o'
1412            shared_suffix = 'dylib'
1413        else:
1414            object_suffix = 'o'
1415            shared_suffix = 'so'
1416        return (cc, stlinker, object_suffix, shared_suffix)
1417
1418    def pbcompile(self, compiler, source, objectfile, extra_args=None):
1419        cmd = compiler.get_exelist()
1420        extra_args = extra_args or []
1421        if compiler.get_argument_syntax() == 'msvc':
1422            cmd += ['/nologo', '/Fo' + objectfile, '/c', source] + extra_args
1423        else:
1424            cmd += ['-c', source, '-o', objectfile] + extra_args
1425        subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1426
1427    def test_prebuilt_object(self):
1428        (compiler, _, object_suffix, _) = self.detect_prebuild_env()
1429        tdir = os.path.join(self.unit_test_dir, '15 prebuilt object')
1430        source = os.path.join(tdir, 'source.c')
1431        objectfile = os.path.join(tdir, 'prebuilt.' + object_suffix)
1432        self.pbcompile(compiler, source, objectfile)
1433        try:
1434            self.init(tdir)
1435            self.build()
1436            self.run_tests()
1437        finally:
1438            os.unlink(objectfile)
1439
1440    def build_static_lib(self, compiler, linker, source, objectfile, outfile, extra_args=None):
1441        if extra_args is None:
1442            extra_args = []
1443        if compiler.get_argument_syntax() == 'msvc':
1444            link_cmd = ['lib', '/NOLOGO', '/OUT:' + outfile, objectfile]
1445        else:
1446            link_cmd = ['ar', 'csr', outfile, objectfile]
1447        link_cmd = linker.get_exelist()
1448        link_cmd += linker.get_always_args()
1449        link_cmd += linker.get_std_link_args()
1450        link_cmd += linker.get_output_args(outfile)
1451        link_cmd += [objectfile]
1452        self.pbcompile(compiler, source, objectfile, extra_args=extra_args)
1453        try:
1454            subprocess.check_call(link_cmd)
1455        finally:
1456            os.unlink(objectfile)
1457
1458    def test_prebuilt_static_lib(self):
1459        (cc, stlinker, object_suffix, _) = self.detect_prebuild_env()
1460        tdir = os.path.join(self.unit_test_dir, '16 prebuilt static')
1461        source = os.path.join(tdir, 'libdir/best.c')
1462        objectfile = os.path.join(tdir, 'libdir/best.' + object_suffix)
1463        stlibfile = os.path.join(tdir, 'libdir/libbest.a')
1464        self.build_static_lib(cc, stlinker, source, objectfile, stlibfile)
1465        # Run the test
1466        try:
1467            self.init(tdir)
1468            self.build()
1469            self.run_tests()
1470        finally:
1471            os.unlink(stlibfile)
1472
1473    def build_shared_lib(self, compiler, source, objectfile, outfile, impfile, extra_args=None):
1474        if extra_args is None:
1475            extra_args = []
1476        if compiler.get_argument_syntax() == 'msvc':
1477            link_cmd = compiler.get_linker_exelist() + [
1478                '/NOLOGO', '/DLL', '/DEBUG', '/IMPLIB:' + impfile,
1479                '/OUT:' + outfile, objectfile]
1480        else:
1481            if not (compiler.info.is_windows() or compiler.info.is_cygwin() or compiler.info.is_darwin()):
1482                extra_args += ['-fPIC']
1483            link_cmd = compiler.get_exelist() + ['-shared', '-o', outfile, objectfile]
1484            if not is_osx():
1485                link_cmd += ['-Wl,-soname=' + os.path.basename(outfile)]
1486        self.pbcompile(compiler, source, objectfile, extra_args=extra_args)
1487        try:
1488            subprocess.check_call(link_cmd)
1489        finally:
1490            os.unlink(objectfile)
1491
1492    def test_prebuilt_shared_lib(self):
1493        (cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env()
1494        tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared')
1495        source = os.path.join(tdir, 'alexandria.c')
1496        objectfile = os.path.join(tdir, 'alexandria.' + object_suffix)
1497        impfile = os.path.join(tdir, 'alexandria.lib')
1498        if cc.get_argument_syntax() == 'msvc':
1499            shlibfile = os.path.join(tdir, 'alexandria.' + shared_suffix)
1500        elif is_cygwin():
1501            shlibfile = os.path.join(tdir, 'cygalexandria.' + shared_suffix)
1502        else:
1503            shlibfile = os.path.join(tdir, 'libalexandria.' + shared_suffix)
1504        self.build_shared_lib(cc, source, objectfile, shlibfile, impfile)
1505        # Run the test
1506        try:
1507            self.init(tdir)
1508            self.build()
1509            self.run_tests()
1510        finally:
1511            os.unlink(shlibfile)
1512            if is_windows():
1513                # Clean up all the garbage MSVC writes in the
1514                # source tree.
1515                for fname in glob(os.path.join(tdir, 'alexandria.*')):
1516                    if os.path.splitext(fname)[1] not in ['.c', '.h']:
1517                        os.unlink(fname)
1518
1519    @skipIfNoPkgconfig
1520    def test_pkgconfig_static(self):
1521        '''
1522        Test that the we prefer static libraries when `static: true` is
1523        passed to dependency() with pkg-config. Can't be an ordinary test
1524        because we need to build libs and try to find them from meson.build
1525
1526        Also test that it's not a hard error to have unsatisfiable library deps
1527        since system libraries -lm will never be found statically.
1528        https://github.com/mesonbuild/meson/issues/2785
1529        '''
1530        (cc, stlinker, objext, shext) = self.detect_prebuild_env()
1531        testdir = os.path.join(self.unit_test_dir, '18 pkgconfig static')
1532        source = os.path.join(testdir, 'foo.c')
1533        objectfile = os.path.join(testdir, 'foo.' + objext)
1534        stlibfile = os.path.join(testdir, 'libfoo.a')
1535        impfile = os.path.join(testdir, 'foo.lib')
1536        if cc.get_argument_syntax() == 'msvc':
1537            shlibfile = os.path.join(testdir, 'foo.' + shext)
1538        elif is_cygwin():
1539            shlibfile = os.path.join(testdir, 'cygfoo.' + shext)
1540        else:
1541            shlibfile = os.path.join(testdir, 'libfoo.' + shext)
1542        # Build libs
1543        self.build_static_lib(cc, stlinker, source, objectfile, stlibfile, extra_args=['-DFOO_STATIC'])
1544        self.build_shared_lib(cc, source, objectfile, shlibfile, impfile)
1545        # Run test
1546        try:
1547            self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': self.builddir})
1548            self.build()
1549            self.run_tests()
1550        finally:
1551            os.unlink(stlibfile)
1552            os.unlink(shlibfile)
1553            if is_windows():
1554                # Clean up all the garbage MSVC writes in the
1555                # source tree.
1556                for fname in glob(os.path.join(testdir, 'foo.*')):
1557                    if os.path.splitext(fname)[1] not in ['.c', '.h', '.in']:
1558                        os.unlink(fname)
1559
1560    @skipIfNoPkgconfig
1561    @mock.patch.dict(os.environ)
1562    def test_pkgconfig_gen_escaping(self):
1563        testdir = os.path.join(self.common_test_dir, '44 pkgconfig-gen')
1564        prefix = '/usr/with spaces'
1565        libdir = 'lib'
1566        self.init(testdir, extra_args=['--prefix=' + prefix,
1567                                       '--libdir=' + libdir])
1568        # Find foo dependency
1569        os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir
1570        env = get_fake_env(testdir, self.builddir, self.prefix)
1571        kwargs = {'required': True, 'silent': True}
1572        foo_dep = PkgConfigDependency('libfoo', env, kwargs)
1573        # Ensure link_args are properly quoted
1574        libdir = PurePath(prefix) / PurePath(libdir)
1575        link_args = ['-L' + libdir.as_posix(), '-lfoo']
1576        self.assertEqual(foo_dep.get_link_args(), link_args)
1577        # Ensure include args are properly quoted
1578        incdir = PurePath(prefix) / PurePath('include')
1579        cargs = ['-I' + incdir.as_posix(), '-DLIBFOO']
1580        # pkg-config and pkgconf does not respect the same order
1581        self.assertEqual(sorted(foo_dep.get_compile_args()), sorted(cargs))
1582
1583    def test_array_option_change(self):
1584        def get_opt():
1585            opts = self.introspect('--buildoptions')
1586            for x in opts:
1587                if x.get('name') == 'list':
1588                    return x
1589            raise Exception(opts)
1590
1591        expected = {
1592            'name': 'list',
1593            'description': 'list',
1594            'section': 'user',
1595            'type': 'array',
1596            'value': ['foo', 'bar'],
1597            'machine': 'any',
1598        }
1599        tdir = os.path.join(self.unit_test_dir, '19 array option')
1600        self.init(tdir)
1601        original = get_opt()
1602        self.assertDictEqual(original, expected)
1603
1604        expected['value'] = ['oink', 'boink']
1605        self.setconf('-Dlist=oink,boink')
1606        changed = get_opt()
1607        self.assertEqual(changed, expected)
1608
1609    def test_array_option_bad_change(self):
1610        def get_opt():
1611            opts = self.introspect('--buildoptions')
1612            for x in opts:
1613                if x.get('name') == 'list':
1614                    return x
1615            raise Exception(opts)
1616
1617        expected = {
1618            'name': 'list',
1619            'description': 'list',
1620            'section': 'user',
1621            'type': 'array',
1622            'value': ['foo', 'bar'],
1623            'machine': 'any',
1624        }
1625        tdir = os.path.join(self.unit_test_dir, '19 array option')
1626        self.init(tdir)
1627        original = get_opt()
1628        self.assertDictEqual(original, expected)
1629        with self.assertRaises(subprocess.CalledProcessError):
1630            self.setconf('-Dlist=bad')
1631        changed = get_opt()
1632        self.assertDictEqual(changed, expected)
1633
1634    def test_array_option_empty_equivalents(self):
1635        """Array options treat -Dopt=[] and -Dopt= as equivalent."""
1636        def get_opt():
1637            opts = self.introspect('--buildoptions')
1638            for x in opts:
1639                if x.get('name') == 'list':
1640                    return x
1641            raise Exception(opts)
1642
1643        expected = {
1644            'name': 'list',
1645            'description': 'list',
1646            'section': 'user',
1647            'type': 'array',
1648            'value': [],
1649            'machine': 'any',
1650        }
1651        tdir = os.path.join(self.unit_test_dir, '19 array option')
1652        self.init(tdir, extra_args='-Dlist=')
1653        original = get_opt()
1654        self.assertDictEqual(original, expected)
1655
1656    def opt_has(self, name, value):
1657        res = self.introspect('--buildoptions')
1658        found = False
1659        for i in res:
1660            if i['name'] == name:
1661                self.assertEqual(i['value'], value)
1662                found = True
1663                break
1664        self.assertTrue(found, "Array option not found in introspect data.")
1665
1666    def test_free_stringarray_setting(self):
1667        testdir = os.path.join(self.common_test_dir, '40 options')
1668        self.init(testdir)
1669        self.opt_has('free_array_opt', [])
1670        self.setconf('-Dfree_array_opt=foo,bar', will_build=False)
1671        self.opt_has('free_array_opt', ['foo', 'bar'])
1672        self.setconf("-Dfree_array_opt=['a,b', 'c,d']", will_build=False)
1673        self.opt_has('free_array_opt', ['a,b', 'c,d'])
1674
1675    # When running under Travis Mac CI, the file updates seem to happen
1676    # too fast so the timestamps do not get properly updated.
1677    # Call this method before file operations in appropriate places
1678    # to make things work.
1679    def mac_ci_delay(self):
1680        if is_osx() and is_ci():
1681            import time
1682            time.sleep(1)
1683
1684    def test_options_with_choices_changing(self) -> None:
1685        """Detect when options like arrays or combos have their choices change."""
1686        testdir = Path(os.path.join(self.unit_test_dir, '84 change option choices'))
1687        options1 = str(testdir / 'meson_options.1.txt')
1688        options2 = str(testdir / 'meson_options.2.txt')
1689
1690        # Test that old options are changed to the new defaults if they are not valid
1691        real_options = str(testdir / 'meson_options.txt')
1692        self.addCleanup(os.unlink, real_options)
1693
1694        shutil.copy(options1, real_options)
1695        self.init(str(testdir))
1696        self.mac_ci_delay()
1697        shutil.copy(options2, real_options)
1698
1699        self.build()
1700        opts = self.introspect('--buildoptions')
1701        for item in opts:
1702            if item['name'] == 'combo':
1703                self.assertEqual(item['value'], 'b')
1704                self.assertEqual(item['choices'], ['b', 'c', 'd'])
1705            elif item['name'] == 'arr':
1706                self.assertEqual(item['value'], ['b'])
1707                self.assertEqual(item['choices'], ['b', 'c', 'd'])
1708
1709        self.wipe()
1710        self.mac_ci_delay()
1711
1712        # When the old options are valid they should remain
1713        shutil.copy(options1, real_options)
1714        self.init(str(testdir), extra_args=['-Dcombo=c', '-Darray=b,c'])
1715        self.mac_ci_delay()
1716        shutil.copy(options2, real_options)
1717        self.build()
1718        opts = self.introspect('--buildoptions')
1719        for item in opts:
1720            if item['name'] == 'combo':
1721                self.assertEqual(item['value'], 'c')
1722                self.assertEqual(item['choices'], ['b', 'c', 'd'])
1723            elif item['name'] == 'arr':
1724                self.assertEqual(item['value'], ['b', 'c'])
1725                self.assertEqual(item['choices'], ['b', 'c', 'd'])
1726
1727    def test_subproject_promotion(self):
1728        testdir = os.path.join(self.unit_test_dir, '12 promote')
1729        workdir = os.path.join(self.builddir, 'work')
1730        shutil.copytree(testdir, workdir)
1731        spdir = os.path.join(workdir, 'subprojects')
1732        s3dir = os.path.join(spdir, 's3')
1733        scommondir = os.path.join(spdir, 'scommon')
1734        self.assertFalse(os.path.isdir(s3dir))
1735        subprocess.check_call(self.wrap_command + ['promote', 's3'],
1736                              cwd=workdir,
1737                              stdout=subprocess.DEVNULL)
1738        self.assertTrue(os.path.isdir(s3dir))
1739        self.assertFalse(os.path.isdir(scommondir))
1740        self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'scommon'],
1741                                            cwd=workdir,
1742                                            stderr=subprocess.DEVNULL), 0)
1743        self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'invalid/path/to/scommon'],
1744                                            cwd=workdir,
1745                                            stderr=subprocess.DEVNULL), 0)
1746        self.assertFalse(os.path.isdir(scommondir))
1747        subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/scommon'], cwd=workdir)
1748        self.assertTrue(os.path.isdir(scommondir))
1749        promoted_wrap = os.path.join(spdir, 'athing.wrap')
1750        self.assertFalse(os.path.isfile(promoted_wrap))
1751        subprocess.check_call(self.wrap_command + ['promote', 'athing'], cwd=workdir)
1752        self.assertTrue(os.path.isfile(promoted_wrap))
1753        self.init(workdir)
1754        self.build()
1755
1756    def test_subproject_promotion_wrap(self):
1757        testdir = os.path.join(self.unit_test_dir, '44 promote wrap')
1758        workdir = os.path.join(self.builddir, 'work')
1759        shutil.copytree(testdir, workdir)
1760        spdir = os.path.join(workdir, 'subprojects')
1761
1762        ambiguous_wrap = os.path.join(spdir, 'ambiguous.wrap')
1763        self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'ambiguous'],
1764                                            cwd=workdir,
1765                                            stderr=subprocess.DEVNULL), 0)
1766        self.assertFalse(os.path.isfile(ambiguous_wrap))
1767        subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/ambiguous.wrap'], cwd=workdir)
1768        self.assertTrue(os.path.isfile(ambiguous_wrap))
1769
1770    def test_warning_location(self):
1771        tdir = os.path.join(self.unit_test_dir, '22 warning location')
1772        out = self.init(tdir)
1773        for expected in [
1774            r'meson.build:4: WARNING: Keyword argument "link_with" defined multiple times.',
1775            r'sub' + os.path.sep + r'meson.build:3: WARNING: Keyword argument "link_with" defined multiple times.',
1776            r'meson.build:6: WARNING: a warning of some sort',
1777            r'sub' + os.path.sep + r'meson.build:4: WARNING: subdir warning',
1778            r'meson.build:7: WARNING: Module unstable-simd has no backwards or forwards compatibility and might not exist in future releases.',
1779            r"meson.build:11: WARNING: The variable(s) 'MISSING' in the input file 'conf.in' are not present in the given configuration data.",
1780        ]:
1781            self.assertRegex(out, re.escape(expected))
1782
1783        for wd in [
1784            self.src_root,
1785            self.builddir,
1786            os.getcwd(),
1787        ]:
1788            self.new_builddir()
1789            out = self.init(tdir, workdir=wd)
1790            expected = os.path.join(relpath(tdir, self.src_root), 'meson.build')
1791            relwd = relpath(self.src_root, wd)
1792            if relwd != '.':
1793                expected = os.path.join(relwd, expected)
1794                expected = '\n' + expected + ':'
1795            self.assertIn(expected, out)
1796
1797    def test_error_location_path(self):
1798        '''Test locations in meson errors contain correct paths'''
1799        # this list contains errors from all the different steps in the
1800        # lexer/parser/interpreter we have tests for.
1801        for (t, f) in [
1802            ('10 out of bounds', 'meson.build'),
1803            ('18 wrong plusassign', 'meson.build'),
1804            ('60 bad option argument', 'meson_options.txt'),
1805            ('98 subdir parse error', os.path.join('subdir', 'meson.build')),
1806            ('99 invalid option file', 'meson_options.txt'),
1807        ]:
1808            tdir = os.path.join(self.src_root, 'test cases', 'failing', t)
1809
1810            for wd in [
1811                self.src_root,
1812                self.builddir,
1813                os.getcwd(),
1814            ]:
1815                try:
1816                    self.init(tdir, workdir=wd)
1817                except subprocess.CalledProcessError as e:
1818                    expected = os.path.join('test cases', 'failing', t, f)
1819                    relwd = relpath(self.src_root, wd)
1820                    if relwd != '.':
1821                        expected = os.path.join(relwd, expected)
1822                    expected = '\n' + expected + ':'
1823                    self.assertIn(expected, e.output)
1824                else:
1825                    self.fail('configure unexpectedly succeeded')
1826
1827    def test_permitted_method_kwargs(self):
1828        tdir = os.path.join(self.unit_test_dir, '25 non-permitted kwargs')
1829        with self.assertRaises(subprocess.CalledProcessError) as cm:
1830            self.init(tdir)
1831        self.assertIn('ERROR: compiler.has_header_symbol got unknown keyword arguments "prefixxx"', cm.exception.output)
1832
1833    def test_templates(self):
1834        ninja = mesonbuild.environment.detect_ninja()
1835        if ninja is None:
1836            raise SkipTest('This test currently requires ninja. Fix this once "meson build" works.')
1837
1838        langs = ['c']
1839        env = get_fake_env()
1840        for l in ['cpp', 'cs', 'd', 'java', 'cuda', 'fortran', 'objc', 'objcpp', 'rust']:
1841            try:
1842                comp = detect_compiler_for(env, l, MachineChoice.HOST)
1843                with tempfile.TemporaryDirectory() as d:
1844                    comp.sanity_check(d, env)
1845                langs.append(l)
1846            except EnvironmentException:
1847                pass
1848
1849        # The D template fails under mac CI and we don't know why.
1850        # Patches welcome
1851        if is_osx():
1852            langs = [l for l in langs if l != 'd']
1853
1854        for lang in langs:
1855            for target_type in ('executable', 'library'):
1856                # test empty directory
1857                with tempfile.TemporaryDirectory() as tmpdir:
1858                    self._run(self.meson_command + ['init', '--language', lang, '--type', target_type],
1859                              workdir=tmpdir)
1860                    self._run(self.setup_command + ['--backend=ninja', 'builddir'],
1861                              workdir=tmpdir)
1862                    self._run(ninja,
1863                              workdir=os.path.join(tmpdir, 'builddir'))
1864            # test directory with existing code file
1865            if lang in {'c', 'cpp', 'd'}:
1866                with tempfile.TemporaryDirectory() as tmpdir:
1867                    with open(os.path.join(tmpdir, 'foo.' + lang), 'w', encoding='utf-8') as f:
1868                        f.write('int main(void) {}')
1869                    self._run(self.meson_command + ['init', '-b'], workdir=tmpdir)
1870            elif lang in {'java'}:
1871                with tempfile.TemporaryDirectory() as tmpdir:
1872                    with open(os.path.join(tmpdir, 'Foo.' + lang), 'w', encoding='utf-8') as f:
1873                        f.write('public class Foo { public static void main() {} }')
1874                    self._run(self.meson_command + ['init', '-b'], workdir=tmpdir)
1875
1876    def test_compiler_run_command(self):
1877        '''
1878        The test checks that the compiler object can be passed to
1879        run_command().
1880        '''
1881        testdir = os.path.join(self.unit_test_dir, '24 compiler run_command')
1882        self.init(testdir)
1883
1884    def test_identical_target_name_in_subproject_flat_layout(self):
1885        '''
1886        Test that identical targets in different subprojects do not collide
1887        if layout is flat.
1888        '''
1889        testdir = os.path.join(self.common_test_dir, '172 identical target name in subproject flat layout')
1890        self.init(testdir, extra_args=['--layout=flat'])
1891        self.build()
1892
1893    def test_identical_target_name_in_subdir_flat_layout(self):
1894        '''
1895        Test that identical targets in different subdirs do not collide
1896        if layout is flat.
1897        '''
1898        testdir = os.path.join(self.common_test_dir, '181 same target name flat layout')
1899        self.init(testdir, extra_args=['--layout=flat'])
1900        self.build()
1901
1902    def test_flock(self):
1903        exception_raised = False
1904        with tempfile.TemporaryDirectory() as tdir:
1905            os.mkdir(os.path.join(tdir, 'meson-private'))
1906            with BuildDirLock(tdir):
1907                try:
1908                    with BuildDirLock(tdir):
1909                        pass
1910                except MesonException:
1911                    exception_raised = True
1912        self.assertTrue(exception_raised, 'Double locking did not raise exception.')
1913
1914    @skipIf(is_osx(), 'Test not applicable to OSX')
1915    def test_check_module_linking(self):
1916        """
1917        Test that link_with: a shared module issues a warning
1918        https://github.com/mesonbuild/meson/issues/2865
1919        (That an error is raised on OSX is exercised by test failing/78)
1920        """
1921        tdir = os.path.join(self.unit_test_dir, '30 shared_mod linking')
1922        out = self.init(tdir)
1923        msg = ('WARNING: target links against shared modules. This is not '
1924               'recommended as it is not supported on some platforms')
1925        self.assertIn(msg, out)
1926
1927    def test_ndebug_if_release_disabled(self):
1928        testdir = os.path.join(self.unit_test_dir, '28 ndebug if-release')
1929        self.init(testdir, extra_args=['--buildtype=release', '-Db_ndebug=if-release'])
1930        self.build()
1931        exe = os.path.join(self.builddir, 'main')
1932        self.assertEqual(b'NDEBUG=1', subprocess.check_output(exe).strip())
1933
1934    def test_ndebug_if_release_enabled(self):
1935        testdir = os.path.join(self.unit_test_dir, '28 ndebug if-release')
1936        self.init(testdir, extra_args=['--buildtype=debugoptimized', '-Db_ndebug=if-release'])
1937        self.build()
1938        exe = os.path.join(self.builddir, 'main')
1939        self.assertEqual(b'NDEBUG=0', subprocess.check_output(exe).strip())
1940
1941    def test_guessed_linker_dependencies(self):
1942        '''
1943        Test that meson adds dependencies for libraries based on the final
1944        linker command line.
1945        '''
1946        testdirbase = os.path.join(self.unit_test_dir, '29 guessed linker dependencies')
1947        testdirlib = os.path.join(testdirbase, 'lib')
1948
1949        extra_args = None
1950        libdir_flags = ['-L']
1951        env = get_fake_env(testdirlib, self.builddir, self.prefix)
1952        if detect_c_compiler(env, MachineChoice.HOST).get_id() in {'msvc', 'clang-cl', 'intel-cl'}:
1953            # msvc-like compiler, also test it with msvc-specific flags
1954            libdir_flags += ['/LIBPATH:', '-LIBPATH:']
1955        else:
1956            # static libraries are not linkable with -l with msvc because meson installs them
1957            # as .a files which unix_args_to_native will not know as it expects libraries to use
1958            # .lib as extension. For a DLL the import library is installed as .lib. Thus for msvc
1959            # this tests needs to use shared libraries to test the path resolving logic in the
1960            # dependency generation code path.
1961            extra_args = ['--default-library', 'static']
1962
1963        initial_builddir = self.builddir
1964        initial_installdir = self.installdir
1965
1966        for libdir_flag in libdir_flags:
1967            # build library
1968            self.new_builddir()
1969            self.init(testdirlib, extra_args=extra_args)
1970            self.build()
1971            self.install()
1972            libbuilddir = self.builddir
1973            installdir = self.installdir
1974            libdir = os.path.join(self.installdir, self.prefix.lstrip('/').lstrip('\\'), 'lib')
1975
1976            # build user of library
1977            self.new_builddir()
1978            # replace is needed because meson mangles platform paths passed via LDFLAGS
1979            self.init(os.path.join(testdirbase, 'exe'),
1980                      override_envvars={"LDFLAGS": '{}{}'.format(libdir_flag, libdir.replace('\\', '/'))})
1981            self.build()
1982            self.assertBuildIsNoop()
1983
1984            # rebuild library
1985            exebuilddir = self.builddir
1986            self.installdir = installdir
1987            self.builddir = libbuilddir
1988            # Microsoft's compiler is quite smart about touching import libs on changes,
1989            # so ensure that there is actually a change in symbols.
1990            self.setconf('-Dmore_exports=true')
1991            self.build()
1992            self.install()
1993            # no ensure_backend_detects_changes needed because self.setconf did that already
1994
1995            # assert user of library will be rebuild
1996            self.builddir = exebuilddir
1997            self.assertRebuiltTarget('app')
1998
1999            # restore dirs for the next test case
2000            self.installdir = initial_builddir
2001            self.builddir = initial_installdir
2002
2003    def test_conflicting_d_dash_option(self):
2004        testdir = os.path.join(self.unit_test_dir, '37 mixed command line args')
2005        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as e:
2006            self.init(testdir, extra_args=['-Dbindir=foo', '--bindir=bar'])
2007            # Just to ensure that we caught the correct error
2008            self.assertIn('as both', e.stderr)
2009
2010    def _test_same_option_twice(self, arg, args):
2011        testdir = os.path.join(self.unit_test_dir, '37 mixed command line args')
2012        self.init(testdir, extra_args=args)
2013        opts = self.introspect('--buildoptions')
2014        for item in opts:
2015            if item['name'] == arg:
2016                self.assertEqual(item['value'], 'bar')
2017                return
2018        raise Exception(f'Missing {arg} value?')
2019
2020    def test_same_dash_option_twice(self):
2021        self._test_same_option_twice('bindir', ['--bindir=foo', '--bindir=bar'])
2022
2023    def test_same_d_option_twice(self):
2024        self._test_same_option_twice('bindir', ['-Dbindir=foo', '-Dbindir=bar'])
2025
2026    def test_same_project_d_option_twice(self):
2027        self._test_same_option_twice('one', ['-Done=foo', '-Done=bar'])
2028
2029    def _test_same_option_twice_configure(self, arg, args):
2030        testdir = os.path.join(self.unit_test_dir, '37 mixed command line args')
2031        self.init(testdir)
2032        self.setconf(args)
2033        opts = self.introspect('--buildoptions')
2034        for item in opts:
2035            if item['name'] == arg:
2036                self.assertEqual(item['value'], 'bar')
2037                return
2038        raise Exception(f'Missing {arg} value?')
2039
2040    def test_same_dash_option_twice_configure(self):
2041        self._test_same_option_twice_configure(
2042            'bindir', ['--bindir=foo', '--bindir=bar'])
2043
2044    def test_same_d_option_twice_configure(self):
2045        self._test_same_option_twice_configure(
2046            'bindir', ['-Dbindir=foo', '-Dbindir=bar'])
2047
2048    def test_same_project_d_option_twice_configure(self):
2049        self._test_same_option_twice_configure(
2050            'one', ['-Done=foo', '-Done=bar'])
2051
2052    def test_command_line(self):
2053        testdir = os.path.join(self.unit_test_dir, '34 command line')
2054
2055        # Verify default values when passing no args that affect the
2056        # configuration, and as a bonus, test that --profile-self works.
2057        out = self.init(testdir, extra_args=['--profile-self', '--fatal-meson-warnings'])
2058        self.assertNotIn('[default: true]', out)
2059        obj = mesonbuild.coredata.load(self.builddir)
2060        self.assertEqual(obj.options[OptionKey('default_library')].value, 'static')
2061        self.assertEqual(obj.options[OptionKey('warning_level')].value, '1')
2062        self.assertEqual(obj.options[OptionKey('set_sub_opt')].value, True)
2063        self.assertEqual(obj.options[OptionKey('subp_opt', 'subp')].value, 'default3')
2064        self.wipe()
2065
2066        # warning_level is special, it's --warnlevel instead of --warning-level
2067        # for historical reasons
2068        self.init(testdir, extra_args=['--warnlevel=2', '--fatal-meson-warnings'])
2069        obj = mesonbuild.coredata.load(self.builddir)
2070        self.assertEqual(obj.options[OptionKey('warning_level')].value, '2')
2071        self.setconf('--warnlevel=3')
2072        obj = mesonbuild.coredata.load(self.builddir)
2073        self.assertEqual(obj.options[OptionKey('warning_level')].value, '3')
2074        self.wipe()
2075
2076        # But when using -D syntax, it should be 'warning_level'
2077        self.init(testdir, extra_args=['-Dwarning_level=2', '--fatal-meson-warnings'])
2078        obj = mesonbuild.coredata.load(self.builddir)
2079        self.assertEqual(obj.options[OptionKey('warning_level')].value, '2')
2080        self.setconf('-Dwarning_level=3')
2081        obj = mesonbuild.coredata.load(self.builddir)
2082        self.assertEqual(obj.options[OptionKey('warning_level')].value, '3')
2083        self.wipe()
2084
2085        # Mixing --option and -Doption is forbidden
2086        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm:
2087            self.init(testdir, extra_args=['--warnlevel=1', '-Dwarning_level=3'])
2088            if isinstance(cm.exception, subprocess.CalledProcessError):
2089                self.assertNotEqual(0, cm.exception.returncode)
2090                self.assertIn('as both', cm.exception.output)
2091            else:
2092                self.assertIn('as both', str(cm.exception))
2093        self.init(testdir)
2094        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm:
2095            self.setconf(['--warnlevel=1', '-Dwarning_level=3'])
2096            if isinstance(cm.exception, subprocess.CalledProcessError):
2097                self.assertNotEqual(0, cm.exception.returncode)
2098                self.assertIn('as both', cm.exception.output)
2099            else:
2100                self.assertIn('as both', str(cm.exception))
2101        self.wipe()
2102
2103        # --default-library should override default value from project()
2104        self.init(testdir, extra_args=['--default-library=both', '--fatal-meson-warnings'])
2105        obj = mesonbuild.coredata.load(self.builddir)
2106        self.assertEqual(obj.options[OptionKey('default_library')].value, 'both')
2107        self.setconf('--default-library=shared')
2108        obj = mesonbuild.coredata.load(self.builddir)
2109        self.assertEqual(obj.options[OptionKey('default_library')].value, 'shared')
2110        if self.backend is Backend.ninja:
2111            # reconfigure target works only with ninja backend
2112            self.build('reconfigure')
2113            obj = mesonbuild.coredata.load(self.builddir)
2114            self.assertEqual(obj.options[OptionKey('default_library')].value, 'shared')
2115        self.wipe()
2116
2117        # Should fail on unknown options
2118        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm:
2119            self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo'])
2120            self.assertNotEqual(0, cm.exception.returncode)
2121            self.assertIn(msg, cm.exception.output)
2122        self.wipe()
2123
2124        # Should fail on malformed option
2125        msg = "Option 'foo' must have a value separated by equals sign."
2126        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm:
2127            self.init(testdir, extra_args=['-Dfoo'])
2128            if isinstance(cm.exception, subprocess.CalledProcessError):
2129                self.assertNotEqual(0, cm.exception.returncode)
2130                self.assertIn(msg, cm.exception.output)
2131            else:
2132                self.assertIn(msg, str(cm.exception))
2133        self.init(testdir)
2134        with self.assertRaises((subprocess.CalledProcessError, RuntimeError)) as cm:
2135            self.setconf('-Dfoo')
2136            if isinstance(cm.exception, subprocess.CalledProcessError):
2137                self.assertNotEqual(0, cm.exception.returncode)
2138                self.assertIn(msg, cm.exception.output)
2139            else:
2140                self.assertIn(msg, str(cm.exception))
2141        self.wipe()
2142
2143        # It is not an error to set wrong option for unknown subprojects or
2144        # language because we don't have control on which one will be selected.
2145        self.init(testdir, extra_args=['-Dc_wrong=1', '-Dwrong:bad=1', '-Db_wrong=1'])
2146        self.wipe()
2147
2148        # Test we can set subproject option
2149        self.init(testdir, extra_args=['-Dsubp:subp_opt=foo', '--fatal-meson-warnings'])
2150        obj = mesonbuild.coredata.load(self.builddir)
2151        self.assertEqual(obj.options[OptionKey('subp_opt', 'subp')].value, 'foo')
2152        self.wipe()
2153
2154        # c_args value should be parsed with split_args
2155        self.init(testdir, extra_args=['-Dc_args=-Dfoo -Dbar "-Dthird=one two"', '--fatal-meson-warnings'])
2156        obj = mesonbuild.coredata.load(self.builddir)
2157        self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dfoo', '-Dbar', '-Dthird=one two'])
2158
2159        self.setconf('-Dc_args="foo bar" one two')
2160        obj = mesonbuild.coredata.load(self.builddir)
2161        self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['foo bar', 'one', 'two'])
2162        self.wipe()
2163
2164        self.init(testdir, extra_args=['-Dset_percent_opt=myoption%', '--fatal-meson-warnings'])
2165        obj = mesonbuild.coredata.load(self.builddir)
2166        self.assertEqual(obj.options[OptionKey('set_percent_opt')].value, 'myoption%')
2167        self.wipe()
2168
2169        # Setting a 2nd time the same option should override the first value
2170        try:
2171            self.init(testdir, extra_args=['--bindir=foo', '--bindir=bar',
2172                                           '-Dbuildtype=plain', '-Dbuildtype=release',
2173                                           '-Db_sanitize=address', '-Db_sanitize=thread',
2174                                           '-Dc_args=-Dfoo', '-Dc_args=-Dbar',
2175                                           '-Db_lundef=false', '--fatal-meson-warnings'])
2176            obj = mesonbuild.coredata.load(self.builddir)
2177            self.assertEqual(obj.options[OptionKey('bindir')].value, 'bar')
2178            self.assertEqual(obj.options[OptionKey('buildtype')].value, 'release')
2179            self.assertEqual(obj.options[OptionKey('b_sanitize')].value, 'thread')
2180            self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dbar'])
2181            self.setconf(['--bindir=bar', '--bindir=foo',
2182                          '-Dbuildtype=release', '-Dbuildtype=plain',
2183                          '-Db_sanitize=thread', '-Db_sanitize=address',
2184                          '-Dc_args=-Dbar', '-Dc_args=-Dfoo'])
2185            obj = mesonbuild.coredata.load(self.builddir)
2186            self.assertEqual(obj.options[OptionKey('bindir')].value, 'foo')
2187            self.assertEqual(obj.options[OptionKey('buildtype')].value, 'plain')
2188            self.assertEqual(obj.options[OptionKey('b_sanitize')].value, 'address')
2189            self.assertEqual(obj.options[OptionKey('args', lang='c')].value, ['-Dfoo'])
2190            self.wipe()
2191        except KeyError:
2192            # Ignore KeyError, it happens on CI for compilers that does not
2193            # support b_sanitize. We have to test with a base option because
2194            # they used to fail this test with Meson 0.46 an earlier versions.
2195            pass
2196
2197    def test_warning_level_0(self):
2198        testdir = os.path.join(self.common_test_dir, '207 warning level 0')
2199
2200        # Verify default values when passing no args
2201        self.init(testdir)
2202        obj = mesonbuild.coredata.load(self.builddir)
2203        self.assertEqual(obj.options[OptionKey('warning_level')].value, '0')
2204        self.wipe()
2205
2206        # verify we can override w/ --warnlevel
2207        self.init(testdir, extra_args=['--warnlevel=1'])
2208        obj = mesonbuild.coredata.load(self.builddir)
2209        self.assertEqual(obj.options[OptionKey('warning_level')].value, '1')
2210        self.setconf('--warnlevel=0')
2211        obj = mesonbuild.coredata.load(self.builddir)
2212        self.assertEqual(obj.options[OptionKey('warning_level')].value, '0')
2213        self.wipe()
2214
2215        # verify we can override w/ -Dwarning_level
2216        self.init(testdir, extra_args=['-Dwarning_level=1'])
2217        obj = mesonbuild.coredata.load(self.builddir)
2218        self.assertEqual(obj.options[OptionKey('warning_level')].value, '1')
2219        self.setconf('-Dwarning_level=0')
2220        obj = mesonbuild.coredata.load(self.builddir)
2221        self.assertEqual(obj.options[OptionKey('warning_level')].value, '0')
2222        self.wipe()
2223
2224    def test_feature_check_usage_subprojects(self):
2225        testdir = os.path.join(self.unit_test_dir, '41 featurenew subprojects')
2226        out = self.init(testdir)
2227        # Parent project warns correctly
2228        self.assertRegex(out, "WARNING: Project targeting '>=0.45'.*'0.47.0': dict")
2229        # Subprojects warn correctly
2230        self.assertRegex(out, r"\| WARNING: Project targeting '>=0.40'.*'0.44.0': disabler")
2231        self.assertRegex(out, r"\| WARNING: Project targeting '!=0.40'.*'0.44.0': disabler")
2232        # Subproject has a new-enough meson_version, no warning
2233        self.assertNotRegex(out, "WARNING: Project targeting.*Python")
2234        # Ensure a summary is printed in the subproject and the outer project
2235        self.assertRegex(out, r"\| WARNING: Project specifies a minimum meson_version '>=0.40'")
2236        self.assertRegex(out, r"\| \* 0.44.0: {'disabler'}")
2237        self.assertRegex(out, "WARNING: Project specifies a minimum meson_version '>=0.45'")
2238        self.assertRegex(out, " * 0.47.0: {'dict'}")
2239
2240    def test_configure_file_warnings(self):
2241        testdir = os.path.join(self.common_test_dir, "14 configure file")
2242        out = self.init(testdir)
2243        self.assertRegex(out, "WARNING:.*'empty'.*config.h.in.*not present.*")
2244        self.assertRegex(out, "WARNING:.*'FOO_BAR'.*nosubst-nocopy2.txt.in.*not present.*")
2245        self.assertRegex(out, "WARNING:.*'empty'.*config.h.in.*not present.*")
2246        self.assertRegex(out, "WARNING:.*empty configuration_data.*test.py.in")
2247        # Warnings for configuration files that are overwritten.
2248        self.assertRegex(out, "WARNING:.*\"double_output.txt\".*overwrites")
2249        self.assertRegex(out, "WARNING:.*\"subdir.double_output2.txt\".*overwrites")
2250        self.assertNotRegex(out, "WARNING:.*no_write_conflict.txt.*overwrites")
2251        self.assertNotRegex(out, "WARNING:.*@BASENAME@.*overwrites")
2252        self.assertRegex(out, "WARNING:.*\"sameafterbasename\".*overwrites")
2253        # No warnings about empty configuration data objects passed to files with substitutions
2254        self.assertNotRegex(out, "WARNING:.*empty configuration_data.*nosubst-nocopy1.txt.in")
2255        self.assertNotRegex(out, "WARNING:.*empty configuration_data.*nosubst-nocopy2.txt.in")
2256        with open(os.path.join(self.builddir, 'nosubst-nocopy1.txt'), 'rb') as f:
2257            self.assertEqual(f.read().strip(), b'/* #undef FOO_BAR */')
2258        with open(os.path.join(self.builddir, 'nosubst-nocopy2.txt'), 'rb') as f:
2259            self.assertEqual(f.read().strip(), b'')
2260        self.assertRegex(out, r"DEPRECATION:.*\['array'\] is invalid.*dict")
2261
2262    def test_dirs(self):
2263        with tempfile.TemporaryDirectory() as containing:
2264            with tempfile.TemporaryDirectory(dir=containing) as srcdir:
2265                mfile = os.path.join(srcdir, 'meson.build')
2266                of = open(mfile, 'w', encoding='utf-8')
2267                of.write("project('foobar', 'c')\n")
2268                of.close()
2269                pc = subprocess.run(self.setup_command,
2270                                    cwd=srcdir,
2271                                    stdout=subprocess.PIPE,
2272                                    stderr=subprocess.DEVNULL)
2273                self.assertIn(b'Must specify at least one directory name', pc.stdout)
2274                with tempfile.TemporaryDirectory(dir=srcdir) as builddir:
2275                    subprocess.run(self.setup_command,
2276                                   check=True,
2277                                   cwd=builddir,
2278                                   stdout=subprocess.DEVNULL,
2279                                   stderr=subprocess.DEVNULL)
2280
2281    def get_opts_as_dict(self):
2282        result = {}
2283        for i in self.introspect('--buildoptions'):
2284            result[i['name']] = i['value']
2285        return result
2286
2287    def test_buildtype_setting(self):
2288        testdir = os.path.join(self.common_test_dir, '1 trivial')
2289        self.init(testdir)
2290        opts = self.get_opts_as_dict()
2291        self.assertEqual(opts['buildtype'], 'debug')
2292        self.assertEqual(opts['debug'], True)
2293        self.setconf('-Ddebug=false')
2294        opts = self.get_opts_as_dict()
2295        self.assertEqual(opts['debug'], False)
2296        self.assertEqual(opts['buildtype'], 'debug')
2297        self.assertEqual(opts['optimization'], '0')
2298        self.setconf('-Doptimization=g')
2299        opts = self.get_opts_as_dict()
2300        self.assertEqual(opts['debug'], False)
2301        self.assertEqual(opts['buildtype'], 'debug')
2302        self.assertEqual(opts['optimization'], 'g')
2303
2304    @skipIfNoPkgconfig
2305    @skipIf(is_windows(), 'Help needed with fixing this test on windows')
2306    def test_native_dep_pkgconfig(self):
2307        testdir = os.path.join(self.unit_test_dir,
2308                               '46 native dep pkgconfig var')
2309        with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile:
2310            crossfile.write(textwrap.dedent(
2311                '''[binaries]
2312                pkgconfig = '{}'
2313
2314                [properties]
2315
2316                [host_machine]
2317                system = 'linux'
2318                cpu_family = 'arm'
2319                cpu = 'armv7'
2320                endian = 'little'
2321                '''.format(os.path.join(testdir, 'cross_pkgconfig.py'))))
2322            crossfile.flush()
2323            self.meson_cross_file = crossfile.name
2324
2325        env = {'PKG_CONFIG_LIBDIR':  os.path.join(testdir,
2326                                                  'native_pkgconfig')}
2327        self.init(testdir, extra_args=['-Dstart_native=false'], override_envvars=env)
2328        self.wipe()
2329        self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env)
2330
2331    @skipIfNoPkgconfig
2332    @skipIf(is_windows(), 'Help needed with fixing this test on windows')
2333    def test_pkg_config_libdir(self):
2334        testdir = os.path.join(self.unit_test_dir,
2335                               '46 native dep pkgconfig var')
2336        with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile:
2337            crossfile.write(textwrap.dedent(
2338                '''[binaries]
2339                pkgconfig = 'pkg-config'
2340
2341                [properties]
2342                pkg_config_libdir = ['{}']
2343
2344                [host_machine]
2345                system = 'linux'
2346                cpu_family = 'arm'
2347                cpu = 'armv7'
2348                endian = 'little'
2349                '''.format(os.path.join(testdir, 'cross_pkgconfig'))))
2350            crossfile.flush()
2351            self.meson_cross_file = crossfile.name
2352
2353        env = {'PKG_CONFIG_LIBDIR':  os.path.join(testdir,
2354                                                  'native_pkgconfig')}
2355        self.init(testdir, extra_args=['-Dstart_native=false'], override_envvars=env)
2356        self.wipe()
2357        self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env)
2358
2359    def __reconfigure(self):
2360        # Set an older version to force a reconfigure from scratch
2361        filename = os.path.join(self.privatedir, 'coredata.dat')
2362        with open(filename, 'rb') as f:
2363            obj = pickle.load(f)
2364        obj.version = '0.47.0'
2365        with open(filename, 'wb') as f:
2366            pickle.dump(obj, f)
2367
2368    def test_reconfigure(self):
2369        testdir = os.path.join(self.unit_test_dir, '48 reconfigure')
2370        self.init(testdir, extra_args=['-Dopt1=val1', '-Dsub1:werror=true'])
2371        self.setconf('-Dopt2=val2')
2372
2373        self.__reconfigure()
2374
2375        out = self.init(testdir, extra_args=['--reconfigure', '-Dopt3=val3'])
2376        self.assertRegex(out, 'Regenerating configuration from scratch')
2377        self.assertRegex(out, 'opt1 val1')
2378        self.assertRegex(out, 'opt2 val2')
2379        self.assertRegex(out, 'opt3 val3')
2380        self.assertRegex(out, 'opt4 default4')
2381        self.assertRegex(out, 'sub1:werror true')
2382        self.build()
2383        self.run_tests()
2384
2385        # Create a file in builddir and verify wipe command removes it
2386        filename = os.path.join(self.builddir, 'something')
2387        open(filename, 'w', encoding='utf-8').close()
2388        self.assertTrue(os.path.exists(filename))
2389        out = self.init(testdir, extra_args=['--wipe', '-Dopt4=val4'])
2390        self.assertFalse(os.path.exists(filename))
2391        self.assertRegex(out, 'opt1 val1')
2392        self.assertRegex(out, 'opt2 val2')
2393        self.assertRegex(out, 'opt3 val3')
2394        self.assertRegex(out, 'opt4 val4')
2395        self.assertRegex(out, 'sub1:werror true')
2396        self.assertTrue(Path(self.builddir, '.gitignore').exists())
2397        self.build()
2398        self.run_tests()
2399
2400    def test_wipe_from_builddir(self):
2401        testdir = os.path.join(self.common_test_dir, '157 custom target subdir depend files')
2402        self.init(testdir)
2403        self.__reconfigure()
2404        self.init(testdir, extra_args=['--wipe'], workdir=self.builddir)
2405
2406    def test_target_construct_id_from_path(self):
2407        # This id is stable but not guessable.
2408        # The test is supposed to prevent unintentional
2409        # changes of target ID generation.
2410        target_id = Target.construct_id_from_path('some/obscure/subdir',
2411                                                  'target-id', '@suffix')
2412        self.assertEqual('5e002d3@@target-id@suffix', target_id)
2413        target_id = Target.construct_id_from_path('subproject/foo/subdir/bar',
2414                                                  'target2-id', '@other')
2415        self.assertEqual('81d46d1@@target2-id@other', target_id)
2416
2417    def test_introspect_projectinfo_without_configured_build(self):
2418        testfile = os.path.join(self.common_test_dir, '33 run program', 'meson.build')
2419        res = self.introspect_directory(testfile, '--projectinfo')
2420        self.assertEqual(set(res['buildsystem_files']), {'meson.build'})
2421        self.assertEqual(res['version'], 'undefined')
2422        self.assertEqual(res['descriptive_name'], 'run command')
2423        self.assertEqual(res['subprojects'], [])
2424
2425        testfile = os.path.join(self.common_test_dir, '40 options', 'meson.build')
2426        res = self.introspect_directory(testfile, '--projectinfo')
2427        self.assertEqual(set(res['buildsystem_files']), {'meson_options.txt', 'meson.build'})
2428        self.assertEqual(res['version'], 'undefined')
2429        self.assertEqual(res['descriptive_name'], 'options')
2430        self.assertEqual(res['subprojects'], [])
2431
2432        testfile = os.path.join(self.common_test_dir, '43 subproject options', 'meson.build')
2433        res = self.introspect_directory(testfile, '--projectinfo')
2434        self.assertEqual(set(res['buildsystem_files']), {'meson_options.txt', 'meson.build'})
2435        self.assertEqual(res['version'], 'undefined')
2436        self.assertEqual(res['descriptive_name'], 'suboptions')
2437        self.assertEqual(len(res['subprojects']), 1)
2438        subproject_files = {f.replace('\\', '/') for f in res['subprojects'][0]['buildsystem_files']}
2439        self.assertEqual(subproject_files, {'subprojects/subproject/meson_options.txt', 'subprojects/subproject/meson.build'})
2440        self.assertEqual(res['subprojects'][0]['name'], 'subproject')
2441        self.assertEqual(res['subprojects'][0]['version'], 'undefined')
2442        self.assertEqual(res['subprojects'][0]['descriptive_name'], 'subproject')
2443
2444    def test_introspect_projectinfo_subprojects(self):
2445        testdir = os.path.join(self.common_test_dir, '98 subproject subdir')
2446        self.init(testdir)
2447        res = self.introspect('--projectinfo')
2448        expected = {
2449            'descriptive_name': 'proj',
2450            'version': 'undefined',
2451            'subproject_dir': 'subprojects',
2452            'subprojects': [
2453                {
2454                    'descriptive_name': 'sub',
2455                    'name': 'sub',
2456                    'version': '1.0'
2457                },
2458                {
2459                    'descriptive_name': 'sub_implicit',
2460                    'name': 'sub_implicit',
2461                    'version': '1.0',
2462                },
2463                {
2464                    'descriptive_name': 'sub-novar',
2465                    'name': 'sub_novar',
2466                    'version': '1.0',
2467                },
2468                {
2469                    'descriptive_name': 'sub_static',
2470                    'name': 'sub_static',
2471                    'version': 'undefined'
2472                },
2473                {
2474                    'descriptive_name': 'subsub',
2475                    'name': 'subsub',
2476                    'version': 'undefined'
2477                },
2478                {
2479                    'descriptive_name': 'subsubsub',
2480                    'name': 'subsubsub',
2481                    'version': 'undefined'
2482                },
2483            ]
2484        }
2485        res['subprojects'] = sorted(res['subprojects'], key=lambda i: i['name'])
2486        self.assertDictEqual(expected, res)
2487
2488    def test_introspection_target_subproject(self):
2489        testdir = os.path.join(self.common_test_dir, '42 subproject')
2490        self.init(testdir)
2491        res = self.introspect('--targets')
2492
2493        expected = {
2494            'sublib': 'sublib',
2495            'simpletest': 'sublib',
2496            'user': None
2497        }
2498
2499        for entry in res:
2500            name = entry['name']
2501            self.assertEqual(entry['subproject'], expected[name])
2502
2503    def test_introspect_projectinfo_subproject_dir(self):
2504        testdir = os.path.join(self.common_test_dir, '75 custom subproject dir')
2505        self.init(testdir)
2506        res = self.introspect('--projectinfo')
2507
2508        self.assertEqual(res['subproject_dir'], 'custom_subproject_dir')
2509
2510    def test_introspect_projectinfo_subproject_dir_from_source(self):
2511        testfile = os.path.join(self.common_test_dir, '75 custom subproject dir', 'meson.build')
2512        res = self.introspect_directory(testfile, '--projectinfo')
2513
2514        self.assertEqual(res['subproject_dir'], 'custom_subproject_dir')
2515
2516    @skipIfNoExecutable('clang-format')
2517    def test_clang_format(self):
2518        if self.backend is not Backend.ninja:
2519            raise SkipTest(f'Clang-format is for now only supported on Ninja, not {self.backend.name}')
2520        testdir = os.path.join(self.unit_test_dir, '54 clang-format')
2521        testfile = os.path.join(testdir, 'prog.c')
2522        badfile = os.path.join(testdir, 'prog_orig_c')
2523        goodfile = os.path.join(testdir, 'prog_expected_c')
2524        testheader = os.path.join(testdir, 'header.h')
2525        badheader = os.path.join(testdir, 'header_orig_h')
2526        goodheader = os.path.join(testdir, 'header_expected_h')
2527        try:
2528            shutil.copyfile(badfile, testfile)
2529            shutil.copyfile(badheader, testheader)
2530            self.init(testdir)
2531            self.assertNotEqual(Path(testfile).read_text(encoding='utf-8'),
2532                                Path(goodfile).read_text(encoding='utf-8'))
2533            self.assertNotEqual(Path(testheader).read_text(encoding='utf-8'),
2534                                Path(goodheader).read_text(encoding='utf-8'))
2535            self.run_target('clang-format')
2536            self.assertEqual(Path(testheader).read_text(encoding='utf-8'),
2537                             Path(goodheader).read_text(encoding='utf-8'))
2538        finally:
2539            if os.path.exists(testfile):
2540                os.unlink(testfile)
2541            if os.path.exists(testheader):
2542                os.unlink(testheader)
2543
2544    @skipIfNoExecutable('clang-tidy')
2545    def test_clang_tidy(self):
2546        if self.backend is not Backend.ninja:
2547            raise SkipTest(f'Clang-tidy is for now only supported on Ninja, not {self.backend.name}')
2548        if shutil.which('c++') is None:
2549            raise SkipTest('Clang-tidy breaks when ccache is used and "c++" not in path.')
2550        if is_osx():
2551            raise SkipTest('Apple ships a broken clang-tidy that chokes on -pipe.')
2552        testdir = os.path.join(self.unit_test_dir, '69 clang-tidy')
2553        dummydir = os.path.join(testdir, 'dummydir.h')
2554        self.init(testdir, override_envvars={'CXX': 'c++'})
2555        out = self.run_target('clang-tidy')
2556        self.assertIn('cttest.cpp:4:20', out)
2557        self.assertNotIn(dummydir, out)
2558
2559    def test_identity_cross(self):
2560        testdir = os.path.join(self.unit_test_dir, '70 cross')
2561        # Do a build to generate a cross file where the host is this target
2562        self.init(testdir, extra_args=['-Dgenerate=true'])
2563        self.meson_cross_file = os.path.join(self.builddir, "crossfile")
2564        self.assertTrue(os.path.exists(self.meson_cross_file))
2565        # Now verify that this is detected as cross
2566        self.new_builddir()
2567        self.init(testdir)
2568
2569    def test_introspect_buildoptions_without_configured_build(self):
2570        testdir = os.path.join(self.unit_test_dir, '59 introspect buildoptions')
2571        testfile = os.path.join(testdir, 'meson.build')
2572        res_nb = self.introspect_directory(testfile, ['--buildoptions'] + self.meson_args)
2573        self.init(testdir, default_args=False)
2574        res_wb = self.introspect('--buildoptions')
2575        self.maxDiff = None
2576        # XXX: These now generate in a different order, is that okay?
2577        self.assertListEqual(sorted(res_nb, key=lambda x: x['name']), sorted(res_wb, key=lambda x: x['name']))
2578
2579    def test_meson_configure_from_source_does_not_crash(self):
2580        testdir = os.path.join(self.unit_test_dir, '59 introspect buildoptions')
2581        self._run(self.mconf_command + [testdir])
2582
2583    def test_introspect_buildoptions_cross_only(self):
2584        testdir = os.path.join(self.unit_test_dir, '83 cross only introspect')
2585        testfile = os.path.join(testdir, 'meson.build')
2586        res = self.introspect_directory(testfile, ['--buildoptions'] + self.meson_args)
2587        optnames = [o['name'] for o in res]
2588        self.assertIn('c_args', optnames)
2589        self.assertNotIn('build.c_args', optnames)
2590
2591    def test_introspect_json_flat(self):
2592        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2593        out = self.init(testdir, extra_args=['-Dlayout=flat'])
2594        infodir = os.path.join(self.builddir, 'meson-info')
2595        self.assertPathExists(infodir)
2596
2597        with open(os.path.join(infodir, 'intro-targets.json'), encoding='utf-8') as fp:
2598            targets = json.load(fp)
2599
2600        for i in targets:
2601            for out in i['filename']:
2602                assert os.path.relpath(out, self.builddir).startswith('meson-out')
2603
2604    def test_introspect_json_dump(self):
2605        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2606        self.init(testdir)
2607        infodir = os.path.join(self.builddir, 'meson-info')
2608        self.assertPathExists(infodir)
2609
2610        def assertKeyTypes(key_type_list, obj, strict: bool = True):
2611            for i in key_type_list:
2612                if isinstance(i[1], (list, tuple)) and None in i[1]:
2613                    i = (i[0], tuple([x for x in i[1] if x is not None]))
2614                    if i[0] not in obj or obj[i[0]] is None:
2615                        continue
2616                self.assertIn(i[0], obj)
2617                self.assertIsInstance(obj[i[0]], i[1])
2618            if strict:
2619                for k in obj.keys():
2620                    found = False
2621                    for i in key_type_list:
2622                        if k == i[0]:
2623                            found = True
2624                            break
2625                    self.assertTrue(found, f'Key "{k}" not in expected list')
2626
2627        root_keylist = [
2628            ('benchmarks', list),
2629            ('buildoptions', list),
2630            ('buildsystem_files', list),
2631            ('dependencies', list),
2632            ('installed', dict),
2633            ('projectinfo', dict),
2634            ('targets', list),
2635            ('tests', list),
2636        ]
2637
2638        test_keylist = [
2639            ('cmd', list),
2640            ('env', dict),
2641            ('name', str),
2642            ('timeout', int),
2643            ('suite', list),
2644            ('is_parallel', bool),
2645            ('protocol', str),
2646            ('depends', list),
2647            ('workdir', (str, None)),
2648            ('priority', int),
2649        ]
2650
2651        buildoptions_keylist = [
2652            ('name', str),
2653            ('section', str),
2654            ('type', str),
2655            ('description', str),
2656            ('machine', str),
2657            ('choices', (list, None)),
2658            ('value', (str, int, bool, list)),
2659        ]
2660
2661        buildoptions_typelist = [
2662            ('combo', str, [('choices', list)]),
2663            ('string', str, []),
2664            ('boolean', bool, []),
2665            ('integer', int, []),
2666            ('array', list, []),
2667        ]
2668
2669        buildoptions_sections = ['core', 'backend', 'base', 'compiler', 'directory', 'user', 'test']
2670        buildoptions_machines = ['any', 'build', 'host']
2671
2672        dependencies_typelist = [
2673            ('name', str),
2674            ('version', str),
2675            ('compile_args', list),
2676            ('link_args', list),
2677        ]
2678
2679        targets_typelist = [
2680            ('name', str),
2681            ('id', str),
2682            ('type', str),
2683            ('defined_in', str),
2684            ('filename', list),
2685            ('build_by_default', bool),
2686            ('target_sources', list),
2687            ('extra_files', list),
2688            ('subproject', (str, None)),
2689            ('install_filename', (list, None)),
2690            ('installed', bool),
2691        ]
2692
2693        targets_sources_typelist = [
2694            ('language', str),
2695            ('compiler', list),
2696            ('parameters', list),
2697            ('sources', list),
2698            ('generated_sources', list),
2699        ]
2700
2701        # First load all files
2702        res = {}
2703        for i in root_keylist:
2704            curr = os.path.join(infodir, 'intro-{}.json'.format(i[0]))
2705            self.assertPathExists(curr)
2706            with open(curr, encoding='utf-8') as fp:
2707                res[i[0]] = json.load(fp)
2708
2709        assertKeyTypes(root_keylist, res)
2710
2711        # Match target ids to input and output files for ease of reference
2712        src_to_id = {}
2713        out_to_id = {}
2714        name_to_out = {}
2715        for i in res['targets']:
2716            print(json.dump(i, sys.stdout))
2717            out_to_id.update({os.path.relpath(out, self.builddir): i['id']
2718                              for out in i['filename']})
2719            name_to_out.update({i['name']: i['filename']})
2720            for group in i['target_sources']:
2721                src_to_id.update({os.path.relpath(src, testdir): i['id']
2722                                  for src in group['sources']})
2723
2724        # Check Tests and benchmarks
2725        tests_to_find = ['test case 1', 'test case 2', 'benchmark 1']
2726        deps_to_find = {'test case 1': [src_to_id['t1.cpp']],
2727                        'test case 2': [src_to_id['t2.cpp'], src_to_id['t3.cpp']],
2728                        'benchmark 1': [out_to_id['file2'], out_to_id['file3'], out_to_id['file4'], src_to_id['t3.cpp']]}
2729        for i in res['benchmarks'] + res['tests']:
2730            assertKeyTypes(test_keylist, i)
2731            if i['name'] in tests_to_find:
2732                tests_to_find.remove(i['name'])
2733            self.assertEqual(sorted(i['depends']),
2734                             sorted(deps_to_find[i['name']]))
2735        self.assertListEqual(tests_to_find, [])
2736
2737        # Check buildoptions
2738        buildopts_to_find = {'cpp_std': 'c++11'}
2739        for i in res['buildoptions']:
2740            assertKeyTypes(buildoptions_keylist, i)
2741            valid_type = False
2742            for j in buildoptions_typelist:
2743                if i['type'] == j[0]:
2744                    self.assertIsInstance(i['value'], j[1])
2745                    assertKeyTypes(j[2], i, strict=False)
2746                    valid_type = True
2747                    break
2748
2749            self.assertIn(i['section'], buildoptions_sections)
2750            self.assertIn(i['machine'], buildoptions_machines)
2751            self.assertTrue(valid_type)
2752            if i['name'] in buildopts_to_find:
2753                self.assertEqual(i['value'], buildopts_to_find[i['name']])
2754                buildopts_to_find.pop(i['name'], None)
2755        self.assertDictEqual(buildopts_to_find, {})
2756
2757        # Check buildsystem_files
2758        bs_files = ['meson.build', 'meson_options.txt', 'sharedlib/meson.build', 'staticlib/meson.build']
2759        bs_files = [os.path.join(testdir, x) for x in bs_files]
2760        self.assertPathListEqual(list(sorted(res['buildsystem_files'])), list(sorted(bs_files)))
2761
2762        # Check dependencies
2763        dependencies_to_find = ['threads']
2764        for i in res['dependencies']:
2765            assertKeyTypes(dependencies_typelist, i)
2766            if i['name'] in dependencies_to_find:
2767                dependencies_to_find.remove(i['name'])
2768        self.assertListEqual(dependencies_to_find, [])
2769
2770        # Check projectinfo
2771        self.assertDictEqual(res['projectinfo'], {'version': '1.2.3', 'descriptive_name': 'introspection', 'subproject_dir': 'subprojects', 'subprojects': []})
2772
2773        # Check targets
2774        targets_to_find = {
2775            'sharedTestLib': ('shared library', True, False, 'sharedlib/meson.build',
2776                              [os.path.join(testdir, 'sharedlib', 'shared.cpp')]),
2777            'staticTestLib': ('static library', True, False, 'staticlib/meson.build',
2778                              [os.path.join(testdir, 'staticlib', 'static.c')]),
2779            'custom target test 1': ('custom', False, False, 'meson.build',
2780                                     [os.path.join(testdir, 'cp.py')]),
2781            'custom target test 2': ('custom', False, False, 'meson.build',
2782                                     name_to_out['custom target test 1']),
2783            'test1': ('executable', True, True, 'meson.build',
2784                      [os.path.join(testdir, 't1.cpp')]),
2785            'test2': ('executable', True, False, 'meson.build',
2786                      [os.path.join(testdir, 't2.cpp')]),
2787            'test3': ('executable', True, False, 'meson.build',
2788                      [os.path.join(testdir, 't3.cpp')]),
2789            'custom target test 3': ('custom', False, False, 'meson.build',
2790                                     name_to_out['test3']),
2791        }
2792        for i in res['targets']:
2793            assertKeyTypes(targets_typelist, i)
2794            if i['name'] in targets_to_find:
2795                tgt = targets_to_find[i['name']]
2796                self.assertEqual(i['type'], tgt[0])
2797                self.assertEqual(i['build_by_default'], tgt[1])
2798                self.assertEqual(i['installed'], tgt[2])
2799                self.assertPathEqual(i['defined_in'], os.path.join(testdir, tgt[3]))
2800                targets_to_find.pop(i['name'], None)
2801            for j in i['target_sources']:
2802                assertKeyTypes(targets_sources_typelist, j)
2803                self.assertEqual(j['sources'], [os.path.normpath(f) for f in tgt[4]])
2804        self.assertDictEqual(targets_to_find, {})
2805
2806    def test_introspect_file_dump_equals_all(self):
2807        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2808        self.init(testdir)
2809        res_all = self.introspect('--all')
2810        res_file = {}
2811
2812        root_keylist = [
2813            'benchmarks',
2814            'buildoptions',
2815            'buildsystem_files',
2816            'dependencies',
2817            'installed',
2818            'install_plan',
2819            'projectinfo',
2820            'targets',
2821            'tests',
2822        ]
2823
2824        infodir = os.path.join(self.builddir, 'meson-info')
2825        self.assertPathExists(infodir)
2826        for i in root_keylist:
2827            curr = os.path.join(infodir, f'intro-{i}.json')
2828            self.assertPathExists(curr)
2829            with open(curr, encoding='utf-8') as fp:
2830                res_file[i] = json.load(fp)
2831
2832        self.assertEqual(res_all, res_file)
2833
2834    def test_introspect_meson_info(self):
2835        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2836        introfile = os.path.join(self.builddir, 'meson-info', 'meson-info.json')
2837        self.init(testdir)
2838        self.assertPathExists(introfile)
2839        with open(introfile, encoding='utf-8') as fp:
2840            res1 = json.load(fp)
2841
2842        for i in ['meson_version', 'directories', 'introspection', 'build_files_updated', 'error']:
2843            self.assertIn(i, res1)
2844
2845        self.assertEqual(res1['error'], False)
2846        self.assertEqual(res1['build_files_updated'], True)
2847
2848    def test_introspect_config_update(self):
2849        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2850        introfile = os.path.join(self.builddir, 'meson-info', 'intro-buildoptions.json')
2851        self.init(testdir)
2852        self.assertPathExists(introfile)
2853        with open(introfile, encoding='utf-8') as fp:
2854            res1 = json.load(fp)
2855
2856        for i in res1:
2857            if i['name'] == 'cpp_std':
2858                i['value'] = 'c++14'
2859            if i['name'] == 'build.cpp_std':
2860                i['value'] = 'c++14'
2861            if i['name'] == 'buildtype':
2862                i['value'] = 'release'
2863            if i['name'] == 'optimization':
2864                i['value'] = '3'
2865            if i['name'] == 'debug':
2866                i['value'] = False
2867
2868        self.setconf('-Dcpp_std=c++14')
2869        self.setconf('-Dbuildtype=release')
2870
2871        with open(introfile, encoding='utf-8') as fp:
2872            res2 = json.load(fp)
2873
2874        self.assertListEqual(res1, res2)
2875
2876    def test_introspect_targets_from_source(self):
2877        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2878        testfile = os.path.join(testdir, 'meson.build')
2879        introfile = os.path.join(self.builddir, 'meson-info', 'intro-targets.json')
2880        self.init(testdir)
2881        self.assertPathExists(introfile)
2882        with open(introfile, encoding='utf-8') as fp:
2883            res_wb = json.load(fp)
2884
2885        res_nb = self.introspect_directory(testfile, ['--targets'] + self.meson_args)
2886
2887        # Account for differences in output
2888        res_wb = [i for i in res_wb if i['type'] != 'custom']
2889        for i in res_wb:
2890            i['filename'] = [os.path.relpath(x, self.builddir) for x in i['filename']]
2891            if 'install_filename' in i:
2892                del i['install_filename']
2893
2894            sources = []
2895            for j in i['target_sources']:
2896                sources += j['sources']
2897            i['target_sources'] = [{
2898                'language': 'unknown',
2899                'compiler': [],
2900                'parameters': [],
2901                'sources': sources,
2902                'generated_sources': []
2903            }]
2904
2905        self.maxDiff = None
2906        self.assertListEqual(res_nb, res_wb)
2907
2908    def test_introspect_ast_source(self):
2909        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2910        testfile = os.path.join(testdir, 'meson.build')
2911        res_nb = self.introspect_directory(testfile, ['--ast'] + self.meson_args)
2912
2913        node_counter = {}
2914
2915        def accept_node(json_node):
2916            self.assertIsInstance(json_node, dict)
2917            for i in ['lineno', 'colno', 'end_lineno', 'end_colno']:
2918                self.assertIn(i, json_node)
2919                self.assertIsInstance(json_node[i], int)
2920            self.assertIn('node', json_node)
2921            n = json_node['node']
2922            self.assertIsInstance(n, str)
2923            self.assertIn(n, nodes)
2924            if n not in node_counter:
2925                node_counter[n] = 0
2926            node_counter[n] = node_counter[n] + 1
2927            for nodeDesc in nodes[n]:
2928                key = nodeDesc[0]
2929                func = nodeDesc[1]
2930                self.assertIn(key, json_node)
2931                if func is None:
2932                    tp = nodeDesc[2]
2933                    self.assertIsInstance(json_node[key], tp)
2934                    continue
2935                func(json_node[key])
2936
2937        def accept_node_list(node_list):
2938            self.assertIsInstance(node_list, list)
2939            for i in node_list:
2940                accept_node(i)
2941
2942        def accept_kwargs(kwargs):
2943            self.assertIsInstance(kwargs, list)
2944            for i in kwargs:
2945                self.assertIn('key', i)
2946                self.assertIn('val', i)
2947                accept_node(i['key'])
2948                accept_node(i['val'])
2949
2950        nodes = {
2951            'BooleanNode': [('value', None, bool)],
2952            'IdNode': [('value', None, str)],
2953            'NumberNode': [('value', None, int)],
2954            'StringNode': [('value', None, str)],
2955            'FormatStringNode': [('value', None, str)],
2956            'ContinueNode': [],
2957            'BreakNode': [],
2958            'ArgumentNode': [('positional', accept_node_list), ('kwargs', accept_kwargs)],
2959            'ArrayNode': [('args', accept_node)],
2960            'DictNode': [('args', accept_node)],
2961            'EmptyNode': [],
2962            'OrNode': [('left', accept_node), ('right', accept_node)],
2963            'AndNode': [('left', accept_node), ('right', accept_node)],
2964            'ComparisonNode': [('left', accept_node), ('right', accept_node), ('ctype', None, str)],
2965            'ArithmeticNode': [('left', accept_node), ('right', accept_node), ('op', None, str)],
2966            'NotNode': [('right', accept_node)],
2967            'CodeBlockNode': [('lines', accept_node_list)],
2968            'IndexNode': [('object', accept_node), ('index', accept_node)],
2969            'MethodNode': [('object', accept_node), ('args', accept_node), ('name', None, str)],
2970            'FunctionNode': [('args', accept_node), ('name', None, str)],
2971            'AssignmentNode': [('value', accept_node), ('var_name', None, str)],
2972            'PlusAssignmentNode': [('value', accept_node), ('var_name', None, str)],
2973            'ForeachClauseNode': [('items', accept_node), ('block', accept_node), ('varnames', None, list)],
2974            'IfClauseNode': [('ifs', accept_node_list), ('else', accept_node)],
2975            'IfNode': [('condition', accept_node), ('block', accept_node)],
2976            'UMinusNode': [('right', accept_node)],
2977            'TernaryNode': [('condition', accept_node), ('true', accept_node), ('false', accept_node)],
2978        }
2979
2980        accept_node(res_nb)
2981
2982        for n, c in [('ContinueNode', 2), ('BreakNode', 1), ('NotNode', 3)]:
2983            self.assertIn(n, node_counter)
2984            self.assertEqual(node_counter[n], c)
2985
2986    def test_introspect_dependencies_from_source(self):
2987        testdir = os.path.join(self.unit_test_dir, '57 introspection')
2988        testfile = os.path.join(testdir, 'meson.build')
2989        res_nb = self.introspect_directory(testfile, ['--scan-dependencies'] + self.meson_args)
2990        expected = [
2991            {
2992                'name': 'threads',
2993                'required': True,
2994                'version': [],
2995                'has_fallback': False,
2996                'conditional': False
2997            },
2998            {
2999                'name': 'zlib',
3000                'required': False,
3001                'version': [],
3002                'has_fallback': False,
3003                'conditional': False
3004            },
3005            {
3006                'name': 'bugDep1',
3007                'required': True,
3008                'version': [],
3009                'has_fallback': False,
3010                'conditional': False
3011            },
3012            {
3013                'name': 'somethingthatdoesnotexist',
3014                'required': True,
3015                'version': ['>=1.2.3'],
3016                'has_fallback': False,
3017                'conditional': True
3018            },
3019            {
3020                'name': 'look_i_have_a_fallback',
3021                'required': True,
3022                'version': ['>=1.0.0', '<=99.9.9'],
3023                'has_fallback': True,
3024                'conditional': True
3025            }
3026        ]
3027        self.maxDiff = None
3028        self.assertListEqual(res_nb, expected)
3029
3030    def test_unstable_coredata(self):
3031        testdir = os.path.join(self.common_test_dir, '1 trivial')
3032        self.init(testdir)
3033        # just test that the command does not fail (e.g. because it throws an exception)
3034        self._run([*self.meson_command, 'unstable-coredata', self.builddir])
3035
3036    @skip_if_no_cmake
3037    def test_cmake_prefix_path(self):
3038        testdir = os.path.join(self.unit_test_dir, '63 cmake_prefix_path')
3039        self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')])
3040
3041    @skip_if_no_cmake
3042    def test_cmake_parser(self):
3043        testdir = os.path.join(self.unit_test_dir, '64 cmake parser')
3044        self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')])
3045
3046    def test_alias_target(self):
3047        testdir = os.path.join(self.unit_test_dir, '65 alias target')
3048        self.init(testdir)
3049        self.build()
3050        self.assertPathDoesNotExist(os.path.join(self.builddir, 'prog' + exe_suffix))
3051        self.assertPathDoesNotExist(os.path.join(self.builddir, 'hello.txt'))
3052        self.run_target('build-all')
3053        self.assertPathExists(os.path.join(self.builddir, 'prog' + exe_suffix))
3054        self.assertPathExists(os.path.join(self.builddir, 'hello.txt'))
3055        out = self.run_target('aliased-run')
3056        self.assertIn('a run target was here', out)
3057
3058    def test_configure(self):
3059        testdir = os.path.join(self.common_test_dir, '2 cpp')
3060        self.init(testdir)
3061        self._run(self.mconf_command + [self.builddir])
3062
3063    def test_summary(self):
3064        testdir = os.path.join(self.unit_test_dir, '72 summary')
3065        out = self.init(testdir, extra_args=['-Denabled_opt=enabled'])
3066        expected = textwrap.dedent(r'''
3067            Some Subproject 2.0
3068
3069                string : bar
3070                integer: 1
3071                boolean: True
3072
3073            subsub undefined
3074
3075                Something: Some value
3076
3077            My Project 1.0
3078
3079              Configuration
3080                Some boolean   : False
3081                Another boolean: True
3082                Some string    : Hello World
3083                A list         : string
3084                                 1
3085                                 True
3086                empty list     :
3087                enabled_opt    : enabled
3088                A number       : 1
3089                yes            : YES
3090                no             : NO
3091                coma list      : a, b, c
3092
3093              Stuff
3094                missing prog   : NO
3095                existing prog  : ''' + sys.executable + '''
3096                missing dep    : NO
3097                external dep   : YES 1.2.3
3098                internal dep   : YES
3099
3100              Plugins
3101                long coma list : alpha, alphacolor, apetag, audiofx, audioparsers, auparse,
3102                                 autodetect, avi
3103
3104              Subprojects
3105                sub            : YES
3106                sub2           : NO Problem encountered: This subproject failed
3107                subsub         : YES
3108
3109              User defined options
3110                backend        : ''' + self.backend.name + '''
3111                libdir         : lib
3112                prefix         : /usr
3113                enabled_opt    : enabled
3114            ''')
3115        expected_lines = expected.split('\n')[1:]
3116        out_start = out.find(expected_lines[0])
3117        out_lines = out[out_start:].split('\n')[:len(expected_lines)]
3118        if sys.version_info < (3, 7, 0):
3119            # Dictionary order is not stable in Python <3.7, so sort the lines
3120            # while comparing
3121            expected_lines = sorted(expected_lines)
3122            out_lines = sorted(out_lines)
3123        for e, o in zip(expected_lines, out_lines):
3124            if e.startswith('    external dep'):
3125                self.assertRegex(o, r'^    external dep   : (YES [0-9.]*|NO)$')
3126            else:
3127                self.assertEqual(o, e)
3128
3129    def test_meson_compile(self):
3130        """Test the meson compile command."""
3131
3132        def get_exe_name(basename: str) -> str:
3133            if is_windows():
3134                return f'{basename}.exe'
3135            else:
3136                return basename
3137
3138        def get_shared_lib_name(basename: str) -> str:
3139            if mesonbuild.environment.detect_msys2_arch():
3140                return f'lib{basename}.dll'
3141            elif is_windows():
3142                return f'{basename}.dll'
3143            elif is_cygwin():
3144                return f'cyg{basename}.dll'
3145            elif is_osx():
3146                return f'lib{basename}.dylib'
3147            else:
3148                return f'lib{basename}.so'
3149
3150        def get_static_lib_name(basename: str) -> str:
3151            return f'lib{basename}.a'
3152
3153        # Base case (no targets or additional arguments)
3154
3155        testdir = os.path.join(self.common_test_dir, '1 trivial')
3156        self.init(testdir)
3157
3158        self._run([*self.meson_command, 'compile', '-C', self.builddir])
3159        self.assertPathExists(os.path.join(self.builddir, get_exe_name('trivialprog')))
3160
3161        # `--clean`
3162
3163        self._run([*self.meson_command, 'compile', '-C', self.builddir, '--clean'])
3164        self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
3165
3166        # Target specified in a project with unique names
3167
3168        testdir = os.path.join(self.common_test_dir, '6 linkshared')
3169        self.init(testdir, extra_args=['--wipe'])
3170        # Multiple targets and target type specified
3171        self._run([*self.meson_command, 'compile', '-C', self.builddir, 'mylib', 'mycpplib:shared_library'])
3172        # Check that we have a shared lib, but not an executable, i.e. check that target actually worked
3173        self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mylib')))
3174        self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('prog')))
3175        self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mycpplib')))
3176        self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('cppprog')))
3177
3178        # Target specified in a project with non unique names
3179
3180        testdir = os.path.join(self.common_test_dir, '185 same target name')
3181        self.init(testdir, extra_args=['--wipe'])
3182        self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo'])
3183        self.assertPathExists(os.path.join(self.builddir, get_static_lib_name('foo')))
3184        self._run([*self.meson_command, 'compile', '-C', self.builddir, 'sub/foo'])
3185        self.assertPathExists(os.path.join(self.builddir, 'sub', get_static_lib_name('foo')))
3186
3187        # run_target
3188
3189        testdir = os.path.join(self.common_test_dir, '51 run target')
3190        self.init(testdir, extra_args=['--wipe'])
3191        out = self._run([*self.meson_command, 'compile', '-C', self.builddir, 'py3hi'])
3192        self.assertIn('I am Python3.', out)
3193
3194        # `--$BACKEND-args`
3195
3196        testdir = os.path.join(self.common_test_dir, '1 trivial')
3197        if self.backend is Backend.ninja:
3198            self.init(testdir, extra_args=['--wipe'])
3199            # Dry run - should not create a program
3200            self._run([*self.meson_command, 'compile', '-C', self.builddir, '--ninja-args=-n'])
3201            self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
3202        elif self.backend is Backend.vs:
3203            self.init(testdir, extra_args=['--wipe'])
3204            self._run([*self.meson_command, 'compile', '-C', self.builddir])
3205            # Explicitly clean the target through msbuild interface
3206            self._run([*self.meson_command, 'compile', '-C', self.builddir, '--vs-args=-t:{}:Clean'.format(re.sub(r'[\%\$\@\;\.\(\)\']', '_', get_exe_name('trivialprog')))])
3207            self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
3208
3209    def test_spurious_reconfigure_built_dep_file(self):
3210        testdir = os.path.join(self.unit_test_dir, '74 dep files')
3211
3212        # Regression test: Spurious reconfigure was happening when build
3213        # directory is inside source directory.
3214        # See https://gitlab.freedesktop.org/gstreamer/gst-build/-/issues/85.
3215        srcdir = os.path.join(self.builddir, 'srctree')
3216        shutil.copytree(testdir, srcdir)
3217        builddir = os.path.join(srcdir, '_build')
3218        self.change_builddir(builddir)
3219
3220        self.init(srcdir)
3221        self.build()
3222
3223        # During first configure the file did not exist so no dependency should
3224        # have been set. A rebuild should not trigger a reconfigure.
3225        self.clean()
3226        out = self.build()
3227        self.assertNotIn('Project configured', out)
3228
3229        self.init(srcdir, extra_args=['--reconfigure'])
3230
3231        # During the reconfigure the file did exist, but is inside build
3232        # directory, so no dependency should have been set. A rebuild should not
3233        # trigger a reconfigure.
3234        self.clean()
3235        out = self.build()
3236        self.assertNotIn('Project configured', out)
3237
3238    def _test_junit(self, case: str) -> None:
3239        try:
3240            import lxml.etree as et
3241        except ImportError:
3242            raise SkipTest('lxml required, but not found.')
3243
3244        schema = et.XMLSchema(et.parse(str(Path(self.src_root) / 'data' / 'schema.xsd')))
3245
3246        self.init(case)
3247        self.run_tests()
3248
3249        junit = et.parse(str(Path(self.builddir) / 'meson-logs' / 'testlog.junit.xml'))
3250        try:
3251            schema.assertValid(junit)
3252        except et.DocumentInvalid as e:
3253            self.fail(e.error_log)
3254
3255    def test_junit_valid_tap(self):
3256        self._test_junit(os.path.join(self.common_test_dir, '206 tap tests'))
3257
3258    def test_junit_valid_exitcode(self):
3259        self._test_junit(os.path.join(self.common_test_dir, '41 test args'))
3260
3261    def test_junit_valid_gtest(self):
3262        self._test_junit(os.path.join(self.framework_test_dir, '2 gtest'))
3263
3264    def test_link_language_linker(self):
3265        # TODO: there should be some way to query how we're linking things
3266        # without resorting to reading the ninja.build file
3267        if self.backend is not Backend.ninja:
3268            raise SkipTest('This test reads the ninja file')
3269
3270        testdir = os.path.join(self.common_test_dir, '225 link language')
3271        self.init(testdir)
3272
3273        build_ninja = os.path.join(self.builddir, 'build.ninja')
3274        with open(build_ninja, encoding='utf-8') as f:
3275            contents = f.read()
3276
3277        self.assertRegex(contents, r'build main(\.exe)?.*: c_LINKER')
3278        self.assertRegex(contents, r'build (lib|cyg)?mylib.*: c_LINKER')
3279
3280    def test_commands_documented(self):
3281        '''
3282        Test that all listed meson commands are documented in Commands.md.
3283        '''
3284
3285        # The docs directory is not in release tarballs.
3286        if not os.path.isdir('docs'):
3287            raise SkipTest('Doc directory does not exist.')
3288        doc_path = 'docs/markdown/Commands.md'
3289
3290        md = None
3291        with open(doc_path, encoding='utf-8') as f:
3292            md = f.read()
3293        self.assertIsNotNone(md)
3294
3295        ## Get command sections
3296
3297        section_pattern = re.compile(r'^### (.+)$', re.MULTILINE)
3298        md_command_section_matches = [i for i in section_pattern.finditer(md)]
3299        md_command_sections = dict()
3300        for i, s in enumerate(md_command_section_matches):
3301            section_end = len(md) if i == len(md_command_section_matches) - 1 else md_command_section_matches[i + 1].start()
3302            md_command_sections[s.group(1)] = (s.start(), section_end)
3303
3304        ## Validate commands
3305
3306        md_commands = {k for k,v in md_command_sections.items()}
3307
3308        help_output = self._run(self.meson_command + ['--help'])
3309        help_commands = {c.strip() for c in re.findall(r'usage:(?:.+)?{((?:[a-z]+,*)+?)}', help_output, re.MULTILINE|re.DOTALL)[0].split(',')}
3310
3311        self.assertEqual(md_commands | {'help'}, help_commands, f'Doc file: `{doc_path}`')
3312
3313        ## Validate that each section has proper placeholders
3314
3315        def get_data_pattern(command):
3316            return re.compile(
3317                r'{{ ' + command + r'_usage.inc }}[\r\n]'
3318                r'.*?'
3319                r'{{ ' + command + r'_arguments.inc }}[\r\n]',
3320                flags = re.MULTILINE|re.DOTALL)
3321
3322        for command in md_commands:
3323            m = get_data_pattern(command).search(md, pos=md_command_sections[command][0], endpos=md_command_sections[command][1])
3324            self.assertIsNotNone(m, f'Command `{command}` is missing placeholders for dynamic data. Doc file: `{doc_path}`')
3325
3326    def _check_coverage_files(self, types=('text', 'xml', 'html')):
3327        covdir = Path(self.builddir) / 'meson-logs'
3328        files = []
3329        if 'text' in types:
3330            files.append('coverage.txt')
3331        if 'xml' in types:
3332            files.append('coverage.xml')
3333        if 'html' in types:
3334            files.append('coveragereport/index.html')
3335        for f in files:
3336            self.assertTrue((covdir / f).is_file(), msg=f'{f} is not a file')
3337
3338    def test_coverage(self):
3339        if mesonbuild.environment.detect_msys2_arch():
3340            raise SkipTest('Skipped due to problems with coverage on MSYS2')
3341        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
3342        if not gcovr_exe:
3343            raise SkipTest('gcovr not found, or too old')
3344        testdir = os.path.join(self.common_test_dir, '1 trivial')
3345        env = get_fake_env(testdir, self.builddir, self.prefix)
3346        cc = detect_c_compiler(env, MachineChoice.HOST)
3347        if cc.get_id() == 'clang':
3348            if not mesonbuild.environment.detect_llvm_cov():
3349                raise SkipTest('llvm-cov not found')
3350        if cc.get_id() == 'msvc':
3351            raise SkipTest('Test only applies to non-MSVC compilers')
3352        self.init(testdir, extra_args=['-Db_coverage=true'])
3353        self.build()
3354        self.run_tests()
3355        self.run_target('coverage')
3356        self._check_coverage_files()
3357
3358    def test_coverage_complex(self):
3359        if mesonbuild.environment.detect_msys2_arch():
3360            raise SkipTest('Skipped due to problems with coverage on MSYS2')
3361        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
3362        if not gcovr_exe:
3363            raise SkipTest('gcovr not found, or too old')
3364        testdir = os.path.join(self.common_test_dir, '105 generatorcustom')
3365        env = get_fake_env(testdir, self.builddir, self.prefix)
3366        cc = detect_c_compiler(env, MachineChoice.HOST)
3367        if cc.get_id() == 'clang':
3368            if not mesonbuild.environment.detect_llvm_cov():
3369                raise SkipTest('llvm-cov not found')
3370        if cc.get_id() == 'msvc':
3371            raise SkipTest('Test only applies to non-MSVC compilers')
3372        self.init(testdir, extra_args=['-Db_coverage=true'])
3373        self.build()
3374        self.run_tests()
3375        self.run_target('coverage')
3376        self._check_coverage_files()
3377
3378    def test_coverage_html(self):
3379        if mesonbuild.environment.detect_msys2_arch():
3380            raise SkipTest('Skipped due to problems with coverage on MSYS2')
3381        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
3382        if not gcovr_exe:
3383            raise SkipTest('gcovr not found, or too old')
3384        testdir = os.path.join(self.common_test_dir, '1 trivial')
3385        env = get_fake_env(testdir, self.builddir, self.prefix)
3386        cc = detect_c_compiler(env, MachineChoice.HOST)
3387        if cc.get_id() == 'clang':
3388            if not mesonbuild.environment.detect_llvm_cov():
3389                raise SkipTest('llvm-cov not found')
3390        if cc.get_id() == 'msvc':
3391            raise SkipTest('Test only applies to non-MSVC compilers')
3392        self.init(testdir, extra_args=['-Db_coverage=true'])
3393        self.build()
3394        self.run_tests()
3395        self.run_target('coverage-html')
3396        self._check_coverage_files(['html'])
3397
3398    def test_coverage_text(self):
3399        if mesonbuild.environment.detect_msys2_arch():
3400            raise SkipTest('Skipped due to problems with coverage on MSYS2')
3401        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
3402        if not gcovr_exe:
3403            raise SkipTest('gcovr not found, or too old')
3404        testdir = os.path.join(self.common_test_dir, '1 trivial')
3405        env = get_fake_env(testdir, self.builddir, self.prefix)
3406        cc = detect_c_compiler(env, MachineChoice.HOST)
3407        if cc.get_id() == 'clang':
3408            if not mesonbuild.environment.detect_llvm_cov():
3409                raise SkipTest('llvm-cov not found')
3410        if cc.get_id() == 'msvc':
3411            raise SkipTest('Test only applies to non-MSVC compilers')
3412        self.init(testdir, extra_args=['-Db_coverage=true'])
3413        self.build()
3414        self.run_tests()
3415        self.run_target('coverage-text')
3416        self._check_coverage_files(['text'])
3417
3418    def test_coverage_xml(self):
3419        if mesonbuild.environment.detect_msys2_arch():
3420            raise SkipTest('Skipped due to problems with coverage on MSYS2')
3421        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
3422        if not gcovr_exe:
3423            raise SkipTest('gcovr not found, or too old')
3424        testdir = os.path.join(self.common_test_dir, '1 trivial')
3425        env = get_fake_env(testdir, self.builddir, self.prefix)
3426        cc = detect_c_compiler(env, MachineChoice.HOST)
3427        if cc.get_id() == 'clang':
3428            if not mesonbuild.environment.detect_llvm_cov():
3429                raise SkipTest('llvm-cov not found')
3430        if cc.get_id() == 'msvc':
3431            raise SkipTest('Test only applies to non-MSVC compilers')
3432        self.init(testdir, extra_args=['-Db_coverage=true'])
3433        self.build()
3434        self.run_tests()
3435        self.run_target('coverage-xml')
3436        self._check_coverage_files(['xml'])
3437
3438    def test_coverage_escaping(self):
3439        if mesonbuild.environment.detect_msys2_arch():
3440            raise SkipTest('Skipped due to problems with coverage on MSYS2')
3441        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
3442        if not gcovr_exe:
3443            raise SkipTest('gcovr not found, or too old')
3444        testdir = os.path.join(self.common_test_dir, '243 escape++')
3445        env = get_fake_env(testdir, self.builddir, self.prefix)
3446        cc = detect_c_compiler(env, MachineChoice.HOST)
3447        if cc.get_id() == 'clang':
3448            if not mesonbuild.environment.detect_llvm_cov():
3449                raise SkipTest('llvm-cov not found')
3450        if cc.get_id() == 'msvc':
3451            raise SkipTest('Test only applies to non-MSVC compilers')
3452        self.init(testdir, extra_args=['-Db_coverage=true'])
3453        self.build()
3454        self.run_tests()
3455        self.run_target('coverage')
3456        self._check_coverage_files()
3457
3458    def test_cross_file_constants(self):
3459        with temp_filename() as crossfile1, temp_filename() as crossfile2:
3460            with open(crossfile1, 'w', encoding='utf-8') as f:
3461                f.write(textwrap.dedent(
3462                    '''
3463                    [constants]
3464                    compiler = 'gcc'
3465                    '''))
3466            with open(crossfile2, 'w', encoding='utf-8') as f:
3467                f.write(textwrap.dedent(
3468                    '''
3469                    [constants]
3470                    toolchain = '/toolchain/'
3471                    common_flags = ['--sysroot=' + toolchain / 'sysroot']
3472
3473                    [properties]
3474                    c_args = common_flags + ['-DSOMETHING']
3475                    cpp_args = c_args + ['-DSOMETHING_ELSE']
3476
3477                    [binaries]
3478                    c = toolchain / compiler
3479                    '''))
3480
3481            values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2])
3482            self.assertEqual(values['binaries']['c'], '/toolchain/gcc')
3483            self.assertEqual(values['properties']['c_args'],
3484                             ['--sysroot=/toolchain/sysroot', '-DSOMETHING'])
3485            self.assertEqual(values['properties']['cpp_args'],
3486                             ['--sysroot=/toolchain/sysroot', '-DSOMETHING', '-DSOMETHING_ELSE'])
3487
3488    @skipIf(is_windows(), 'Directory cleanup fails for some reason')
3489    def test_wrap_git(self):
3490        with tempfile.TemporaryDirectory() as tmpdir:
3491            srcdir = os.path.join(tmpdir, 'src')
3492            shutil.copytree(os.path.join(self.unit_test_dir, '81 wrap-git'), srcdir)
3493            upstream = os.path.join(srcdir, 'subprojects', 'wrap_git_upstream')
3494            upstream_uri = Path(upstream).as_uri()
3495            _git_init(upstream)
3496            with open(os.path.join(srcdir, 'subprojects', 'wrap_git.wrap'), 'w', encoding='utf-8') as f:
3497                f.write(textwrap.dedent('''
3498                  [wrap-git]
3499                  url = {}
3500                  patch_directory = wrap_git_builddef
3501                  revision = master
3502                '''.format(upstream_uri)))
3503            self.init(srcdir)
3504            self.build()
3505            self.run_tests()
3506
3507    def test_multi_output_custom_target_no_warning(self):
3508        testdir = os.path.join(self.common_test_dir, '228 custom_target source')
3509
3510        out = self.init(testdir)
3511        self.assertNotRegex(out, 'WARNING:.*Using the first one.')
3512        self.build()
3513        self.run_tests()
3514
3515    @skipUnless(is_linux() and (re.search('^i.86$|^x86$|^x64$|^x86_64$|^amd64$', platform.processor()) is not None),
3516        'Requires ASM compiler for x86 or x86_64 platform currently only available on Linux CI runners')
3517    def test_nostdlib(self):
3518        testdir = os.path.join(self.unit_test_dir, '78 nostdlib')
3519        machinefile = os.path.join(self.builddir, 'machine.txt')
3520        with open(machinefile, 'w', encoding='utf-8') as f:
3521            f.write(textwrap.dedent('''
3522                [properties]
3523                c_stdlib = 'mylibc'
3524                '''))
3525
3526        # Test native C stdlib
3527        self.meson_native_file = machinefile
3528        self.init(testdir)
3529        self.build()
3530
3531        # Test cross C stdlib
3532        self.new_builddir()
3533        self.meson_native_file = None
3534        self.meson_cross_file = machinefile
3535        self.init(testdir)
3536        self.build()
3537
3538    def test_meson_version_compare(self):
3539        testdir = os.path.join(self.unit_test_dir, '82 meson version compare')
3540        out = self.init(testdir)
3541        self.assertNotRegex(out, r'WARNING')
3542
3543    def test_wrap_redirect(self):
3544        redirect_wrap = os.path.join(self.builddir, 'redirect.wrap')
3545        real_wrap = os.path.join(self.builddir, 'foo/subprojects/real.wrap')
3546        os.makedirs(os.path.dirname(real_wrap))
3547
3548        # Invalid redirect, filename must have .wrap extension
3549        with open(redirect_wrap, 'w', encoding='utf-8') as f:
3550            f.write(textwrap.dedent('''
3551                [wrap-redirect]
3552                filename = foo/subprojects/real.wrapper
3553                '''))
3554        with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be a .wrap file'):
3555            PackageDefinition(redirect_wrap)
3556
3557        # Invalid redirect, filename cannot be in parent directory
3558        with open(redirect_wrap, 'w', encoding='utf-8') as f:
3559            f.write(textwrap.dedent('''
3560                [wrap-redirect]
3561                filename = ../real.wrap
3562                '''))
3563        with self.assertRaisesRegex(WrapException, 'wrap-redirect filename cannot contain ".."'):
3564            PackageDefinition(redirect_wrap)
3565
3566        # Invalid redirect, filename must be in foo/subprojects/real.wrap
3567        with open(redirect_wrap, 'w', encoding='utf-8') as f:
3568            f.write(textwrap.dedent('''
3569                [wrap-redirect]
3570                filename = foo/real.wrap
3571                '''))
3572        with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be in the form foo/subprojects/bar.wrap'):
3573            wrap = PackageDefinition(redirect_wrap)
3574
3575        # Correct redirect
3576        with open(redirect_wrap, 'w', encoding='utf-8') as f:
3577            f.write(textwrap.dedent('''
3578                [wrap-redirect]
3579                filename = foo/subprojects/real.wrap
3580                '''))
3581        with open(real_wrap, 'w', encoding='utf-8') as f:
3582            f.write(textwrap.dedent('''
3583                [wrap-git]
3584                url = http://invalid
3585                '''))
3586        wrap = PackageDefinition(redirect_wrap)
3587        self.assertEqual(wrap.get('url'), 'http://invalid')
3588
3589    @skip_if_no_cmake
3590    def test_nested_cmake_rebuild(self) -> None:
3591        # This checks a bug where if a non-meson project is used as a third
3592        # level (or deeper) subproject it doesn't cause a rebuild if the build
3593        # files for that project are changed
3594        testdir = os.path.join(self.unit_test_dir, '85 nested subproject regenerate depends')
3595        cmakefile = Path(testdir) / 'subprojects' / 'sub2' / 'CMakeLists.txt'
3596        self.init(testdir)
3597        self.build()
3598        with cmakefile.open('a', encoding='utf-8'):
3599            os.utime(str(cmakefile))
3600        self.assertReconfiguredBuildIsNoop()
3601
3602    def test_version_file(self):
3603        srcdir = os.path.join(self.common_test_dir, '2 cpp')
3604        self.init(srcdir)
3605        projinfo = self.introspect('--projectinfo')
3606        self.assertEqual(projinfo['version'], '1.0.0')
3607
3608    def test_cflags_cppflags(self):
3609        envs = {'CPPFLAGS': '-DCPPFLAG',
3610                'CFLAGS': '-DCFLAG',
3611                'CXXFLAGS': '-DCXXFLAG'}
3612        srcdir = os.path.join(self.unit_test_dir, '89 multiple envvars')
3613        self.init(srcdir, override_envvars=envs)
3614        self.build()
3615
3616    def test_build_b_options(self) -> None:
3617        # Currently (0.57) these do nothing, but they've always been allowed
3618        srcdir = os.path.join(self.common_test_dir, '2 cpp')
3619        self.init(srcdir, extra_args=['-Dbuild.b_lto=true'])
3620
3621    def test_install_skip_subprojects(self):
3622        testdir = os.path.join(self.unit_test_dir, '92 install skip subprojects')
3623        self.init(testdir)
3624        self.build()
3625
3626        main_expected = [
3627            '',
3628            'share',
3629            'include',
3630            'foo',
3631            'bin',
3632            'share/foo',
3633            'share/foo/foo.dat',
3634            'include/foo.h',
3635            'foo/foofile',
3636            'bin/foo' + exe_suffix,
3637        ]
3638        bar_expected = [
3639            'bar',
3640            'share/foo/bar.dat',
3641            'include/bar.h',
3642            'bin/bar' + exe_suffix,
3643            'bar/barfile'
3644        ]
3645        env = get_fake_env(testdir, self.builddir, self.prefix)
3646        cc = detect_c_compiler(env, MachineChoice.HOST)
3647        if cc.get_argument_syntax() == 'msvc':
3648            main_expected.append('bin/foo.pdb')
3649            bar_expected.append('bin/bar.pdb')
3650        prefix = destdir_join(self.installdir, self.prefix)
3651        main_expected = [Path(prefix, p) for p in main_expected]
3652        bar_expected = [Path(prefix, p) for p in bar_expected]
3653        all_expected = main_expected + bar_expected
3654
3655        def check_installed_files(extra_args, expected):
3656            args = ['install', '--destdir', self.installdir] + extra_args
3657            self._run(self.meson_command + args, workdir=self.builddir)
3658            all_files = [p for p in Path(self.installdir).rglob('*')]
3659            self.assertEqual(sorted(expected), sorted(all_files))
3660            windows_proof_rmtree(self.installdir)
3661
3662        check_installed_files([], all_expected)
3663        check_installed_files(['--skip-subprojects'], main_expected)
3664        check_installed_files(['--skip-subprojects', 'bar'], main_expected)
3665        check_installed_files(['--skip-subprojects', 'another'], all_expected)
3666
3667    def test_adding_subproject_to_configure_project(self) -> None:
3668        srcdir = os.path.join(self.unit_test_dir, '93 new subproject in configured project')
3669        self.init(srcdir)
3670        self.build()
3671        self.setconf('-Duse-sub=true')
3672        self.build()
3673
3674    def test_devenv(self):
3675        testdir = os.path.join(self.unit_test_dir, '91 devenv')
3676        self.init(testdir)
3677        self.build()
3678
3679        cmd = self.meson_command + ['devenv', '-C', self.builddir]
3680        script = os.path.join(testdir, 'test-devenv.py')
3681        app = os.path.join(self.builddir, 'app')
3682        self._run(cmd + python_command + [script])
3683        self.assertEqual('This is text.', self._run(cmd + [app]).strip())
3684
3685    def test_clang_format_check(self):
3686        if self.backend is not Backend.ninja:
3687            raise SkipTest(f'Skipping clang-format tests with {self.backend.name} backend')
3688        if not shutil.which('clang-format'):
3689            raise SkipTest('clang-format not found')
3690
3691        testdir = os.path.join(self.unit_test_dir, '94 clangformat')
3692        newdir = os.path.join(self.builddir, 'testdir')
3693        shutil.copytree(testdir, newdir)
3694        self.new_builddir()
3695        self.init(newdir)
3696
3697        # Should reformat 1 file but not return error
3698        output = self.build('clang-format')
3699        self.assertEqual(1, output.count('File reformatted:'))
3700
3701        # Reset source tree then try again with clang-format-check, it should
3702        # return an error code this time.
3703        windows_proof_rmtree(newdir)
3704        shutil.copytree(testdir, newdir)
3705        with self.assertRaises(subprocess.CalledProcessError):
3706            output = self.build('clang-format-check')
3707            self.assertEqual(1, output.count('File reformatted:'))
3708
3709        # The check format should not touch any files. Thus
3710        # running format again has some work to do.
3711        output = self.build('clang-format')
3712        self.assertEqual(1, output.count('File reformatted:'))
3713        self.build('clang-format-check')
3714
3715    def test_custom_target_implicit_include(self):
3716        testdir = os.path.join(self.unit_test_dir, '95 custominc')
3717        self.init(testdir)
3718        self.build()
3719        compdb = self.get_compdb()
3720        matches = 0
3721        for c in compdb:
3722            if 'prog.c' in c['file']:
3723                self.assertNotIn('easytogrepfor', c['command'])
3724                matches += 1
3725        self.assertEqual(matches, 1)
3726        matches = 0
3727        for c in compdb:
3728            if 'prog2.c' in c['file']:
3729                self.assertIn('easytogrepfor', c['command'])
3730                matches += 1
3731        self.assertEqual(matches, 1)
3732
3733    def test_env_flags_to_linker(self) -> None:
3734        # Compilers that act as drivers should add their compiler flags to the
3735        # linker, those that do not shouldn't
3736        with mock.patch.dict(os.environ, {'CFLAGS': '-DCFLAG', 'LDFLAGS': '-flto'}):
3737            env = get_fake_env()
3738
3739            # Get the compiler so we know which compiler class to mock.
3740            cc =  detect_compiler_for(env, 'c', MachineChoice.HOST)
3741            cc_type = type(cc)
3742
3743            # Test a compiler that acts as a linker
3744            with mock.patch.object(cc_type, 'INVOKES_LINKER', True):
3745                cc =  detect_compiler_for(env, 'c', MachineChoice.HOST)
3746                link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language)
3747                self.assertEqual(sorted(link_args), sorted(['-DCFLAG', '-flto']))
3748
3749            # And one that doesn't
3750            with mock.patch.object(cc_type, 'INVOKES_LINKER', False):
3751                cc =  detect_compiler_for(env, 'c', MachineChoice.HOST)
3752                link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language)
3753                self.assertEqual(sorted(link_args), sorted(['-flto']))
3754
3755    def test_install_tag(self) -> None:
3756        testdir = os.path.join(self.unit_test_dir, '98 install all targets')
3757        self.init(testdir)
3758        self.build()
3759
3760        env = get_fake_env(testdir, self.builddir, self.prefix)
3761        cc = detect_c_compiler(env, MachineChoice.HOST)
3762
3763        def shared_lib_name(name):
3764            if cc.get_id() in {'msvc', 'clang-cl'}:
3765                return f'bin/{name}.dll'
3766            elif is_windows():
3767                return f'bin/lib{name}.dll'
3768            elif is_cygwin():
3769                return f'bin/cyg{name}.dll'
3770            elif is_osx():
3771                return f'lib/lib{name}.dylib'
3772            return f'lib/lib{name}.so'
3773
3774        def exe_name(name):
3775            if is_windows() or is_cygwin():
3776                return f'{name}.exe'
3777            return name
3778
3779        installpath = Path(self.installdir)
3780
3781        expected_common = {
3782            installpath,
3783            Path(installpath, 'usr'),
3784        }
3785
3786        expected_devel = expected_common | {
3787            Path(installpath, 'usr/include'),
3788            Path(installpath, 'usr/include/bar-devel.h'),
3789            Path(installpath, 'usr/include/bar2-devel.h'),
3790            Path(installpath, 'usr/include/foo1-devel.h'),
3791            Path(installpath, 'usr/include/foo2-devel.h'),
3792            Path(installpath, 'usr/include/foo3-devel.h'),
3793            Path(installpath, 'usr/include/out-devel.h'),
3794            Path(installpath, 'usr/lib'),
3795            Path(installpath, 'usr/lib/libstatic.a'),
3796            Path(installpath, 'usr/lib/libboth.a'),
3797            Path(installpath, 'usr/lib/libboth2.a'),
3798        }
3799
3800        if cc.get_id() in {'msvc', 'clang-cl'}:
3801            expected_devel |= {
3802                Path(installpath, 'usr/bin'),
3803                Path(installpath, 'usr/bin/app.pdb'),
3804                Path(installpath, 'usr/bin/app2.pdb'),
3805                Path(installpath, 'usr/bin/both.pdb'),
3806                Path(installpath, 'usr/bin/both2.pdb'),
3807                Path(installpath, 'usr/bin/bothcustom.pdb'),
3808                Path(installpath, 'usr/bin/shared.pdb'),
3809                Path(installpath, 'usr/lib/both.lib'),
3810                Path(installpath, 'usr/lib/both2.lib'),
3811                Path(installpath, 'usr/lib/bothcustom.lib'),
3812                Path(installpath, 'usr/lib/shared.lib'),
3813            }
3814        elif is_windows() or is_cygwin():
3815            expected_devel |= {
3816                Path(installpath, 'usr/lib/libboth.dll.a'),
3817                Path(installpath, 'usr/lib/libboth2.dll.a'),
3818                Path(installpath, 'usr/lib/libshared.dll.a'),
3819                Path(installpath, 'usr/lib/libbothcustom.dll.a'),
3820            }
3821
3822        expected_runtime = expected_common | {
3823            Path(installpath, 'usr/bin'),
3824            Path(installpath, 'usr/bin/' + exe_name('app')),
3825            Path(installpath, 'usr/bin/' + exe_name('app2')),
3826            Path(installpath, 'usr/' + shared_lib_name('shared')),
3827            Path(installpath, 'usr/' + shared_lib_name('both')),
3828            Path(installpath, 'usr/' + shared_lib_name('both2')),
3829        }
3830
3831        expected_custom = expected_common | {
3832            Path(installpath, 'usr/share'),
3833            Path(installpath, 'usr/share/bar-custom.txt'),
3834            Path(installpath, 'usr/share/foo-custom.h'),
3835            Path(installpath, 'usr/share/out1-custom.txt'),
3836            Path(installpath, 'usr/share/out2-custom.txt'),
3837            Path(installpath, 'usr/share/out3-custom.txt'),
3838            Path(installpath, 'usr/share/custom_files'),
3839            Path(installpath, 'usr/share/custom_files/data.txt'),
3840            Path(installpath, 'usr/lib'),
3841            Path(installpath, 'usr/lib/libbothcustom.a'),
3842            Path(installpath, 'usr/' + shared_lib_name('bothcustom')),
3843        }
3844
3845        if is_windows() or is_cygwin():
3846            expected_custom |= {Path(installpath, 'usr/bin')}
3847        else:
3848            expected_runtime |= {Path(installpath, 'usr/lib')}
3849
3850        expected_runtime_custom = expected_runtime | expected_custom
3851
3852        expected_all = expected_devel | expected_runtime | expected_custom | {
3853            Path(installpath, 'usr/share/foo-notag.h'),
3854            Path(installpath, 'usr/share/bar-notag.txt'),
3855            Path(installpath, 'usr/share/out1-notag.txt'),
3856            Path(installpath, 'usr/share/out2-notag.txt'),
3857            Path(installpath, 'usr/share/out3-notag.txt'),
3858            Path(installpath, 'usr/share/foo2.h'),
3859            Path(installpath, 'usr/share/out1.txt'),
3860            Path(installpath, 'usr/share/out2.txt'),
3861        }
3862
3863        def do_install(tags=None):
3864            extra_args = ['--tags', tags] if tags else []
3865            self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir] + extra_args, workdir=self.builddir)
3866            installed = self.read_install_logs()
3867            return sorted(installed)
3868
3869        self.assertEqual(sorted(expected_devel), do_install('devel'))
3870        self.assertEqual(sorted(expected_runtime), do_install('runtime'))
3871        self.assertEqual(sorted(expected_custom), do_install('custom'))
3872        self.assertEqual(sorted(expected_runtime_custom), do_install('runtime,custom'))
3873        self.assertEqual(sorted(expected_all), do_install())
3874
3875    def test_introspect_install_plan(self):
3876        testdir = os.path.join(self.unit_test_dir, '98 install all targets')
3877        introfile = os.path.join(self.builddir, 'meson-info', 'intro-install_plan.json')
3878        self.init(testdir)
3879        self.assertPathExists(introfile)
3880        with open(introfile, encoding='utf-8') as fp:
3881            res = json.load(fp)
3882
3883        env = get_fake_env(testdir, self.builddir, self.prefix)
3884
3885        def output_name(name, type_):
3886            return type_(name=name, subdir=None, subproject=None,
3887                         for_machine=MachineChoice.HOST, sources=[],
3888                         objects=[], environment=env, kwargs={}).filename
3889
3890        shared_lib_name = lambda name: output_name(name, SharedLibrary)
3891        static_lib_name = lambda name: output_name(name, StaticLibrary)
3892        exe_name = lambda name: output_name(name, Executable)
3893
3894        expected = {
3895            'targets': {
3896                f'{self.builddir}/out1-notag.txt': {
3897                    'destination': '{prefix}/share/out1-notag.txt',
3898                    'tag': None,
3899                },
3900                f'{self.builddir}/out2-notag.txt': {
3901                    'destination': '{prefix}/share/out2-notag.txt',
3902                    'tag': None,
3903                },
3904                f'{self.builddir}/libstatic.a': {
3905                    'destination': '{libdir_static}/libstatic.a',
3906                    'tag': 'devel',
3907                },
3908                f'{self.builddir}/' + exe_name('app'): {
3909                    'destination': '{bindir}/' + exe_name('app'),
3910                    'tag': 'runtime',
3911                },
3912                f'{self.builddir}/subdir/' + exe_name('app2'): {
3913                    'destination': '{bindir}/' + exe_name('app2'),
3914                    'tag': 'runtime',
3915                },
3916                f'{self.builddir}/' + shared_lib_name('shared'): {
3917                    'destination': '{libdir_shared}/' + shared_lib_name('shared'),
3918                    'tag': 'runtime',
3919                },
3920                f'{self.builddir}/' + shared_lib_name('both'): {
3921                    'destination': '{libdir_shared}/' + shared_lib_name('both'),
3922                    'tag': 'runtime',
3923                },
3924                f'{self.builddir}/' + static_lib_name('both'): {
3925                    'destination': '{libdir_static}/' + static_lib_name('both'),
3926                    'tag': 'devel',
3927                },
3928                f'{self.builddir}/' + shared_lib_name('bothcustom'): {
3929                    'destination': '{libdir_shared}/' + shared_lib_name('bothcustom'),
3930                    'tag': 'custom',
3931                },
3932                f'{self.builddir}/' + static_lib_name('bothcustom'): {
3933                    'destination': '{libdir_static}/' + static_lib_name('bothcustom'),
3934                    'tag': 'custom',
3935                },
3936                f'{self.builddir}/subdir/' + shared_lib_name('both2'): {
3937                    'destination': '{libdir_shared}/' + shared_lib_name('both2'),
3938                    'tag': 'runtime',
3939                },
3940                f'{self.builddir}/subdir/' + static_lib_name('both2'): {
3941                    'destination': '{libdir_static}/' + static_lib_name('both2'),
3942                    'tag': 'devel',
3943                },
3944                f'{self.builddir}/out1-custom.txt': {
3945                    'destination': '{prefix}/share/out1-custom.txt',
3946                    'tag': 'custom',
3947                },
3948                f'{self.builddir}/out2-custom.txt': {
3949                    'destination': '{prefix}/share/out2-custom.txt',
3950                    'tag': 'custom',
3951                },
3952                f'{self.builddir}/out3-custom.txt': {
3953                    'destination': '{prefix}/share/out3-custom.txt',
3954                    'tag': 'custom',
3955                },
3956                f'{self.builddir}/subdir/out1.txt': {
3957                    'destination': '{prefix}/share/out1.txt',
3958                    'tag': None,
3959                },
3960                f'{self.builddir}/subdir/out2.txt': {
3961                    'destination': '{prefix}/share/out2.txt',
3962                    'tag': None,
3963                },
3964                f'{self.builddir}/out-devel.h': {
3965                    'destination': '{prefix}/include/out-devel.h',
3966                    'tag': 'devel',
3967                },
3968                f'{self.builddir}/out3-notag.txt': {
3969                    'destination': '{prefix}/share/out3-notag.txt',
3970                    'tag': None,
3971                },
3972            },
3973            'configure': {
3974                f'{self.builddir}/foo-notag.h': {
3975                    'destination': '{prefix}/share/foo-notag.h',
3976                    'tag': None,
3977                },
3978                f'{self.builddir}/foo2-devel.h': {
3979                    'destination': '{prefix}/include/foo2-devel.h',
3980                    'tag': 'devel',
3981                },
3982                f'{self.builddir}/foo-custom.h': {
3983                    'destination': '{prefix}/share/foo-custom.h',
3984                    'tag': 'custom',
3985                },
3986                f'{self.builddir}/subdir/foo2.h': {
3987                    'destination': '{prefix}/share/foo2.h',
3988                    'tag': None,
3989                },
3990            },
3991            'data': {
3992                f'{testdir}/bar-notag.txt': {
3993                    'destination': '{datadir}/share/bar-notag.txt',
3994                    'tag': None,
3995                },
3996                f'{testdir}/bar-devel.h': {
3997                    'destination': '{datadir}/include/bar-devel.h',
3998                    'tag': 'devel',
3999                },
4000                f'{testdir}/bar-custom.txt': {
4001                    'destination': '{datadir}/share/bar-custom.txt',
4002                    'tag': 'custom',
4003                },
4004                f'{testdir}/subdir/bar2-devel.h': {
4005                    'destination': '{datadir}/include/bar2-devel.h',
4006                    'tag': 'devel',
4007                },
4008            },
4009            'headers': {
4010                f'{testdir}/foo1-devel.h': {
4011                    'destination': '{includedir}/foo1-devel.h',
4012                    'tag': 'devel',
4013                },
4014                f'{testdir}/subdir/foo3-devel.h': {
4015                    'destination': '{includedir}/foo3-devel.h',
4016                    'tag': 'devel',
4017                },
4018            }
4019        }
4020
4021        fix_path = lambda path: os.path.sep.join(path.split('/'))
4022        expected_fixed = {
4023            data_type: {
4024                fix_path(source): {
4025                    key: fix_path(value) if key == 'destination' else value
4026                    for key, value in attributes.items()
4027                }
4028                for source, attributes in files.items()
4029            }
4030            for data_type, files in expected.items()
4031        }
4032
4033        for data_type, files in expected_fixed.items():
4034            for file, details in files.items():
4035                with self.subTest(key='{}.{}'.format(data_type, file)):
4036                    self.assertEqual(res[data_type][file], details)
4037
4038    @skip_if_not_language('rust')
4039    @unittest.skipIf(not shutil.which('clippy-driver'), 'Test requires clippy-driver')
4040    def test_rust_clippy(self) -> None:
4041        if self.backend is not Backend.ninja:
4042            raise unittest.SkipTest('Rust is only supported with ninja currently')
4043        # Wehn clippy is used, we should get an exception since a variable named
4044        # "foo" is used, but is on our denylist
4045        testdir = os.path.join(self.rust_test_dir, '1 basic')
4046        self.init(testdir, extra_args=['--werror'], override_envvars={'RUSTC': 'clippy-driver'})
4047        with self.assertRaises(subprocess.CalledProcessError) as cm:
4048            self.build()
4049        self.assertIn('error: use of a blacklisted/placeholder name `foo`', cm.exception.stdout)
4050