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